Skip to content

Commit 3fed99a

Browse files
committed
feat: add support for add and remove commands
1 parent 95072c2 commit 3fed99a

File tree

4 files changed

+240
-0
lines changed

4 files changed

+240
-0
lines changed

src/commands/add.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Command } from "commander";
2+
import * as path from "path";
3+
import chalk from "chalk";
4+
import { WorktreeManager } from "../git/WorktreeManager";
5+
6+
interface AddCommandOptions {
7+
branch?: boolean;
8+
track?: string;
9+
}
10+
11+
export function createAddCommand(): Command {
12+
const command = new Command("add");
13+
14+
command
15+
.description("Create a new worktree")
16+
.argument("<name>", "Branch name or new branch name (with -b)")
17+
.option("-b, --branch", "Create a new branch", false)
18+
.option(
19+
"-t, --track <remote-branch>",
20+
"Set up tracking for the specified remote branch",
21+
)
22+
.action(async (name: string, options: AddCommandOptions) => {
23+
try {
24+
await runAdd(name, options);
25+
} catch (error) {
26+
console.error(
27+
chalk.red("Error:"),
28+
error instanceof Error ? error.message : error,
29+
);
30+
process.exit(1);
31+
}
32+
});
33+
34+
return command;
35+
}
36+
37+
async function runAdd(name: string, options: AddCommandOptions): Promise<void> {
38+
const manager = new WorktreeManager();
39+
await manager.initialize();
40+
41+
// Determine the worktree path based on the branch name
42+
// Convert branch name like "feature/my-feature" to a path like "../feature/my-feature"
43+
const worktreePath = getWorktreePath(name);
44+
45+
console.log(chalk.blue(`Creating worktree for '${name}'...`));
46+
47+
if (options.branch) {
48+
// Create a new branch and worktree
49+
await manager.addWorktree(worktreePath, name, {
50+
createBranch: true,
51+
track: options.track,
52+
});
53+
console.log(
54+
chalk.green("✓ Created new branch and worktree:"),
55+
chalk.bold(name),
56+
);
57+
} else {
58+
// Create worktree from existing branch
59+
await manager.addWorktree(worktreePath, name, {
60+
createBranch: false,
61+
track: options.track,
62+
});
63+
console.log(chalk.green("✓ Created worktree:"), chalk.bold(name));
64+
}
65+
66+
console.log(chalk.gray(" Path:"), worktreePath);
67+
console.log();
68+
console.log(chalk.yellow("To switch to this worktree:"));
69+
console.log(chalk.gray(` cd ${worktreePath}`));
70+
}
71+
72+
function getWorktreePath(branchName: string): string {
73+
// Get the current working directory
74+
const cwd = process.cwd();
75+
const parentDir = path.dirname(cwd);
76+
77+
// Use the branch name as the directory name
78+
// Replace slashes with the OS path separator for nested branches
79+
const dirName = branchName.replace(/\//g, path.sep);
80+
81+
return path.join(parentDir, dirName);
82+
}

src/commands/remove.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Command } from "commander";
2+
import chalk from "chalk";
3+
import inquirer from "inquirer";
4+
import { WorktreeManager } from "../git/WorktreeManager";
5+
6+
interface RemoveCommandOptions {
7+
force: boolean;
8+
yes: boolean;
9+
}
10+
11+
export function createRemoveCommand(): Command {
12+
const command = new Command("remove");
13+
14+
command
15+
.description("Remove a worktree")
16+
.argument("<name>", "Branch name or path of the worktree to remove")
17+
.option(
18+
"--force",
19+
"Remove the worktree even if it has uncommitted changes",
20+
false,
21+
)
22+
.option("-y, --yes", "Skip confirmation prompt", false)
23+
.action(async (name: string, options: RemoveCommandOptions) => {
24+
try {
25+
await runRemove(name, options);
26+
} catch (error) {
27+
console.error(
28+
chalk.red("Error:"),
29+
error instanceof Error ? error.message : error,
30+
);
31+
process.exit(1);
32+
}
33+
});
34+
35+
return command;
36+
}
37+
38+
async function runRemove(
39+
name: string,
40+
options: RemoveCommandOptions,
41+
): Promise<void> {
42+
const manager = new WorktreeManager();
43+
await manager.initialize();
44+
45+
// Find the worktree by branch name or path
46+
const worktrees = await manager.listWorktrees();
47+
const worktree = worktrees.find(
48+
(wt) =>
49+
wt.branch === name ||
50+
wt.path === name ||
51+
wt.path.endsWith(`/${name}`) ||
52+
wt.path.endsWith(`\\${name}`),
53+
);
54+
55+
if (!worktree) {
56+
throw new Error(
57+
`Worktree '${name}' not found. Use 'grove list' to see available worktrees.`,
58+
);
59+
}
60+
61+
if (worktree.isMain) {
62+
throw new Error(
63+
`Cannot remove the main worktree (${worktree.branch}). This is the primary worktree.`,
64+
);
65+
}
66+
67+
if (worktree.isLocked) {
68+
throw new Error(
69+
`Worktree '${worktree.branch}' is locked. Unlock it first with 'git worktree unlock'.`,
70+
);
71+
}
72+
73+
// Warn about dirty worktrees
74+
if (worktree.isDirty && !options.force) {
75+
console.log(chalk.yellow("Warning: This worktree has uncommitted changes."));
76+
console.log(
77+
chalk.yellow("Use --force to remove it anyway, or commit/stash your changes first."),
78+
);
79+
console.log();
80+
}
81+
82+
// Show worktree info
83+
console.log(chalk.blue("Worktree to remove:"));
84+
console.log(chalk.gray(" Path:"), worktree.path);
85+
console.log(chalk.gray(" Branch:"), worktree.branch);
86+
if (worktree.isDirty) {
87+
console.log(chalk.yellow(" Status: dirty (uncommitted changes)"));
88+
}
89+
console.log();
90+
91+
// Confirm removal
92+
if (!options.yes) {
93+
const answers = await inquirer.prompt([
94+
{
95+
type: "confirm",
96+
name: "proceed",
97+
message: `Are you sure you want to remove the worktree for '${worktree.branch}'?`,
98+
default: false,
99+
},
100+
]);
101+
102+
if (!answers.proceed) {
103+
console.log(chalk.blue("Operation cancelled."));
104+
return;
105+
}
106+
}
107+
108+
// Remove the worktree
109+
await manager.removeWorktree(worktree.path, options.force);
110+
console.log(
111+
chalk.green("✓ Removed worktree:"),
112+
chalk.bold(worktree.branch),
113+
);
114+
}

