Skip to content

Commit 6cc9e1a

Browse files
Merge pull request #18 from johnlindquist/feat/integrate-git-checks
feat: Add main worktree clean checks and enhance purge error handling
2 parents fbf5008 + 188053a commit 6cc9e1a

14 files changed

Lines changed: 976 additions & 44 deletions

File tree

build/commands/config.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import chalk from 'chalk';
2+
import { getDefaultEditor, setDefaultEditor, getConfigPath } from '../config.js';
3+
export async function configHandler(action, key, value) {
4+
try {
5+
switch (action) {
6+
case 'get':
7+
if (key === 'editor') {
8+
const editor = getDefaultEditor();
9+
console.log(chalk.blue(`Default editor is currently set to: ${chalk.bold(editor)}`));
10+
}
11+
else {
12+
console.error(chalk.red(`Unknown configuration key to get: ${key}`));
13+
process.exit(1);
14+
}
15+
break;
16+
case 'set':
17+
if (key === 'editor' && value) {
18+
setDefaultEditor(value);
19+
console.log(chalk.green(`Default editor set to: ${chalk.bold(value)}`));
20+
}
21+
else if (key === 'editor') {
22+
console.error(chalk.red(`You must provide an editor name.`));
23+
process.exit(1);
24+
}
25+
else {
26+
console.error(chalk.red(`Unknown configuration key to set: ${key}`));
27+
process.exit(1);
28+
}
29+
break;
30+
case 'path':
31+
const configPath = getConfigPath();
32+
console.log(chalk.blue(`Configuration file path: ${configPath}`));
33+
break;
34+
default:
35+
console.error(chalk.red(`Unknown config action: ${action}`));
36+
process.exit(1);
37+
}
38+
}
39+
catch (error) {
40+
if (error instanceof Error) {
41+
console.error(chalk.red('Configuration command failed:'), error.message);
42+
}
43+
else {
44+
console.error(chalk.red('Configuration command failed:'), error);
45+
}
46+
process.exit(1);
47+
}
48+
}

build/commands/list.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { execa } from "execa";
2+
import chalk from "chalk";
3+
export async function listWorktreesHandler() {
4+
try {
5+
// Confirm we're in a git repo
6+
await execa("git", ["rev-parse", "--is-inside-work-tree"]);
7+
// List worktrees
8+
const { stdout } = await execa("git", ["worktree", "list"]);
9+
console.log(chalk.blue("Existing worktrees:\n"));
10+
console.log(stdout);
11+
}
12+
catch (error) {
13+
if (error instanceof Error) {
14+
console.error(chalk.red("Error listing worktrees:"), error.message);
15+
}
16+
else {
17+
console.error(chalk.red("Error listing worktrees:"), error);
18+
}
19+
process.exit(1);
20+
}
21+
}

