@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.
- package/dist/commands/clean.js +13 -12
- package/dist/errors.js +9 -9
- package/dist/index.js +50 -92
- package/dist/utils/config.js +53 -2
- package/package.json +9 -2
package/dist/commands/clean.js
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
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
|
|
45
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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: (
|
|
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,
|
|
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(
|
|
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(
|
|
37
|
+
console.error(chalk_1.default.red('Program error:'), error.message);
|
|
38
38
|
process.exit(1);
|
|
39
39
|
}
|
|
40
|
-
console.error(
|
|
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(
|
|
45
|
+
console.log(chalk_1.default.green('✓'), message);
|
|
46
46
|
}
|
|
47
47
|
// 错误消息处理
|
|
48
48
|
function printError(message) {
|
|
49
|
-
console.error(
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
updateNotifier({ pkg }).notify();
|
|
75
|
+
updateNotifier({ pkg: packageJson }).notify();
|
|
124
76
|
}
|
|
125
77
|
catch (e) {
|
|
126
78
|
// Ignore update check errors
|
|
127
79
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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:
|
|
163
|
-
dryRun:
|
|
95
|
+
interactive: options.interactive || false,
|
|
96
|
+
dryRun: options.dryRun || false,
|
|
164
97
|
stale: isStale,
|
|
165
98
|
staleDays: staleDays
|
|
166
99
|
});
|
|
167
|
-
}
|
|
168
|
-
//
|
|
169
|
-
|
|
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()
|
|
135
|
+
main();
|
package/dist/utils/config.js
CHANGED
|
@@ -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
|
-
|
|
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, ...
|
|
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.
|
|
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",
|