src/git/WorktreeManager.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,44 @@ export class WorktreeManager {
255255
throw new Error(`Failed to clone repository: ${error}`);
256256
}
257257
}
258+
259+
async addWorktree(
260+
worktreePath: string,
261+
branchName: string,
262+
options: { createBranch?: boolean; track?: string } = {},
263+
): Promise<void> {
264+
try {
265+
const args = ["worktree", "add"];
266+
267+
if (options.createBranch) {
268+
args.push("-b", branchName);
269+
if (options.track) {
270+
args.push("--track", options.track);
271+
}
272+
args.push(worktreePath);
273+
if (options.track) {
274+
args.push(options.track);
275+
}
276+
} else {
277+
args.push(worktreePath, branchName);
278+
}
279+
280+
await this.git.raw(args);
281+
} catch (error) {
282+
throw new Error(`Failed to add worktree: ${error}`);
283+
}
284+
}
285+
286+
async removeWorktree(worktreePath: string, force: boolean = false): Promise<void> {
287+
try {
288+
const args = ["worktree", "remove"];
289+
if (force) {
290+
args.push("--force");
291+
}
292+
args.push(worktreePath);
293+
await this.git.raw(args);
294+
} catch (error) {
295+
throw new Error(`Failed to remove worktree: ${error}`);
296+
}
297+
}
258298
}

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import { Command } from 'commander';
44
import chalk from 'chalk';
5+
import { createAddCommand } from './commands/add';
56
import { createInitCommand } from './commands/init';
67
import { createListCommand } from './commands/list';
78
import { createPruneCommand } from './commands/prune';
9+
import { createRemoveCommand } from './commands/remove';
810

911
const program = new Command();
1012

@@ -14,9 +16,11 @@ program
1416
.version(require('../package.json').version);
1517

1618
// Add all commands
19+
program.addCommand(createAddCommand());
1720
program.addCommand(createInitCommand());
1821
program.addCommand(createListCommand());
1922
program.addCommand(createPruneCommand());
23+
program.addCommand(createRemoveCommand());
2024

2125
// Handle unknown commands
2226
program.on('command:*', () => {

0 commit comments

Comments
 (0)