build/commands/merge.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { execa } from "execa";
2+
import chalk from "chalk";
3+
import { stat, rm } from "node:fs/promises";
4+
export async function mergeWorktreeHandler(branchName, options) {
5+
try {
6+
// Validate that we're in a git repository
7+
await execa("git", ["rev-parse", "--is-inside-work-tree"]);
8+
// Get the current branch name (the target for merging)
9+
const { stdout: currentBranch } = await execa("git", ["branch", "--show-current"]);
10+
if (!currentBranch) {
11+
console.error(chalk.red("Failed to determine the current branch."));
12+
process.exit(1);
13+
}
14+
// Parse worktree list to find the worktree for the target branch
15+
const { stdout } = await execa("git", ["worktree", "list", "--porcelain"]);
16+
let targetPath = "";
17+
let tempPath = "";
18+
const lines = stdout.split("\n");
19+
for (const line of lines) {
20+
if (line.startsWith("worktree ")) {
21+
tempPath = line.replace("worktree ", "").trim();
22+
}
23+
else if (line.startsWith("branch ")) {
24+
const fullBranchRef = line.replace("branch ", "").trim();
25+
const shortBranch = fullBranchRef.replace("refs/heads/", "");
26+
if (shortBranch === branchName) {
27+
targetPath = tempPath;
28+
break;
29+
}
30+
}
31+
}
32+
if (!targetPath) {
33+
console.error(chalk.red(`Could not find a worktree for branch "${branchName}".`));
34+
process.exit(1);
35+
}
36+
console.log(chalk.blue(`Merging changes from worktree branch "${branchName}" at ${targetPath} into current branch "${currentBranch}".`));
37+
// Step 1: Commit any pending changes in the target branch worktree
38+
try {
39+
await execa("git", ["-C", targetPath, "add", "."]);
40+
await execa("git", [
41+
"-C",
42+
targetPath,
43+
"commit",
44+
"-m",
45+
`Auto-commit changes before merging ${branchName}`,
46+
]);
47+
console.log(chalk.green("Committed pending changes in target branch worktree."));
48+
}
49+
catch (commitError) {
50+
console.log(chalk.yellow("No pending changes to commit in the target branch or commit failed, proceeding with merge."));
51+
}
52+
// Step 2: Merge the target branch into the current branch
53+
await execa("git", ["merge", branchName]);
54+
console.log(chalk.green(`Merged branch "${branchName}" into "${currentBranch}".`));
55+
// Step 3: Remove the worktree for the merged branch (similar to 'wt remove')
56+
console.log(chalk.blue(`Removing worktree for branch "${branchName}"...`));
57+
const removeArgs = ["worktree", "remove", ...(options.force ? ["--force"] : []), targetPath];
58+
await execa("git", removeArgs);
59+
console.log(chalk.green(`Removed worktree at ${targetPath}.`));
60+
// Optionally remove the physical directory if it still exists
61+
try {
62+
await stat(targetPath);
63+
await rm(targetPath, { recursive: true, force: true });
64+
console.log(chalk.green(`Deleted folder ${targetPath}.`));
65+
}
66+
catch {
67+
// If the directory does not exist, it's fine
68+
}
69+
console.log(chalk.green("Merge command completed successfully!"));
70+
}
71+
catch (error) {
72+
if (error instanceof Error) {
73+
console.error(chalk.red("Failed to merge worktree:"), error.message);
74+
}
75+
else {
76+
console.error(chalk.red("Failed to merge worktree:"), error);
77+
}
78+
process.exit(1);
79+
}
80+
}

