git-blame-ignore 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ export interface BulkCommit {
2
+ sha: string;
3
+ message: string;
4
+ filesChanged: number;
5
+ linesChanged: number;
6
+ whitespaceRatio: number;
7
+ bulkScore: number;
8
+ date: string;
9
+ }
10
+ export interface IgnoreEntry {
11
+ sha: string;
12
+ message: string;
13
+ date: string;
14
+ comment?: string;
15
+ }
16
+ export interface GitBlameIgnoreConfig {
17
+ ignoreFile: string;
18
+ minFilesChanged: number;
19
+ minBulkScore: number;
20
+ whitespaceThreshold: number;
21
+ }
22
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,CAAC;CAC7B"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/jest.config.js ADDED
@@ -0,0 +1,16 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src', '<rootDir>/test'],
5
+ testMatch: ['**/*.test.ts'],
6
+ transform: {
7
+ '^.+\\.ts$': 'ts-jest',
8
+ },
9
+ collectCoverageFrom: [
10
+ 'src/**/*.ts',
11
+ '!src/**/*.test.ts',
12
+ '!src/cli.ts',
13
+ ],
14
+ coverageDirectory: 'coverage',
15
+ coverageReporters: ['text', 'lcov', 'html'],
16
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "git-blame-ignore",
3
+ "version": "1.0.0",
4
+ "description": "CLI that auto-detects bulk-change commits and manages .git-blame-ignore-revs file",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "git-blame-ignore": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/cli.ts",
12
+ "lint": "eslint src --ext .ts",
13
+ "test": "jest",
14
+ "test:watch": "jest --watch",
15
+ "prepublishOnly": "npm run lint && npm run build && npm test"
16
+ },
17
+ "keywords": [
18
+ "git",
19
+ "blame",
20
+ "prettier",
21
+ "eslint",
22
+ "format",
23
+ "development",
24
+ "cli",
25
+ "utility"
26
+ ],
27
+ "author": "Sulthonzh",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "chalk": "^4.1.2",
31
+ "commander": "^11.0.0",
32
+ "inquirer": "^9.2.0",
33
+ "simple-git": "^3.20.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/inquirer": "^9.0.9",
37
+ "@types/jest": "^29.5.0",
38
+ "@types/node": "^20.0.0",
39
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
40
+ "@typescript-eslint/parser": "^6.0.0",
41
+ "eslint": "^8.45.0",
42
+ "jest": "^29.6.0",
43
+ "ts-jest": "^29.1.0",
44
+ "ts-node": "^10.9.0",
45
+ "typescript": "^5.1.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ }
50
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,198 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { BulkCommitScanner } from './scanner';
4
+ import { GitBlameIgnoreFileManager } from './file-manager';
5
+
6
+
7
+ export class GitBlameIgnoreCLI {
8
+ private scanner: BulkCommitScanner;
9
+ private fileManager: GitBlameIgnoreFileManager;
10
+
11
+ constructor() {
12
+ this.scanner = new BulkCommitScanner();
13
+ this.fileManager = new GitBlameIgnoreFileManager();
14
+ }
15
+
16
+ async init(): Promise<void> {
17
+ console.log(chalk.blue('šŸ” git-blame-ignore init - Interactive Setup Wizard'));
18
+ console.log(chalk.gray('Scanning for bulk-change commits...\n'));
19
+
20
+ try {
21
+ const commits = await this.scanner.scan();
22
+
23
+ if (commits.length === 0) {
24
+ console.log(chalk.yellow('šŸ¤” No bulk-change commits found that meet the criteria.'));
25
+ console.log(chalk.gray('Try running formatting tools like prettier or eslint --fix first.'));
26
+ return;
27
+ }
28
+
29
+ console.log(chalk.green(`āœ… Found ${commits.length} bulk-change commits:\n`));
30
+
31
+ // Display commits
32
+ commits.forEach((commit, index) => {
33
+ const scoreColor = commit.bulkScore >= 70 ? chalk.green : commit.bulkScore >= 50 ? chalk.yellow : chalk.red;
34
+ console.log(`${index + 1}. ${commit.sha.substring(0, 7)} ${scoreColor(`[${commit.bulkScore}/100]`)} ${commit.message}`);
35
+ console.log(` šŸ“ ${commit.filesChanged} files changed, šŸ“ ${commit.linesChanged} lines changed`);
36
+ console.log(` šŸ“… ${commit.date}\n`);
37
+ });
38
+
39
+ // Ask user to select commits
40
+ const answers = await inquirer.prompt([
41
+ {
42
+ type: 'checkbox',
43
+ name: 'commits',
44
+ message: 'Which commits would you like to add to .git-blame-ignore-revs?',
45
+ choices: commits.map((commit) => ({
46
+ name: `${commit.sha.substring(0, 7)} - ${commit.message} (${commit.filesChanged} files)`,
47
+ value: commit.sha,
48
+ short: commit.sha.substring(0, 7)
49
+ })),
50
+ pageSize: 10
51
+ }
52
+ ]);
53
+
54
+ if (answers.commits.length === 0) {
55
+ console.log(chalk.yellow('āŒ No commits selected. Exiting.'));
56
+ return;
57
+ }
58
+
59
+ // Add selected commits
60
+ for (const sha of answers.commits) {
61
+ await this.fileManager.add(sha);
62
+ }
63
+
64
+ console.log(chalk.green(`āœ… Successfully added ${answers.commits.length} commits to .git-blame-ignore-revs`));
65
+ console.log(chalk.blue('šŸ’” Now run `git blame` on affected files to see the improved output!'));
66
+
67
+ } catch (error) {
68
+ console.error(chalk.red('āŒ Error during initialization:'), error);
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ async scan(limit: number = 20): Promise<void> {
74
+ console.log(chalk.blue('šŸ” git-blame-ignore scan - Finding bulk-change commits'));
75
+ console.log(chalk.gray(`Analyzing last 100 commits...\n`));
76
+
77
+ try {
78
+ const commits = await this.scanner.scan();
79
+
80
+ if (commits.length === 0) {
81
+ console.log(chalk.yellow('šŸ¤” No bulk-change commits found that meet the criteria.'));
82
+ console.log(chalk.gray('Try running formatting tools like prettier or eslint --fix first.'));
83
+ return;
84
+ }
85
+
86
+ const displayCommits = commits.slice(0, limit);
87
+
88
+ console.log(chalk.green(`šŸ“‹ Found ${commits.length} bulk-change commits (showing first ${Math.min(limit, commits.length)}):\n`));
89
+
90
+ displayCommits.forEach((commit, index) => {
91
+ const scoreColor = commit.bulkScore >= 70 ? chalk.green : commit.bulkScore >= 50 ? chalk.yellow : chalk.red;
92
+ console.log(`${index + 1}. ${commit.sha.substring(0, 7)} ${scoreColor(`[${commit.bulkScore}/100]`)} ${commit.message}`);
93
+ console.log(` šŸ“ ${commit.filesChanged} files changed, šŸ“ ${commit.linesChanged} lines changed`);
94
+ console.log(` šŸ“… ${commit.date} | Whitespace: ${Math.round(commit.whitespaceRatio * 100)}%\n`);
95
+ });
96
+
97
+ if (commits.length > limit) {
98
+ console.log(chalk.gray(`... and ${commits.length - limit} more commits`));
99
+ }
100
+
101
+ } catch (error) {
102
+ console.error(chalk.red('āŒ Error during scan:'), error);
103
+ process.exit(1);
104
+ }
105
+ }
106
+
107
+ async add(sha: string): Promise<void> {
108
+ console.log(chalk.blue(`šŸ“ git-blame-ignore add ${sha}`));
109
+
110
+ try {
111
+ await this.fileManager.add(sha);
112
+ } catch (error) {
113
+ console.error(chalk.red('āŒ Error adding commit:'), error);
114
+ process.exit(1);
115
+ }
116
+ }
117
+
118
+ async list(): Promise<void> {
119
+ console.log(chalk.blue('šŸ“‹ git-blame-ignore list - Current .git-blame-ignore-revs entries'));
120
+
121
+ try {
122
+ const entries = await this.fileManager.read();
123
+
124
+ if (entries.length === 0) {
125
+ console.log(chalk.yellow('šŸ¤” No entries found in .git-blame-ignore-revs'));
126
+ console.log(chalk.gray('Run `git-blame-ignore init` to add some commits.'));
127
+ return;
128
+ }
129
+
130
+ console.log(chalk.green(`šŸ“‹ .git-blame-ignore-revs (${entries.length} entries):\n`));
131
+
132
+ entries.forEach((entry, index) => {
133
+ console.log(`${index + 1}. ${entry.sha.substring(0, 7)} - ${entry.message}`);
134
+ console.log(` šŸ“… ${entry.date}`);
135
+ if (entry.comment) {
136
+ console.log(` šŸ’¬ ${entry.comment}`);
137
+ }
138
+ console.log('');
139
+ });
140
+
141
+ } catch (error) {
142
+ console.error(chalk.red('āŒ Error listing entries:'), error);
143
+ process.exit(1);
144
+ }
145
+ }
146
+
147
+ async remove(sha: string): Promise<void> {
148
+ console.log(chalk.blue(`šŸ—‘ļø git-blame-ignore remove ${sha}`));
149
+
150
+ try {
151
+ await this.fileManager.remove(sha);
152
+ } catch (error) {
153
+ console.error(chalk.red('āŒ Error removing commit:'), error);
154
+ process.exit(1);
155
+ }
156
+ }
157
+
158
+ async check(): Promise<void> {
159
+ console.log(chalk.blue('āœ… git-blame-ignore check - Validating entries'));
160
+
161
+ try {
162
+ const entries = await this.fileManager.read();
163
+
164
+ if (entries.length === 0) {
165
+ console.log(chalk.yellow('šŸ¤” No entries to check.'));
166
+ return;
167
+ }
168
+
169
+ const { valid, invalid } = await this.fileManager.validate(entries);
170
+
171
+ console.log(chalk.green(`āœ… Valid entries: ${valid.length}`));
172
+ console.log(chalk.red(`āŒ Invalid entries: ${invalid.length}\n`));
173
+
174
+ if (invalid.length > 0) {
175
+ console.log(chalk.red('Invalid entries (SHAs not found in repository):'));
176
+ invalid.forEach(entry => {
177
+ console.log(` - ${entry.sha.substring(0, 7)} - ${entry.message}`);
178
+ });
179
+ console.log('');
180
+ }
181
+
182
+ if (valid.length > 0) {
183
+ console.log(chalk.green('Valid entries:'));
184
+ valid.forEach(entry => {
185
+ console.log(` āœ… ${entry.sha.substring(0, 7)} - ${entry.message}`);
186
+ });
187
+ }
188
+
189
+ if (invalid.length > 0) {
190
+ console.log(chalk.yellow('\nšŸ’” Tip: Remove invalid entries with `git-blame-ignore remove <sha>`'));
191
+ }
192
+
193
+ } catch (error) {
194
+ console.error(chalk.red('āŒ Error during validation:'), error);
195
+ process.exit(1);
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,150 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { IgnoreEntry } from './types';
4
+
5
+ export class GitBlameIgnoreFileManager {
6
+ private ignoreFilePath: string;
7
+
8
+ constructor(ignoreFilePath?: string) {
9
+ this.ignoreFilePath = ignoreFilePath || '.git-blame-ignore-revs';
10
+ }
11
+
12
+ async exists(): Promise<boolean> {
13
+ try {
14
+ await fs.access(this.ignoreFilePath);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ async read(): Promise<IgnoreEntry[]> {
22
+ if (!(await this.exists())) {
23
+ return [];
24
+ }
25
+
26
+ try {
27
+ const content = await fs.readFile(this.ignoreFilePath, 'utf-8');
28
+ const entries: IgnoreEntry[] = [];
29
+
30
+ const lines = content.split('\n').filter(line => line.trim() && !line.trim().startsWith('#'));
31
+
32
+ for (const line of lines) {
33
+ const sha = line.trim();
34
+ if (sha && /^[a-f0-9]{40}$/i.test(sha)) {
35
+ const entry = await this.createIgnoreEntry(sha);
36
+ if (entry) {
37
+ entries.push(entry);
38
+ }
39
+ }
40
+ }
41
+
42
+ return entries;
43
+ } catch (error) {
44
+ throw new Error(`Failed to read ${this.ignoreFilePath}: ${error}`);
45
+ }
46
+ }
47
+
48
+ async add(sha: string, comment?: string): Promise<void> {
49
+ const entry = await this.createIgnoreEntry(sha);
50
+ if (!entry) {
51
+ throw new Error(`Invalid commit SHA: ${sha}`);
52
+ }
53
+
54
+ const entries = await this.read();
55
+
56
+ // Check if already exists
57
+ if (entries.some(e => e.sha === sha)) {
58
+ console.log(`Entry ${sha.substring(0, 7)} already exists in ${this.ignoreFilePath}`);
59
+ return;
60
+ }
61
+
62
+ entries.push({
63
+ ...entry,
64
+ comment: comment || `Auto-added by git-blame-ignore`
65
+ });
66
+
67
+ await this.write(entries);
68
+ console.log(`Added ${sha.substring(0, 7)} to ${this.ignoreFilePath}`);
69
+ }
70
+
71
+ async remove(sha: string): Promise<void> {
72
+ const entries = await this.read();
73
+ const filtered = entries.filter(e => e.sha !== sha);
74
+
75
+ if (filtered.length === entries.length) {
76
+ console.log(`Entry ${sha.substring(0, 7)} not found in ${this.ignoreFilePath}`);
77
+ return;
78
+ }
79
+
80
+ await this.write(filtered);
81
+ console.log(`Removed ${sha.substring(0, 7)} from ${this.ignoreFilePath}`);
82
+ }
83
+
84
+ async write(entries: IgnoreEntry[]): Promise<void> {
85
+ try {
86
+ // Create directory if it doesn't exist
87
+ const dir = path.dirname(this.ignoreFilePath);
88
+ await fs.mkdir(dir, { recursive: true });
89
+
90
+ let content = '# .git-blame-ignore-revs - managed by git-blame-ignore\n';
91
+ content += '# Format: SHA of commits to ignore for git blame\n\n';
92
+
93
+ for (const entry of entries) {
94
+ content += `# ${entry.sha.substring(0, 7)} - ${entry.message} (${entry.date})\n`;
95
+ content += `${entry.sha}\n\n`;
96
+ }
97
+
98
+ await fs.writeFile(this.ignoreFilePath, content);
99
+ } catch (error) {
100
+ throw new Error(`Failed to write ${this.ignoreFilePath}: ${error}`);
101
+ }
102
+ }
103
+
104
+ async validate(entries: IgnoreEntry[]): Promise<{ valid: IgnoreEntry[]; invalid: IgnoreEntry[] }> {
105
+ const valid: IgnoreEntry[] = [];
106
+ const invalid: IgnoreEntry[] = [];
107
+
108
+ for (const entry of entries) {
109
+ try {
110
+ // Check if SHA is a valid 40-character hex string
111
+ if (!/^[a-f0-9]{40}$/i.test(entry.sha)) {
112
+ invalid.push(entry);
113
+ continue;
114
+ }
115
+
116
+ // Try to get commit info to verify it exists
117
+ const git = require('simple-git')();
118
+ const result = await git.show([entry.sha, '--format=%H', '--no-patch']);
119
+
120
+ if (result.trim() === entry.sha) {
121
+ valid.push(entry);
122
+ } else {
123
+ invalid.push(entry);
124
+ }
125
+ } catch {
126
+ invalid.push(entry);
127
+ }
128
+ }
129
+
130
+ return { valid, invalid };
131
+ }
132
+
133
+ private async createIgnoreEntry(sha: string): Promise<IgnoreEntry | null> {
134
+ try {
135
+ const git = require('simple-git')();
136
+ const [message, date] = await Promise.all([
137
+ git.show([sha, '--format=%s', '--no-patch']),
138
+ git.show([sha, '--format=%ai', '--no-patch'])
139
+ ]);
140
+
141
+ return {
142
+ sha,
143
+ message: message.trim(),
144
+ date: date.trim(),
145
+ };
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ }
package/src/index.ts ADDED
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import simpleGit from 'simple-git';
6
+ import { readFile, writeFile } from 'fs/promises';
7
+ import { join } from 'path';
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('git-blame-ignore')
13
+ .description('Auto-detects bulk-change commits and manages .git-blame-ignore-revs file')
14
+ .version('1.0.0');
15
+
16
+ program
17
+ .command('scan')
18
+ .description('Scan repository for bulk-change commits')
19
+ .option('-n, --number <number>', 'Number of commits to check (default: 50)', '50')
20
+ .option('-t, --threshold <threshold>', 'Threshold for bulk changes (default: 10)', '10')
21
+ .action(async (options) => {
22
+ try {
23
+ await scanBulkCommits(parseInt(options.number), parseInt(options.threshold));
24
+ } catch (error) {
25
+ console.error(chalk.red('Error:'), (error as Error).message);
26
+ process.exit(1);
27
+ }
28
+ });
29
+
30
+ program
31
+ .command('ignore')
32
+ .description('Add bulk-change commits to .git-blame-ignore-revs')
33
+ .option('-c, --commits <commits>', 'Comma-separated list of commit hashes to ignore')
34
+ .option('-a, --auto', 'Auto-scan and add all detected bulk commits')
35
+ .action(async (options) => {
36
+ try {
37
+ if (options.auto) {
38
+ const bulkCommits = await scanBulkCommits(50, 10);
39
+ if (bulkCommits.length > 0) {
40
+ await addToIgnoreFile(bulkCommits.map(commit => commit.hash));
41
+ }
42
+ } else if (options.commits) {
43
+ const commitHashes = options.commits.split(',').map((hash: string) => hash.trim());
44
+ await addToIgnoreFile(commitHashes);
45
+ } else {
46
+ console.log(chalk.yellow('Please specify either --commits or --auto'));
47
+ }
48
+ } catch (error) {
49
+ console.error(chalk.red('Error:'), (error as Error).message);
50
+ process.exit(1);
51
+ }
52
+ });
53
+
54
+ program
55
+ .command('list')
56
+ .description('List commits currently ignored in .git-blame-ignore-revs')
57
+ .action(async () => {
58
+ try {
59
+ await listIgnoredCommits();
60
+ } catch (error) {
61
+ console.error(chalk.red('Error:'), (error as Error).message);
62
+ process.exit(1);
63
+ }
64
+ });
65
+
66
+ program
67
+ .command('remove')
68
+ .description('Remove commits from .git-blame-ignore-revs')
69
+ .option('-c, --commits <commits>', 'Comma-separated list of commit hashes to remove')
70
+ .option('-a, --all', 'Remove all ignored commits')
71
+ .action(async (options) => {
72
+ try {
73
+ if (options.all) {
74
+ await removeAllIgnoredCommits();
75
+ } else if (options.commits) {
76
+ const commitHashes = options.commits.split(',').map((hash: string) => hash.trim());
77
+ await removeFromIgnoreFile(commitHashes);
78
+ } else {
79
+ console.log(chalk.yellow('Please specify either --commits or --all'));
80
+ }
81
+ } catch (error) {
82
+ console.error(chalk.red('Error:'), (error as Error).message);
83
+ process.exit(1);
84
+ }
85
+ });
86
+
87
+ async function scanBulkCommits(limit: number, threshold: number) {
88
+ const git = simpleGit();
89
+
90
+ // Check if we're in a git repository
91
+ try {
92
+ await git.revparse(['--is-inside-work-tree']);
93
+ } catch (error) {
94
+ throw new Error('Not a git repository');
95
+ }
96
+
97
+ console.log(chalk.blue(`Scanning last ${limit} commits for bulk changes...`));
98
+
99
+ const logOutput = await git.log(['--pretty=format:%H %s %an %ad', `--max-count=${limit}`, '--date=short']);
100
+ const commits = logOutput.all;
101
+
102
+ const bulkCommits: Array<{ hash: string; subject: string; author: string; date: string; changes: number }> = [];
103
+
104
+ for (const commit of commits) {
105
+ try {
106
+ // Get number of changed files in this commit
107
+ const diffSummary = await git.diffSummary([`${commit.hash}^`, commit.hash]);
108
+ const changesCount = diffSummary.files.length;
109
+
110
+ if (changesCount >= threshold) {
111
+ bulkCommits.push({
112
+ hash: commit.hash,
113
+ subject: commit.message.split('\n')[0],
114
+ author: commit.author_name,
115
+ date: commit.date,
116
+ changes: changesCount
117
+ });
118
+ }
119
+ } catch (error) {
120
+ console.warn(chalk.yellow(`Warning: Could not analyze commit ${commit.hash}: ${(error as Error).message}`));
121
+ }
122
+ }
123
+
124
+ if (bulkCommits.length === 0) {
125
+ console.log(chalk.green('āœ… No bulk-change commits found'));
126
+ return [];
127
+ }
128
+
129
+ console.log(chalk.blue('\nšŸ” Bulk-change commits detected:'));
130
+ console.table(bulkCommits.map(commit => ({
131
+ Hash: commit.hash.substring(0, 8),
132
+ Subject: commit.subject,
133
+ Author: commit.author,
134
+ Date: commit.date,
135
+ Changes: commit.changes
136
+ })));
137
+
138
+ return bulkCommits;
139
+ }
140
+
141
+ async function addToIgnoreFile(commitHashes: string[]) {
142
+ const gitignorePath = join(process.cwd(), '.git-blame-ignore-revs');
143
+ let existingContent = '';
144
+
145
+ try {
146
+ existingContent = await readFile(gitignorePath, 'utf-8');
147
+ } catch (error) {
148
+ // File doesn't exist, that's okay
149
+ }
150
+
151
+ const newCommits = commitHashes.filter(hash => !existingContent.includes(hash));
152
+
153
+ if (newCommits.length === 0) {
154
+ console.log(chalk.green('āœ… All specified commits are already ignored'));
155
+ return;
156
+ }
157
+
158
+ const updatedContent = existingContent + (existingContent ? '\n' : '') + newCommits.join('\n') + '\n';
159
+
160
+ try {
161
+ await writeFile(gitignorePath, updatedContent, 'utf-8');
162
+ console.log(chalk.green(`āœ… Added ${newCommits.length} commit(s) to .git-blame-ignore-revs`));
163
+ console.log(chalk.blue('šŸ’” Run `git blame --ignore-revs` to use the ignore file'));
164
+ } catch (error) {
165
+ throw new Error(`Failed to write .git-blame-ignore-revs: ${(error as Error).message}`);
166
+ }
167
+ }
168
+
169
+ async function listIgnoredCommits() {
170
+ const gitignorePath = join(process.cwd(), '.git-blame-ignore-revs');
171
+
172
+ try {
173
+ const content = await readFile(gitignorePath, 'utf-8');
174
+ const commits = content.split('\n').filter(line => line.trim() && !line.startsWith('#'));
175
+
176
+ if (commits.length === 0) {
177
+ console.log(chalk.yellow('No commits are currently ignored'));
178
+ return;
179
+ }
180
+
181
+ console.log(chalk.blue('šŸ“ Currently ignored commits:'));
182
+ commits.forEach((commit, index) => {
183
+ console.log(`${index + 1}. ${commit}`);
184
+ });
185
+ } catch (error) {
186
+ throw new Error('.git-blame-ignore-revs file not found');
187
+ }
188
+ }
189
+
190
+ async function removeFromIgnoreFile(commitHashes: string[]) {
191
+ const gitignorePath = join(process.cwd(), '.git-blame-ignore-revs');
192
+
193
+ try {
194
+ const content = await readFile(gitignorePath, 'utf-8');
195
+ const lines = content.split('\n');
196
+
197
+ const filteredLines = lines.filter(line => {
198
+ return !commitHashes.includes(line.trim());
199
+ });
200
+
201
+ const updatedContent = filteredLines.join('\n');
202
+
203
+ if (updatedContent === content) {
204
+ console.log(chalk.yellow('None of the specified commits are currently ignored'));
205
+ return;
206
+ }
207
+
208
+ await writeFile(gitignorePath, updatedContent, 'utf-8');
209
+ console.log(chalk.green(`āœ… Removed ${commitHashes.length} commit(s) from .git-blame-ignore-revs`));
210
+ } catch (error) {
211
+ throw new Error('.git-blame-ignore-revs file not found');
212
+ }
213
+ }
214
+
215
+ async function removeAllIgnoredCommits() {
216
+ const gitignorePath = join(process.cwd(), '.git-blame-ignore-revs');
217
+
218
+ try {
219
+ const content = await readFile(gitignorePath, 'utf-8');
220
+ const commits = content.split('\n').filter(line => line.trim() && !line.startsWith('#'));
221
+
222
+ if (commits.length === 0) {
223
+ console.log(chalk.yellow('No commits are currently ignored'));
224
+ return;
225
+ }
226
+
227
+ // Create a backup
228
+ const backupPath = gitignorePath + '.backup';
229
+ await writeFile(backupPath, content, 'utf-8');
230
+
231
+ // Clear the file but keep the structure
232
+ await writeFile(gitignorePath, '# Git blame ignore file - auto-generated by git-blame-ignore\n', 'utf-8');
233
+
234
+ console.log(chalk.green(`āœ… Removed all ${commits.length} ignored commits`));
235
+ console.log(chalk.blue(`šŸ’” Backup saved to: ${backupPath}`));
236
+ } catch (error) {
237
+ throw new Error('.git-blame-ignore-revs file not found');
238
+ }
239
+ }
240
+
241
+ program.parse();