@harryisfish/gitt 1.4.1 โ 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/README.md +58 -1
- package/dist/commands/clean.js +80 -21
- package/dist/errors.js +9 -9
- package/dist/index.js +58 -63
- package/dist/utils/config.js +53 -2
- package/dist/utils/git.js +59 -0
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -18,8 +18,10 @@ pnpm add -g @harryisfish/gitt
|
|
|
18
18
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
|
+
### Basic Commands
|
|
22
|
+
|
|
21
23
|
```bash
|
|
22
|
-
# Default behavior (auto-clean)
|
|
24
|
+
# Default behavior (auto-clean deleted branches)
|
|
23
25
|
gitt
|
|
24
26
|
|
|
25
27
|
# Interactive mode (select branches to delete)
|
|
@@ -31,8 +33,63 @@ gitt --interactive
|
|
|
31
33
|
gitt -d
|
|
32
34
|
# or
|
|
33
35
|
gitt --dry-run
|
|
36
|
+
|
|
37
|
+
# Check version
|
|
38
|
+
gitt -v
|
|
39
|
+
# or
|
|
40
|
+
gitt --version
|
|
41
|
+
|
|
42
|
+
# Show help
|
|
43
|
+
gitt -h
|
|
44
|
+
# or
|
|
45
|
+
gitt --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Stale Branch Cleaning (NEW)
|
|
49
|
+
|
|
50
|
+
Clean up branches that haven't been updated for a long time:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Find branches inactive for 90+ days (default)
|
|
54
|
+
gitt --stale
|
|
55
|
+
|
|
56
|
+
# Custom threshold (e.g., 30 days)
|
|
57
|
+
gitt --stale 30
|
|
58
|
+
|
|
59
|
+
# Preview stale branches without deleting
|
|
60
|
+
gitt --stale --dry-run
|
|
61
|
+
|
|
62
|
+
# Interactively select stale branches to delete
|
|
63
|
+
gitt --stale -i
|
|
34
64
|
```
|
|
35
65
|
|
|
66
|
+
**Note:** Stale branch detection:
|
|
67
|
+
- Checks the last commit date on each branch
|
|
68
|
+
- Automatically excludes the main branch
|
|
69
|
+
- Protects branches in use by Git worktrees
|
|
70
|
+
- Can be combined with interactive (`-i`) and dry-run (`-d`) modes
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## Features
|
|
74
|
+
|
|
75
|
+
### ๐ Auto Update Notification
|
|
76
|
+
Gitt automatically checks for updates on each run and notifies you when a new version is available.
|
|
77
|
+
|
|
78
|
+
### ๐ณ Worktree Protection
|
|
79
|
+
Branches currently checked out in Git worktrees are automatically protected from deletion to prevent errors.
|
|
80
|
+
|
|
81
|
+
### ๐งน Smart Branch Cleanup
|
|
82
|
+
- **Remote-deleted branches**: Automatically detect and clean branches removed from remote
|
|
83
|
+
- **Stale branches**: Find and remove branches inactive for X days
|
|
84
|
+
- **Merge status check**: Safely handles both merged and unmerged branches
|
|
85
|
+
- **Interactive mode**: Manual selection with clear indicators
|
|
86
|
+
- **Dry-run mode**: Preview changes before applying
|
|
87
|
+
|
|
88
|
+
### ๐ก๏ธ Branch Protection
|
|
89
|
+
- Honors `.gitt` ignore patterns
|
|
90
|
+
- Respects Git worktree usage
|
|
91
|
+
- Never touches the main branch
|
|
92
|
+
|
|
36
93
|
## Configuration
|
|
37
94
|
|
|
38
95
|
### Main Branch Detection
|
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,81 +32,141 @@ async function cleanDeletedBranches(options = {}) {
|
|
|
33
32
|
{
|
|
34
33
|
title: 'Switch to main branch',
|
|
35
34
|
task: async (ctx) => {
|
|
36
|
-
await git.
|
|
35
|
+
const currentBranch = (await git.branchLocal()).current;
|
|
36
|
+
const willDeleteCurrent = state.deletedBranches.some(b => b.name === currentBranch);
|
|
37
|
+
try {
|
|
38
|
+
await git.checkout(ctx.mainBranch);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
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.`);
|
|
47
|
+
}
|
|
37
48
|
}
|
|
38
49
|
},
|
|
39
50
|
{
|
|
40
51
|
title: 'Sync main with remote',
|
|
41
52
|
task: async () => {
|
|
42
|
-
|
|
53
|
+
try {
|
|
54
|
+
await git.pull();
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
// Ignore pull errors (e.g. if not on branch)
|
|
58
|
+
}
|
|
43
59
|
}
|
|
44
60
|
},
|
|
45
61
|
{
|
|
46
|
-
title: '
|
|
62
|
+
title: 'Analyze branches',
|
|
47
63
|
task: async (ctx) => {
|
|
48
64
|
await git.fetch(['--prune']);
|
|
49
65
|
const branchSummary = await git.branch(['-vv']);
|
|
50
66
|
const config = await (0, config_1.readConfigFile)();
|
|
51
67
|
const ignorePatterns = config.ignoreBranches || [];
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
const worktreeBranches = await (0, git_1.getWorktrees)();
|
|
69
|
+
let candidates = [];
|
|
70
|
+
if (options.stale) {
|
|
71
|
+
// Stale mode: check all local branches
|
|
72
|
+
const allBranches = branchSummary.all;
|
|
73
|
+
for (const branch of allBranches) {
|
|
74
|
+
if (branch === ctx.mainBranch)
|
|
75
|
+
continue;
|
|
76
|
+
const days = await (0, git_1.getBranchLastCommitTime)(branch);
|
|
77
|
+
if (days > (options.staleDays || 90)) {
|
|
78
|
+
candidates.push({
|
|
79
|
+
name: branch,
|
|
80
|
+
reason: `Stale (${days} days)`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Default mode: check "gone" branches
|
|
87
|
+
const goneBranches = branchSummary.all.filter(branch => {
|
|
88
|
+
const branchInfo = branchSummary.branches[branch];
|
|
89
|
+
return branchInfo.label && branchInfo.label.includes(': gone]');
|
|
90
|
+
});
|
|
91
|
+
candidates = goneBranches.map(b => ({ name: b, reason: 'Remote deleted' }));
|
|
92
|
+
}
|
|
56
93
|
// Filter out ignored branches
|
|
57
94
|
if (ignorePatterns.length > 0) {
|
|
58
|
-
|
|
59
|
-
const isIgnored = ignorePatterns.some(pattern => (0, minimatch_1.minimatch)(
|
|
95
|
+
candidates = candidates.filter(c => {
|
|
96
|
+
const isIgnored = ignorePatterns.some(pattern => (0, minimatch_1.minimatch)(c.name, pattern));
|
|
60
97
|
return !isIgnored;
|
|
61
98
|
});
|
|
62
99
|
}
|
|
63
|
-
|
|
100
|
+
// Filter out worktree branches
|
|
101
|
+
candidates = candidates.filter(c => !worktreeBranches.includes(c.name));
|
|
102
|
+
// Check merge status for each branch
|
|
103
|
+
const branchesWithStatus = await Promise.all(candidates.map(async (c) => {
|
|
104
|
+
const isMerged = await (0, git_1.isBranchMerged)(c.name, ctx.mainBranch);
|
|
105
|
+
return { ...c, isMerged };
|
|
106
|
+
}));
|
|
107
|
+
ctx.deletedBranches = branchesWithStatus;
|
|
64
108
|
}
|
|
65
109
|
}
|
|
66
110
|
]);
|
|
67
111
|
await discoveryTasks.run(state);
|
|
68
112
|
// Phase 2: Interaction / Filtering
|
|
69
113
|
if (state.deletedBranches.length === 0) {
|
|
70
|
-
(0,
|
|
114
|
+
(0, errors_1.printSuccess)('No branches need to be cleaned up');
|
|
71
115
|
return;
|
|
72
116
|
}
|
|
73
117
|
if (options.interactive) {
|
|
74
118
|
try {
|
|
119
|
+
const choices = state.deletedBranches.map(b => ({
|
|
120
|
+
name: `${b.name} (${b.reason}${b.isMerged ? '' : ', Unmerged'})`,
|
|
121
|
+
value: b,
|
|
122
|
+
checked: b.isMerged // Only check merged branches by default
|
|
123
|
+
}));
|
|
75
124
|
const selected = await (0, prompts_1.checkbox)({
|
|
76
125
|
message: 'Select branches to delete:',
|
|
77
|
-
choices:
|
|
126
|
+
choices: choices,
|
|
78
127
|
});
|
|
79
128
|
state.deletedBranches = selected;
|
|
80
129
|
}
|
|
81
130
|
catch (e) {
|
|
82
131
|
// User cancelled
|
|
83
|
-
throw new
|
|
132
|
+
throw new errors_1.UserCancelError('Operation cancelled');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Auto mode: Filter out unmerged branches
|
|
137
|
+
const unmerged = state.deletedBranches.filter(b => !b.isMerged);
|
|
138
|
+
if (unmerged.length > 0) {
|
|
139
|
+
console.log('\nSkipping unmerged branches (use -i to force delete):');
|
|
140
|
+
unmerged.forEach(b => console.log(` - ${b.name} (${b.reason})`));
|
|
84
141
|
}
|
|
142
|
+
state.deletedBranches = state.deletedBranches.filter(b => b.isMerged);
|
|
85
143
|
}
|
|
86
144
|
if (state.deletedBranches.length === 0) {
|
|
87
|
-
(0,
|
|
145
|
+
(0, errors_1.printSuccess)('No branches selected for deletion');
|
|
88
146
|
return;
|
|
89
147
|
}
|
|
90
148
|
if (options.dryRun) {
|
|
91
149
|
console.log('\nDry Run: The following branches would be deleted:');
|
|
92
|
-
state.deletedBranches.forEach(b => console.log(` - ${b}`));
|
|
150
|
+
state.deletedBranches.forEach(b => console.log(` - ${b.name} (${b.reason})`));
|
|
93
151
|
return;
|
|
94
152
|
}
|
|
95
153
|
// Phase 3: Execution
|
|
96
154
|
const deleteTasks = new listr2_1.Listr([
|
|
97
155
|
{
|
|
98
|
-
title: 'Delete branches
|
|
99
|
-
task: (
|
|
156
|
+
title: 'Delete branches',
|
|
157
|
+
task: (_ctx) => {
|
|
100
158
|
return new listr2_1.Listr(state.deletedBranches.map(branch => ({
|
|
101
|
-
title: `Delete ${branch}`,
|
|
159
|
+
title: `Delete ${branch.name}`,
|
|
102
160
|
task: async () => {
|
|
103
|
-
|
|
161
|
+
// Always use -D to force delete if we are here (user confirmed or it's merged)
|
|
162
|
+
await git.branch(['-D', branch.name]);
|
|
104
163
|
}
|
|
105
164
|
})), { concurrent: false });
|
|
106
165
|
}
|
|
107
166
|
}
|
|
108
167
|
]);
|
|
109
168
|
await deleteTasks.run(state);
|
|
110
|
-
(0,
|
|
169
|
+
(0, errors_1.printSuccess)('Branch cleanup completed');
|
|
111
170
|
}
|
|
112
171
|
catch (error) {
|
|
113
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,67 +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
|
-
-h, --help Show this help message
|
|
90
|
-
|
|
91
|
-
Examples:
|
|
92
|
-
gitt # Auto-clean deleted branches
|
|
93
|
-
gitt -i # Select branches to delete interactively
|
|
94
|
-
gitt -d # Preview deletion
|
|
95
|
-
gitt ignore "temp/*" # Ignore branches matching "temp/*"
|
|
96
|
-
gitt set-main master # Set main branch to 'master'
|
|
97
|
-
`);
|
|
98
|
-
}
|
|
99
69
|
async function main() {
|
|
100
70
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
process.exit(0);
|
|
71
|
+
// Check for updates before parsing commands
|
|
72
|
+
try {
|
|
73
|
+
// Use eval to prevent TypeScript from transpiling dynamic import to require()
|
|
74
|
+
const { default: updateNotifier } = await eval('import("update-notifier")');
|
|
75
|
+
updateNotifier({ pkg: packageJson }).notify();
|
|
107
76
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (command === 'set-main') {
|
|
111
|
-
const branch = args[1];
|
|
112
|
-
if (!branch) {
|
|
113
|
-
throw new Error('Please specify a branch name');
|
|
114
|
-
}
|
|
115
|
-
await Promise.resolve().then(() => __importStar(require('./commands/config'))).then(m => m.configMainBranch(branch));
|
|
77
|
+
catch (e) {
|
|
78
|
+
// Ignore update check errors
|
|
116
79
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
94
|
+
await (0, clean_1.cleanDeletedBranches)({
|
|
95
|
+
interactive: options.interactive || false,
|
|
96
|
+
dryRun: options.dryRun || false,
|
|
97
|
+
stale: isStale,
|
|
98
|
+
staleDays: staleDays
|
|
99
|
+
});
|
|
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();
|
|
122
115
|
await Promise.resolve().then(() => __importStar(require('./commands/config'))).then(m => m.configIgnoreBranch(pattern));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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);
|
|
133
129
|
}
|
|
134
130
|
catch (error) {
|
|
135
131
|
(0, errors_1.handleError)(error);
|
|
136
|
-
process.exit(1);
|
|
137
132
|
}
|
|
138
133
|
}
|
|
139
134
|
// ๅฏๅจ็จๅบ
|
|
140
|
-
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/dist/utils/git.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getMainBranch = getMainBranch;
|
|
4
4
|
exports.setMainBranch = setMainBranch;
|
|
5
|
+
exports.isBranchMerged = isBranchMerged;
|
|
6
|
+
exports.getWorktrees = getWorktrees;
|
|
7
|
+
exports.getBranchLastCommitTime = getBranchLastCommitTime;
|
|
5
8
|
const simple_git_1 = require("simple-git");
|
|
6
9
|
const errors_1 = require("../errors");
|
|
7
10
|
const config_1 = require("./config");
|
|
@@ -74,3 +77,59 @@ async function setMainBranch(branch) {
|
|
|
74
77
|
throw new errors_1.GitError('Failed to set main branch configuration');
|
|
75
78
|
}
|
|
76
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if a branch is merged into the main branch.
|
|
82
|
+
*/
|
|
83
|
+
async function isBranchMerged(branch, mainBranch) {
|
|
84
|
+
try {
|
|
85
|
+
const mergedBranches = await git.branch(['--merged', mainBranch]);
|
|
86
|
+
return mergedBranches.all.includes(branch);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get a list of branches that are currently checked out in worktrees.
|
|
94
|
+
*/
|
|
95
|
+
async function getWorktrees() {
|
|
96
|
+
try {
|
|
97
|
+
// Output format:
|
|
98
|
+
// /path/to/repo (HEAD detached at 123456)
|
|
99
|
+
// /path/to/worktree [branch-name]
|
|
100
|
+
const worktrees = await git.raw(['worktree', 'list']);
|
|
101
|
+
const lines = worktrees.split('\n').filter(Boolean);
|
|
102
|
+
const branches = [];
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
// Extract branch name from [branch-name]
|
|
105
|
+
const match = line.match(/\[(.*?)\]/);
|
|
106
|
+
if (match && match[1]) {
|
|
107
|
+
branches.push(match[1]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return branches;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
// If worktrees are not supported or error occurs, return empty list
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get the last commit time (in days) for a branch.
|
|
119
|
+
* Returns the number of days since the last commit.
|
|
120
|
+
*/
|
|
121
|
+
async function getBranchLastCommitTime(branch) {
|
|
122
|
+
try {
|
|
123
|
+
// Get unix timestamp of last commit
|
|
124
|
+
const timestamp = await git.raw(['log', '-1', '--format=%at', branch]);
|
|
125
|
+
const lastCommitDate = new Date(parseInt(timestamp.trim()) * 1000);
|
|
126
|
+
const now = new Date();
|
|
127
|
+
const diffTime = Math.abs(now.getTime() - lastCommitDate.getTime());
|
|
128
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
129
|
+
return diffDays;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// If branch doesn't exist or error, return 0 (treat as active/new to be safe)
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
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,16 +35,22 @@
|
|
|
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",
|
|
47
|
+
"@types/update-notifier": "^6.0.8",
|
|
48
|
+
"chalk": "4",
|
|
49
|
+
"commander": "^14.0.2",
|
|
42
50
|
"listr2": "^8.0.0",
|
|
43
51
|
"minimatch": "^10.1.1",
|
|
44
|
-
"simple-git": "^3.22.0"
|
|
52
|
+
"simple-git": "^3.22.0",
|
|
53
|
+
"update-notifier": "^7.3.1"
|
|
45
54
|
},
|
|
46
55
|
"engines": {
|
|
47
56
|
"node": ">=14.0.0"
|