build/commands/new.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { execa } from "execa";
2+
import chalk from "chalk";
3+
import { stat } from "node:fs/promises";
4+
import { resolve, join, dirname, basename } from "node:path";
5+
import { getDefaultEditor } from "../config.js";
6+
import { isWorktreeClean } from "../utils/git.js";
7+
export async function newWorktreeHandler(branchName = "main", options) {
8+
try {
9+
// 1. Validate we're in a git repo
10+
await execa("git", ["rev-parse", "--is-inside-work-tree"]);
11+
console.log(chalk.blue("Checking if main worktree is clean..."));
12+
const isClean = await isWorktreeClean(".");
13+
if (!isClean) {
14+
console.warn(chalk.yellow("⚠️ Warning: Your main worktree is not clean."));
15+
console.warn(chalk.yellow("While 'wt new' might succeed, it's generally recommended to have a clean state."));
16+
console.warn(chalk.cyan("Run 'git status' to review changes. Consider committing or stashing."));
17+
// Decide whether to exit or just warn. Warning might be sufficient here.
18+
// process.exit(1);
19+
}
20+
else {
21+
console.log(chalk.green("✅ Main worktree is clean."));
22+
}
23+
// 2. Build final path for the new worktree
24+
let folderName;
25+
if (options.path) {
26+
folderName = options.path;
27+
}
28+
else {
29+
// Derive the short name for the directory from the branch name
30+
// This handles cases like 'feature/login' -> 'login'
31+
const shortBranchName = branchName.split('/').filter(part => part.length > 0).pop() || branchName;
32+
const currentDir = process.cwd();
33+
const parentDir = dirname(currentDir);
34+
const currentDirName = basename(currentDir);
35+
// Create a sibling directory using the short branch name
36+
folderName = join(parentDir, `${currentDirName}-${shortBranchName}`);
37+
}
38+
const resolvedPath = resolve(folderName);
39+
// Check if directory already exists
40+
let directoryExists = false;
41+
try {
42+
await stat(resolvedPath);
43+
directoryExists = true;
44+
}
45+
catch (error) {
46+
// Directory doesn't exist, continue with creation
47+
}
48+
// 3. Check if branch exists
49+
const { stdout: localBranches } = await execa("git", ["branch", "--list", branchName]);
50+
const { stdout: remoteBranches } = await execa("git", ["branch", "-r", "--list", `origin/${branchName}`]);
51+
const branchExists = !!localBranches || !!remoteBranches;
52+
// 4. Create the new worktree or open the editor if it already exists
53+
if (directoryExists) {
54+
console.log(chalk.yellow(`Directory already exists at: ${resolvedPath}`));
55+
// Check if this is a git worktree by checking for .git file/folder
56+
let isGitWorktree = false;
57+
try {
58+
await stat(join(resolvedPath, ".git"));
59+
isGitWorktree = true;
60+
}
61+
catch (error) {
62+
// Not a git worktree
63+
}
64+
if (isGitWorktree) {
65+
console.log(chalk.green(`Using existing worktree at: ${resolvedPath}`));
66+
}
67+
else {
68+
console.log(chalk.yellow(`Warning: Directory exists but is not a git worktree.`));
69+
}
70+
// Skip to opening editor
71+
}
72+
else {
73+
console.log(chalk.blue(`Creating new worktree for branch "${branchName}" at: ${resolvedPath}`));
74+
if (!branchExists) {
75+
console.log(chalk.yellow(`Branch "${branchName}" doesn't exist. Creating new branch with worktree...`));
76+
// Create a new branch and worktree in one command with -b flag
77+
await execa("git", ["worktree", "add", "-b", branchName, resolvedPath]);
78+
}
79+
else {
80+
console.log(chalk.green(`Using existing branch "${branchName}".`));
81+
await execa("git", ["worktree", "add", resolvedPath, branchName]);
82+
}
83+
// 5. (Optional) Install dependencies if --install flag is provided
84+
if (options.install) {
85+
console.log(chalk.blue(`Installing dependencies using ${options.install} in ${resolvedPath}...`));
86+
await execa(options.install, ["install"], { cwd: resolvedPath, stdio: "inherit" });
87+
}
88+
}
89+
// 6. Open in the specified editor (or use configured default)
90+
const configuredEditor = getDefaultEditor();
91+
const editorCommand = options.editor || configuredEditor; // Use option, then config, fallback is handled by config default
92+
console.log(chalk.blue(`Opening ${resolvedPath} in ${editorCommand}...`));
93+
// Use try-catch to handle if the editor command fails
94+
try {
95+
await execa(editorCommand, [resolvedPath], { stdio: "inherit" });
96+
}
97+
catch (editorError) {
98+
console.error(chalk.red(`Failed to open editor "${editorCommand}". Please ensure it's installed and in your PATH.`));
99+
// Decide if you want to exit or just warn. Let's warn for now.
100+
console.warn(chalk.yellow(`Continuing without opening editor.`));
101+
}
102+
console.log(chalk.green(`Worktree ${directoryExists ? "opened" : "created"} at ${resolvedPath}.`));
103+
if (!directoryExists && options.install)
104+
console.log(chalk.green(`Dependencies installed using ${options.install}.`));
105+
console.log(chalk.green(`Attempted to open in ${editorCommand}.`));
106+
}
107+
catch (error) {
108+
if (error instanceof Error) {
109+
console.error(chalk.red("Failed to create new worktree:"), error.message);
110+
}
111+
else {
112+
console.error(chalk.red("Failed to create new worktree:"), error);
113+
}
114+
process.exit(1);
115+
}
116+
}

0 commit comments

Comments
 (0)