@harryisfish/gitt 1.3.0 → 1.4.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 CHANGED
@@ -19,12 +19,71 @@ pnpm add -g @harryisfish/gitt
19
19
  ## Usage
20
20
 
21
21
  ```bash
22
+ # Default behavior (auto-clean)
22
23
  gitt
24
+
25
+ # Interactive mode (select branches to delete)
26
+ gitt -i
27
+ # or
28
+ gitt --interactive
29
+
30
+ # Dry run (preview what would be deleted)
31
+ gitt -d
32
+ # or
33
+ gitt --dry-run
23
34
  ```
24
35
 
25
36
  ## Configuration
26
37
 
27
- Gitt automatically uses your Git global configuration. No additional setup required.
38
+ ### Main Branch Detection
39
+
40
+ Gitt automatically detects your main branch in the following order:
41
+ 1. **`.gitt` configuration file** (Project level)
42
+ 2. **Git config** `gitt.mainBranch` (User/System level)
43
+ 3. **Remote HEAD** (e.g., `origin/HEAD`)
44
+ 4. **Common names** (`main`, `master`)
45
+
46
+ ### Setting the Main Branch
47
+
48
+ You can explicitly set the main branch for your project using the command:
49
+
50
+ ```bash
51
+ gitt set-main <branch-name>
52
+ ```
53
+
54
+ Example:
55
+ ```bash
56
+ gitt set-main master
57
+ ```
58
+
59
+ gitt set-main master
60
+ ```
61
+
62
+ This will create a `.gitt` file in your project root with your preference.
63
+
64
+ ### Branch Protection
65
+
66
+ You can prevent specific branches from being deleted by adding them to the ignore list.
67
+
68
+ **Using command (Recommended):**
69
+ ```bash
70
+ gitt ignore "release/*"
71
+ gitt ignore "test-branch"
72
+ ```
73
+
74
+ **Manual Configuration:**
75
+ You can also manually edit the `.gitt` configuration file:
76
+ ```json
77
+ {
78
+ "mainBranch": "main",
79
+ "ignoreBranches": [
80
+ "release/*",
81
+ "test-branch",
82
+ "feature/important-*"
83
+ ]
84
+ }
85
+ ```
86
+ Supports glob patterns (e.g., `*`).
28
87
 
29
88
  ## Documentation
30
89
 
@@ -3,26 +3,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cleanDeletedBranches = cleanDeletedBranches;
4
4
  const simple_git_1 = require("simple-git");
5
5
  const listr2_1 = require("listr2");
6
+ const prompts_1 = require("@inquirer/prompts");
7
+ const minimatch_1 = require("minimatch");
6
8
  const errors_1 = require("../errors");
7
9
  const errors_2 = require("../errors");
10
+ const git_1 = require("../utils/git");
11
+ const config_1 = require("../utils/config");
8
12
  const git = (0, simple_git_1.simpleGit)();
9
13
  /**
10
14
  * Clean up local branches that have been deleted on the remote
11
15
  * @throws {GitError} When cleaning operation fails
12
16
  */
13
- async function cleanDeletedBranches() {
17
+ async function cleanDeletedBranches(options = {}) {
14
18
  try {
15
19
  const tasks = new listr2_1.Listr([
16
20
  {
17
21
  title: 'Fetch main from remote',
18
- task: async () => {
19
- await git.fetch(['origin', 'main']);
22
+ task: async (ctx) => {
23
+ const mainBranch = await (0, git_1.getMainBranch)();
24
+ ctx.mainBranch = mainBranch;
25
+ await git.fetch(['origin', mainBranch]);
20
26
  }
21
27
  },
22
28
  {
23
29
  title: 'Switch to main branch',
24
- task: async () => {
25
- await git.checkout('main');
30
+ task: async (ctx) => {
31
+ await git.checkout(ctx.mainBranch);
26
32
  }
27
33
  },
28
34
  {
@@ -36,13 +42,66 @@ async function cleanDeletedBranches() {
36
42
  task: async (ctx) => {
37
43
  await git.fetch(['--prune']);
38
44
  const branchSummary = await git.branch(['-vv']);
39
- const deletedBranches = branchSummary.all.filter(branch => {
45
+ const config = await (0, config_1.readConfigFile)();
46
+ const ignorePatterns = config.ignoreBranches || [];
47
+ let deletedBranches = branchSummary.all.filter(branch => {
40
48
  const branchInfo = branchSummary.branches[branch];
41
49
  return branchInfo.label && branchInfo.label.includes(': gone]');
42
50
  });
51
+ // Filter out ignored branches
52
+ if (ignorePatterns.length > 0) {
53
+ deletedBranches = deletedBranches.filter(branch => {
54
+ const isIgnored = ignorePatterns.some(pattern => (0, minimatch_1.minimatch)(branch, pattern));
55
+ return !isIgnored;
56
+ });
57
+ }
43
58
  ctx.deletedBranches = deletedBranches;
44
59
  }
45
60
  },
61
+ {
62
+ title: 'Select branches to delete',
63
+ enabled: (ctx) => !!options.interactive && Array.isArray(ctx.deletedBranches) && ctx.deletedBranches.length > 0,
64
+ task: async (ctx, task) => {
65
+ const branches = ctx.deletedBranches;
66
+ // Pause the listr task to show prompt
67
+ // We need to stop the spinner to show the prompt clearly
68
+ // But listr2 doesn't make it easy to pause/resume in the middle of a task flow easily without custom renderers
69
+ // So we will do the prompt and then update the context
70
+ // Actually, listr2 supports prompts but it's complex.
71
+ // Let's try to use the prompt directly.
72
+ // We might need to handle the UI interference.
73
+ // For now, let's just run the prompt.
74
+ // In a real TTY, listr2 might fight for control.
75
+ // A common pattern is to run the prompt outside listr or use listr's prompt adapter.
76
+ // But here we are inside a task.
77
+ // Let's try to use the prompt.
78
+ try {
79
+ const selected = await (0, prompts_1.checkbox)({
80
+ message: 'Select branches to delete:',
81
+ choices: branches.map(b => ({ name: b, value: b, checked: true })),
82
+ });
83
+ ctx.deletedBranches = selected;
84
+ }
85
+ catch (e) {
86
+ // If user cancels, we might want to abort or just delete nothing
87
+ ctx.deletedBranches = [];
88
+ }
89
+ }
90
+ },
91
+ {
92
+ title: 'Dry Run: List branches to be deleted',
93
+ enabled: (ctx) => !!options.dryRun && Array.isArray(ctx.deletedBranches) && ctx.deletedBranches.length > 0,
94
+ task: (ctx, task) => {
95
+ const branches = ctx.deletedBranches;
96
+ task.output = `The following branches would be deleted:\n${branches.map(b => ` - ${b}`).join('\n')}`;
97
+ // We don't want to fail, just show info.
98
+ // But listr output is transient.
99
+ // We will print it at the end or use a separate log.
100
+ console.log('\nDry Run: The following branches would be deleted:');
101
+ branches.forEach(b => console.log(` - ${b}`));
102
+ ctx.deletedBranches = []; // Clear so next step does nothing
103
+ }
104
+ },
46
105
  {
47
106
  title: 'Delete branches removed on remote',
48
107
  enabled: (ctx) => Array.isArray(ctx.deletedBranches),
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configMainBranch = configMainBranch;
4
+ exports.configIgnoreBranch = configIgnoreBranch;
5
+ const git_1 = require("../utils/git");
6
+ const errors_1 = require("../errors");
7
+ const config_1 = require("../utils/config");
8
+ async function configMainBranch(branch) {
9
+ try {
10
+ await (0, git_1.setMainBranch)(branch);
11
+ (0, errors_1.printSuccess)(`Successfully set main branch to '${branch}'`);
12
+ }
13
+ catch (error) {
14
+ (0, errors_1.handleError)(error);
15
+ }
16
+ }
17
+ async function configIgnoreBranch(pattern) {
18
+ try {
19
+ const config = await (0, config_1.readConfigFile)();
20
+ const ignoreBranches = config.ignoreBranches || [];
21
+ if (!ignoreBranches.includes(pattern)) {
22
+ ignoreBranches.push(pattern);
23
+ await (0, config_1.writeConfigFile)({ ignoreBranches });
24
+ (0, errors_1.printSuccess)(`Successfully added '${pattern}' to ignore list`);
25
+ }
26
+ else {
27
+ (0, errors_1.printSuccess)(`'${pattern}' is already in the ignore list`);
28
+ }
29
+ }
30
+ catch (error) {
31
+ (0, errors_1.handleError)(error);
32
+ }
33
+ }
package/dist/index.js CHANGED
@@ -1,5 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  Object.defineProperty(exports, "__esModule", { value: true });
4
37
  const simple_git_1 = require("simple-git");
5
38
  const errors_1 = require("./errors");
@@ -41,12 +74,60 @@ async function checkGitRepo() {
41
74
  throw new errors_1.GitError('Cannot access remote repository, please check network connection or repository permissions');
42
75
  }
43
76
  }
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
+ }
44
99
  async function main() {
45
100
  try {
101
+ const args = process.argv.slice(2);
102
+ const command = args[0];
103
+ // Check for help command
104
+ if (args.includes('-h') || args.includes('--help')) {
105
+ printHelp();
106
+ process.exit(0);
107
+ }
46
108
  // 检查 Git 仓库
47
109
  await checkGitRepo();
48
- // 执行清理操作
49
- await (0, clean_1.cleanDeletedBranches)();
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));
116
+ }
117
+ else if (command === 'ignore') {
118
+ const pattern = args[1];
119
+ if (!pattern) {
120
+ throw new Error('Please specify a branch pattern to ignore');
121
+ }
122
+ await Promise.resolve().then(() => __importStar(require('./commands/config'))).then(m => m.configIgnoreBranch(pattern));
123
+ }
124
+ else {
125
+ // Parse options
126
+ const isInteractive = args.includes('-i') || args.includes('--interactive');
127
+ const isDryRun = args.includes('-d') || args.includes('--dry-run');
128
+ // 默认执行清理操作
129
+ await (0, clean_1.cleanDeletedBranches)({ interactive: isInteractive, dryRun: isDryRun });
130
+ }
50
131
  // 退出程序
51
132
  process.exit(0);
52
133
  }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getProjectRoot = getProjectRoot;
37
+ exports.readConfigFile = readConfigFile;
38
+ exports.writeConfigFile = writeConfigFile;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const simple_git_1 = require("simple-git");
42
+ const CONFIG_FILE_NAME = '.gitt';
43
+ /**
44
+ * Get the project root directory (where .git is located).
45
+ */
46
+ async function getProjectRoot() {
47
+ const git = (0, simple_git_1.simpleGit)();
48
+ try {
49
+ const root = await git.revparse(['--show-toplevel']);
50
+ return root.trim();
51
+ }
52
+ catch (e) {
53
+ return process.cwd();
54
+ }
55
+ }
56
+ /**
57
+ * Read the .gitt configuration file.
58
+ */
59
+ async function readConfigFile() {
60
+ try {
61
+ const root = await getProjectRoot();
62
+ const configPath = path.join(root, CONFIG_FILE_NAME);
63
+ if (!fs.existsSync(configPath)) {
64
+ return {};
65
+ }
66
+ const content = fs.readFileSync(configPath, 'utf-8');
67
+ return JSON.parse(content);
68
+ }
69
+ catch (error) {
70
+ return {};
71
+ }
72
+ }
73
+ /**
74
+ * Write to the .gitt configuration file.
75
+ */
76
+ async function writeConfigFile(config) {
77
+ const root = await getProjectRoot();
78
+ const configPath = path.join(root, CONFIG_FILE_NAME);
79
+ // Read existing config to merge
80
+ const currentConfig = await readConfigFile();
81
+ const newConfig = { ...currentConfig, ...config };
82
+ fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
83
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMainBranch = getMainBranch;
4
+ exports.setMainBranch = setMainBranch;
5
+ const simple_git_1 = require("simple-git");
6
+ const errors_1 = require("../errors");
7
+ const config_1 = require("./config");
8
+ const git = (0, simple_git_1.simpleGit)();
9
+ /**
10
+ * Get the main branch name for the current repository.
11
+ * Priority:
12
+ * 1. User config (gitt.mainBranch)
13
+ * 2. Remote HEAD (origin/HEAD)
14
+ * 3. Common names (main, master)
15
+ */
16
+ async function getMainBranch() {
17
+ try {
18
+ // 1. Check .gitt config file
19
+ const fileConfig = await (0, config_1.readConfigFile)();
20
+ if (fileConfig.mainBranch) {
21
+ return fileConfig.mainBranch;
22
+ }
23
+ // 2. Check user config (legacy/fallback)
24
+ const configMain = await git.getConfig('gitt.mainBranch');
25
+ if (configMain.value) {
26
+ return configMain.value;
27
+ }
28
+ // 3. Try to detect from remote HEAD
29
+ try {
30
+ const remotes = await git.listRemote(['--symref', 'origin', 'HEAD']);
31
+ // Output format example: "ref: refs/heads/main\tHEAD"
32
+ const match = remotes.match(/ref: refs\/heads\/([^\s]+)\s+HEAD/);
33
+ if (match && match[1]) {
34
+ return match[1];
35
+ }
36
+ }
37
+ catch (e) {
38
+ // Ignore network/remote errors during detection
39
+ }
40
+ // 4. Check for common local branches
41
+ const localBranches = await git.branchLocal();
42
+ if (localBranches.all.includes('main'))
43
+ return 'main';
44
+ if (localBranches.all.includes('master'))
45
+ return 'master';
46
+ // Default fallback
47
+ return 'main';
48
+ }
49
+ catch (error) {
50
+ throw new errors_1.GitError('Failed to detect main branch');
51
+ }
52
+ }
53
+ /**
54
+ * Set the preferred main branch for the current repository.
55
+ */
56
+ async function setMainBranch(branch) {
57
+ try {
58
+ // Verify branch exists locally or remotely
59
+ const localBranches = await git.branchLocal();
60
+ if (!localBranches.all.includes(branch)) {
61
+ // If not local, check if we can fetch it
62
+ try {
63
+ await git.fetch(['origin', branch]);
64
+ }
65
+ catch (e) {
66
+ throw new errors_1.GitError(`Branch '${branch}' does not exist locally or on remote`);
67
+ }
68
+ }
69
+ await (0, config_1.writeConfigFile)({ mainBranch: branch });
70
+ }
71
+ catch (error) {
72
+ if (error instanceof errors_1.GitError)
73
+ throw error;
74
+ throw new errors_1.GitError('Failed to set main branch configuration');
75
+ }
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harryisfish/gitt",
3
- "version": "1.3.0",
3
+ "version": "1.4.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": {
@@ -38,8 +38,10 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@inquirer/prompts": "^3.3.0",
41
- "simple-git": "^3.22.0",
42
- "listr2": "^8.0.0"
41
+ "@types/minimatch": "^6.0.0",
42
+ "listr2": "^8.0.0",
43
+ "minimatch": "^10.1.1",
44
+ "simple-git": "^3.22.0"
43
45
  },
44
46
  "engines": {
45
47
  "node": ">=14.0.0"
@@ -50,4 +52,4 @@
50
52
  "README.md",
51
53
  "LICENSE"
52
54
  ]
53
- }
55
+ }