@harryisfish/gitt 1.5.0 → 1.6.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.
@@ -6,7 +6,6 @@ const listr2_1 = require("listr2");
6
6
  const prompts_1 = require("@inquirer/prompts");
7
7
  const minimatch_1 = require("minimatch");
8
8
  const errors_1 = require("../errors");
9
- const errors_2 = require("../errors");
10
9
  const git_1 = require("../utils/git");
11
10
  const config_1 = require("../utils/config");
12
11
  const git = (0, simple_git_1.simpleGit)();
@@ -33,16 +32,18 @@ async function cleanDeletedBranches(options = {}) {
33
32
  {
34
33
  title: 'Switch to main branch',
35
34
  task: async (ctx) => {
36
- // Check if we are on a branch that will be deleted?
37
- // For now, just try to switch to main to be safe.
38
- // But if main is checked out in another worktree, this might fail?
39
- // Let's just try.
35
+ const currentBranch = (await git.branchLocal()).current;
36
+ const willDeleteCurrent = state.deletedBranches.some(b => b.name === currentBranch);
40
37
  try {
41
38
  await git.checkout(ctx.mainBranch);
42
39
  }
43
40
  catch (e) {
44
- // If we can't checkout main (e.g. dirty state), warn but continue?
45
- // Ideally we should be on main to delete other branches safely.
41
+ // If current branch will be deleted, we must switch - fail the operation
42
+ if (willDeleteCurrent) {
43
+ throw new errors_1.GitError(`Cannot switch to ${ctx.mainBranch}. Current branch "${currentBranch}" will be deleted but checkout failed.`);
44
+ }
45
+ // Otherwise, warn but continue (we can delete other branches)
46
+ console.warn(`Warning: Could not switch to ${ctx.mainBranch}, but continuing as current branch is not being deleted.`);
46
47
  }
47
48
  }
48
49
  },
@@ -110,7 +111,7 @@ async function cleanDeletedBranches(options = {}) {
110
111
  await discoveryTasks.run(state);
111
112
  // Phase 2: Interaction / Filtering
112
113
  if (state.deletedBranches.length === 0) {
113
- (0, errors_2.printSuccess)('No branches need to be cleaned up');
114
+ (0, errors_1.printSuccess)('No branches need to be cleaned up');
114
115
  return;
115
116
  }
116
117
  if (options.interactive) {
@@ -128,7 +129,7 @@ async function cleanDeletedBranches(options = {}) {
128
129
  }
129
130
  catch (e) {
130
131
  // User cancelled
131
- throw new Error('Operation cancelled');
132
+ throw new errors_1.UserCancelError('Operation cancelled');
132
133
  }
133
134
  }
134
135
  else {
@@ -141,7 +142,7 @@ async function cleanDeletedBranches(options = {}) {
141
142
  state.deletedBranches = state.deletedBranches.filter(b => b.isMerged);
142
143
  }
143
144
  if (state.deletedBranches.length === 0) {
144
- (0, errors_2.printSuccess)('No branches selected for deletion');
145
+ (0, errors_1.printSuccess)('No branches selected for deletion');
145
146
  return;
146
147
  }
147
148
  if (options.dryRun) {
@@ -153,7 +154,7 @@ async function cleanDeletedBranches(options = {}) {
153
154
  const deleteTasks = new listr2_1.Listr([
154
155
  {
155
156
  title: 'Delete branches',
156
- task: (ctx) => {
157
+ task: (_ctx) => {
157
158
  return new listr2_1.Listr(state.deletedBranches.map(branch => ({
158
159
  title: `Delete ${branch.name}`,
159
160
  task: async () => {
@@ -165,7 +166,7 @@ async function cleanDeletedBranches(options = {}) {
165
166
  }
166
167
  ]);
167
168
  await deleteTasks.run(state);
168
- (0, errors_2.printSuccess)('Branch cleanup completed');
169
+ (0, errors_1.printSuccess)('Branch cleanup completed');
169
170
  }
170
171
  catch (error) {
171
172
  throw new errors_1.GitError(error instanceof Error ? error.message : 'Unknown error occurred while cleaning branches');
package/dist/errors.js CHANGED
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.UserCancelError = exports.GitError = void 0;
4
7
  exports.handleError = handleError;
5
8
  exports.printSuccess = printSuccess;
6
9
  exports.printError = printError;
10
+ const chalk_1 = __importDefault(require("chalk"));
7
11
  // 自定义错误类型
8
12
  class GitError extends Error {
9
13
  constructor(message) {
@@ -19,10 +23,6 @@ class UserCancelError extends Error {
19
23
  }
20
24
  }
21
25
  exports.UserCancelError = UserCancelError;
22
- // 错误消息颜色
23
- const ERROR_COLOR = '\x1b[31m';
24
- const SUCCESS_COLOR = '\x1b[32m';
25
- const RESET_COLOR = '\x1b[0m';
26
26
  // 统一错误处理函数
27
27
  function handleError(error) {
28
28
  if (error instanceof UserCancelError) {
@@ -30,21 +30,21 @@ function handleError(error) {
30
30
  process.exit(0);
31
31
  }
32
32
  if (error instanceof GitError) {
33
- console.error(`${ERROR_COLOR}Error: ${RESET_COLOR}${error.message}`);
33
+ console.error(chalk_1.default.red('Error:'), error.message);
34
34
  process.exit(1);
35
35
  }
36
36
  if (error instanceof Error) {
37
- console.error(`${ERROR_COLOR}Program error: ${RESET_COLOR}${error.message}`);
37
+ console.error(chalk_1.default.red('Program error:'), error.message);
38
38
  process.exit(1);
39
39
  }
40
- console.error(`${ERROR_COLOR}Unknown error occurred${RESET_COLOR}`);
40
+ console.error(chalk_1.default.red('Unknown error occurred'));
41
41
  process.exit(1);
42
42
  }
43
43
  // 成功消息处理
44
44
  function printSuccess(message) {
45
- console.log(`${SUCCESS_COLOR}${message}${RESET_COLOR}`);
45
+ console.log(chalk_1.default.green(''), message);
46
46
  }
47
47
  // 错误消息处理
48
48
  function printError(message) {
49
- console.error(`${ERROR_COLOR}${message}${RESET_COLOR}`);
49
+ console.error(chalk_1.default.red(message));
50
50
  }
package/dist/index.js CHANGED
@@ -34,10 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
37
38
  const simple_git_1 = require("simple-git");
38
39
  const errors_1 = require("./errors");
39
40
  const clean_1 = require("./commands/clean");
40
41
  const git = (0, simple_git_1.simpleGit)();
42
+ const packageJson = require('../package.json');
41
43
  // 处理 Ctrl+C 和其他终止信号
42
44
  process.on('SIGINT', () => {
43
45
  throw new errors_1.UserCancelError('\nOperation cancelled');
@@ -45,16 +47,6 @@ process.on('SIGINT', () => {
45
47
  process.on('SIGTERM', () => {
46
48
  throw new errors_1.UserCancelError('\nProgram terminated');
47
49
  });
48
- // 初始化 Git 仓库
49
- async function initGitRepo() {
50
- try {
51
- await git.init();
52
- (0, errors_1.printSuccess)('Git repository initialized successfully');
53
- }
54
- catch (error) {
55
- throw new errors_1.GitError('Failed to initialize Git repository');
56
- }
57
- }
58
50
  // 检查当前目录是否是 Git 仓库
59
51
  async function checkGitRepo() {
60
52
  const isRepo = await git.checkIsRepo();
@@ -74,104 +66,70 @@ async function checkGitRepo() {
74
66
  throw new errors_1.GitError('Cannot access remote repository, please check network connection or repository permissions');
75
67
  }
76
68
  }
77
- function printHelp() {
78
- console.log(`
79
- Usage: gitt [command] [options]
80
-
81
- Commands:
82
- (default) Clean up local branches that have been deleted on remote
83
- set-main <branch> Set the main branch for the current project
84
- ignore <pattern> Add a branch pattern to the ignore list (e.g., "release/*")
85
-
86
- Options:
87
- -i, --interactive Interactive mode: Select branches to delete
88
- -d, --dry-run Dry run: Show what would be deleted without deleting
89
- --stale [days] Find stale branches (default: 90 days, use with a number for custom)
90
- -v, --version Show version number
91
- -h, --help Show this help message
92
-
93
- Examples:
94
- gitt # Auto-clean deleted branches
95
- gitt -i # Select branches to delete interactively
96
- gitt -d # Preview deletion
97
- gitt --stale # Find branches inactive for 90+ days
98
- gitt --stale 30 # Find branches inactive for 30+ days
99
- gitt ignore "temp/*" # Ignore branches matching "temp/*"
100
- gitt set-main master # Set main branch to 'master'
101
- `);
102
- }
103
69
  async function main() {
104
70
  try {
105
- const args = process.argv.slice(2);
106
- const command = args[0];
107
- // Check for version command
108
- if (args.includes('-v') || args.includes('--version')) {
109
- const packageJson = require('../package.json');
110
- console.log(`v${packageJson.version}`);
111
- process.exit(0);
112
- }
113
- // Check for help command
114
- if (args.includes('-h') || args.includes('--help')) {
115
- printHelp();
116
- process.exit(0);
117
- }
118
- // Check for updates
71
+ // Check for updates before parsing commands
119
72
  try {
120
73
  // Use eval to prevent TypeScript from transpiling dynamic import to require()
121
74
  const { default: updateNotifier } = await eval('import("update-notifier")');
122
- const pkg = require('../package.json');
123
- updateNotifier({ pkg }).notify();
75
+ updateNotifier({ pkg: packageJson }).notify();
124
76
  }
125
77
  catch (e) {
126
78
  // Ignore update check errors
127
79
  }
128
- // 检查 Git 仓库
129
- await checkGitRepo();
130
- if (command === 'set-main') {
131
- const branch = args[1];
132
- if (!branch) {
133
- throw new Error('Please specify a branch name');
134
- }
135
- await Promise.resolve().then(() => __importStar(require('./commands/config'))).then(m => m.configMainBranch(branch));
136
- }
137
- else if (command === 'ignore') {
138
- const pattern = args[1];
139
- if (!pattern) {
140
- throw new Error('Please specify a branch pattern to ignore');
141
- }
142
- await Promise.resolve().then(() => __importStar(require('./commands/config'))).then(m => m.configIgnoreBranch(pattern));
143
- }
144
- else {
145
- // Parse options
146
- const isInteractive = args.includes('-i') || args.includes('--interactive');
147
- const isDryRun = args.includes('-d') || args.includes('--dry-run');
148
- // Parse stale options
149
- const staleIndex = args.indexOf('--stale');
150
- const isStale = staleIndex !== -1;
151
- let staleDays = 90; // Default 3 months
152
- if (isStale) {
153
- // Check if next arg is a number (days)
154
- // Note: This is a simple check, ideally use a proper arg parser like commander or yargs
155
- const nextArg = args[staleIndex + 1];
156
- if (nextArg && !nextArg.startsWith('-') && !isNaN(Number(nextArg))) {
157
- staleDays = Number(nextArg);
158
- }
159
- }
160
- // 默认执行清理操作
80
+ const program = new commander_1.Command();
81
+ program
82
+ .name('gitt')
83
+ .description('A CLI tool for Git branch management')
84
+ .version(packageJson.version, '-v, --version', 'Show version number');
85
+ // Default command: clean deleted branches
86
+ program
87
+ .option('-i, --interactive', 'Interactive mode: Select branches to delete')
88
+ .option('-d, --dry-run', 'Dry run: Show what would be deleted without deleting')
89
+ .option('--stale [days]', 'Find stale branches (default: 90 days)', '90')
90
+ .action(async (options) => {
91
+ await checkGitRepo();
92
+ const staleDays = options.stale === true ? 90 : parseInt(options.stale, 10);
93
+ const isStale = options.stale !== undefined;
161
94
  await (0, clean_1.cleanDeletedBranches)({
162
- interactive: isInteractive,
163
- dryRun: isDryRun,
95
+ interactive: options.interactive || false,
96
+ dryRun: options.dryRun || false,
164
97
  stale: isStale,
165
98
  staleDays: staleDays
166
99
  });
167
- }
168
- // 退出程序
169
- process.exit(0);
100
+ });
101
+ // set-main command
102
+ program
103
+ .command('set-main <branch>')
104
+ .description('Set the main branch for the current project')
105
+ .action(async (branch) => {
106
+ await checkGitRepo();
107
+ await Promise.resolve().then(() => __importStar(require('./commands/config'))).then(m => m.configMainBranch(branch));
108
+ });
109
+ // ignore command
110
+ program
111
+ .command('ignore <pattern>')
112
+ .description('Add a branch pattern to the ignore list (e.g., "release/*")')
113
+ .action(async (pattern) => {
114
+ await checkGitRepo();
115
+ await Promise.resolve().then(() => __importStar(require('./commands/config'))).then(m => m.configIgnoreBranch(pattern));
116
+ });
117
+ // Add examples to help
118
+ program.addHelpText('after', `
119
+ Examples:
120
+ $ gitt # Auto-clean deleted branches
121
+ $ gitt -i # Select branches to delete interactively
122
+ $ gitt -d # Preview deletion
123
+ $ gitt --stale # Find branches inactive for 90+ days
124
+ $ gitt --stale 30 # Find branches inactive for 30+ days
125
+ $ gitt ignore "temp/*" # Ignore branches matching "temp/*"
126
+ $ gitt set-main master # Set main branch to 'master'
127
+ `);
128
+ await program.parseAsync(process.argv);
170
129
  }
171
130
  catch (error) {
172
131
  (0, errors_1.handleError)(error);
173
- process.exit(1);
174
132
  }
175
133
  }
176
134
  // 启动程序
177
- main().catch(errors_1.handleError);
135
+ main();
@@ -39,7 +39,50 @@ exports.writeConfigFile = writeConfigFile;
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const simple_git_1 = require("simple-git");
42
+ const minimatch_1 = require("minimatch");
42
43
  const CONFIG_FILE_NAME = '.gitt';
44
+ /**
45
+ * Validate configuration values.
46
+ */
47
+ function validateConfig(config) {
48
+ const validated = {};
49
+ // Validate mainBranch
50
+ if (config.mainBranch !== undefined) {
51
+ if (typeof config.mainBranch !== 'string' || config.mainBranch.trim() === '') {
52
+ throw new Error('Invalid config: mainBranch must be a non-empty string');
53
+ }
54
+ validated.mainBranch = config.mainBranch.trim();
55
+ }
56
+ // Validate ignoreBranches
57
+ if (config.ignoreBranches !== undefined) {
58
+ if (!Array.isArray(config.ignoreBranches)) {
59
+ throw new Error('Invalid config: ignoreBranches must be an array');
60
+ }
61
+ const validPatterns = [];
62
+ for (const pattern of config.ignoreBranches) {
63
+ if (typeof pattern !== 'string' || pattern.trim() === '') {
64
+ throw new Error('Invalid config: ignoreBranches must contain non-empty strings');
65
+ }
66
+ // Test if it's a valid glob pattern by trying to use it
67
+ try {
68
+ (0, minimatch_1.minimatch)('test', pattern);
69
+ validPatterns.push(pattern);
70
+ }
71
+ catch (e) {
72
+ throw new Error(`Invalid config: "${pattern}" is not a valid glob pattern`);
73
+ }
74
+ }
75
+ validated.ignoreBranches = validPatterns;
76
+ }
77
+ // Validate staleDays
78
+ if (config.staleDays !== undefined) {
79
+ if (typeof config.staleDays !== 'number' || config.staleDays < 1 || config.staleDays > 365 || !Number.isInteger(config.staleDays)) {
80
+ throw new Error('Invalid config: staleDays must be an integer between 1 and 365');
81
+ }
82
+ validated.staleDays = config.staleDays;
83
+ }
84
+ return validated;
85
+ }
43
86
  /**
44
87
  * Get the project root directory (where .git is located).
45
88
  */
@@ -64,9 +107,15 @@ async function readConfigFile() {
64
107
  return {};
65
108
  }
66
109
  const content = fs.readFileSync(configPath, 'utf-8');
67
- return JSON.parse(content);
110
+ const parsedConfig = JSON.parse(content);
111
+ return validateConfig(parsedConfig);
68
112
  }
69
113
  catch (error) {
114
+ // If validation fails, throw the error instead of returning empty config
115
+ if (error instanceof Error && error.message.startsWith('Invalid config:')) {
116
+ throw error;
117
+ }
118
+ // For other errors (file read, JSON parse), return empty config
70
119
  return {};
71
120
  }
72
121
  }
@@ -74,10 +123,12 @@ async function readConfigFile() {
74
123
  * Write to the .gitt configuration file.
75
124
  */
76
125
  async function writeConfigFile(config) {
126
+ // Validate new config before writing
127
+ const validatedConfig = validateConfig(config);
77
128
  const root = await getProjectRoot();
78
129
  const configPath = path.join(root, CONFIG_FILE_NAME);
79
130
  // Read existing config to merge
80
131
  const currentConfig = await readConfigFile();
81
- const newConfig = { ...currentConfig, ...config };
132
+ const newConfig = { ...currentConfig, ...validatedConfig };
82
133
  fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
83
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harryisfish/gitt",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "A command-line tool to help you manage Git repositories and remote repositories, such as keeping in sync, pushing, pulling, etc.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -10,6 +10,9 @@
10
10
  "build": "tsc",
11
11
  "start": "tsx src/index.ts",
12
12
  "dev": "tsx watch src/index.ts",
13
+ "test": "vitest",
14
+ "test:ui": "vitest --ui",
15
+ "test:coverage": "vitest --coverage",
13
16
  "prepare": "pnpm run build",
14
17
  "dev:link": "pnpm build && pnpm link --global",
15
18
  "dev:unlink": "pnpm unlink --global"
@@ -32,14 +35,18 @@
32
35
  "homepage": "https://github.com/harryisfish/gitt#readme",
33
36
  "devDependencies": {
34
37
  "@types/node": "^20.11.24",
38
+ "@vitest/ui": "^4.0.14",
35
39
  "ts-node": "^10.9.2",
36
40
  "tsx": "^4.7.1",
37
- "typescript": "^5.3.3"
41
+ "typescript": "^5.3.3",
42
+ "vitest": "^4.0.14"
38
43
  },
39
44
  "dependencies": {
40
45
  "@inquirer/prompts": "^3.3.0",
41
46
  "@types/minimatch": "^6.0.0",
42
47
  "@types/update-notifier": "^6.0.8",
48
+ "chalk": "4",
49
+ "commander": "^14.0.2",
43
50
  "listr2": "^8.0.0",
44
51
  "minimatch": "^10.1.1",
45
52
  "simple-git": "^3.22.0",