worktree-add 1.3.0 → 1.3.1
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
|
@@ -12,7 +12,8 @@ Running `worktree-add <branch>` from inside a repo:
|
|
|
12
12
|
4. If the destination exists, pass `--interactive` to be prompted for confirmation, or `--yes` to move it to the system trash without prompting. Without either flag, the command exits.
|
|
13
13
|
5. Fetches `origin/<branch>` and refreshes your local branch when safe:
|
|
14
14
|
- fast-forwards the local branch if it’s strictly behind `origin/<branch>`
|
|
15
|
-
- warns and keeps the local branch as-is if it’s ahead
|
|
15
|
+
- warns and keeps the local branch as-is if it’s ahead
|
|
16
|
+
- exits with recovery steps if local and remote have diverged
|
|
16
17
|
6. Creates a git worktree:
|
|
17
18
|
- reuses an existing local branch
|
|
18
19
|
- or creates a tracking branch from `origin/<branch>`
|
|
@@ -10,6 +10,7 @@ import { cleanupWorktree } from "./cleanup-worktree.js";
|
|
|
10
10
|
import { openWorktreeApps } from "./open-worktree-apps.js";
|
|
11
11
|
import { registerSigintHandler } from "./register-sigint-handler.js";
|
|
12
12
|
import { resolveWorktreeContext } from "./resolve-worktree-context.js";
|
|
13
|
+
const quoteForShell = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
13
14
|
export async function runWorktreeAdd(branchRaw, options) {
|
|
14
15
|
const dryRun = options.dryRun ?? false;
|
|
15
16
|
const verbose = options.verbose ?? false;
|
|
@@ -66,6 +67,25 @@ export async function runWorktreeAdd(branchRaw, options) {
|
|
|
66
67
|
"Refusing to create a new branch from HEAD in this ambiguous state.\n" +
|
|
67
68
|
`Re-run with --offline to force creating a new local '${context.branch}' from the current HEAD.`);
|
|
68
69
|
}
|
|
70
|
+
if (remoteStatus.status === "diverged") {
|
|
71
|
+
const { ahead, behind } = remoteStatus.divergence;
|
|
72
|
+
const branchForShell = quoteForShell(context.branch);
|
|
73
|
+
const archivedBranchForShell = quoteForShell(`${context.branch}-old`);
|
|
74
|
+
exitWithMessage(`Local branch '${context.branch}' and origin/${context.branch} have diverged (ahead by ${ahead} and behind by ${behind}).\n` +
|
|
75
|
+
"This can mean either a stale local branch-name collision or legitimate local commits plus new remote commits.\n" +
|
|
76
|
+
"Refusing to reuse the local branch automatically.\n" +
|
|
77
|
+
"Note: commands below use POSIX shell quoting. On Windows cmd.exe/PowerShell, adapt quoting for your shell.\n" +
|
|
78
|
+
"If you want to keep local commits, use your local branch directly:\n" +
|
|
79
|
+
` git worktree add -- <path> ${branchForShell}\n` +
|
|
80
|
+
` # or merge/rebase '${context.branch}' with origin/${context.branch}, then retry.\n` +
|
|
81
|
+
"To work on the remote branch, run:\n" +
|
|
82
|
+
" git fetch origin --prune\n" +
|
|
83
|
+
" # if '<branch>-old' already exists, pick a different archive name.\n" +
|
|
84
|
+
` git branch -m -- ${branchForShell} ${archivedBranchForShell}\n` +
|
|
85
|
+
" # or delete the stale local branch instead:\n" +
|
|
86
|
+
` # git branch -D -- ${branchForShell}\n` +
|
|
87
|
+
` worktree-add -- ${branchForShell}`);
|
|
88
|
+
}
|
|
69
89
|
// Only treat origin as existing when confirmed; "unknown" remains false.
|
|
70
90
|
const remoteBranchExists = remoteStatus.status === "exists";
|
|
71
91
|
createWorktree(context.branch, context.destinationDirectory, {
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { StatusLogger } from "../output/create-status-logger.js";
|
|
2
|
-
|
|
2
|
+
type RemoteBranchStatus = "exists" | "missing" | "unknown" | "diverged";
|
|
3
|
+
type BranchDivergence = {
|
|
4
|
+
ahead: number;
|
|
5
|
+
behind: number;
|
|
6
|
+
};
|
|
7
|
+
type NonDivergedRemoteBranchStatus = Exclude<RemoteBranchStatus, "diverged">;
|
|
8
|
+
export type FetchRemoteBranchResult = {
|
|
9
|
+
status: "diverged";
|
|
10
|
+
localExists: true;
|
|
11
|
+
divergence: BranchDivergence;
|
|
12
|
+
} | {
|
|
13
|
+
status: NonDivergedRemoteBranchStatus;
|
|
14
|
+
localExists: boolean;
|
|
15
|
+
divergence: undefined;
|
|
16
|
+
};
|
|
3
17
|
export declare function fetchRemoteBranch(branch: string, options?: {
|
|
4
18
|
dryRun?: boolean;
|
|
5
19
|
logger?: StatusLogger;
|
|
6
|
-
}):
|
|
7
|
-
|
|
8
|
-
localExists: boolean;
|
|
9
|
-
};
|
|
20
|
+
}): FetchRemoteBranchResult;
|
|
21
|
+
export {};
|
|
@@ -14,15 +14,17 @@ export function fetchRemoteBranch(branch, options) {
|
|
|
14
14
|
catch (error) {
|
|
15
15
|
const diagnostic = extractDiagnosticLine(error);
|
|
16
16
|
logger.warn(`Failed to query origin for '${normalized}': ${diagnostic}. Using existing local branch. (If you expected a remote branch, double-check your network connection and branch name.)`);
|
|
17
|
-
return { status: "unknown", localExists };
|
|
17
|
+
return { status: "unknown", localExists, divergence: undefined };
|
|
18
|
+
}
|
|
19
|
+
if (!remoteExists) {
|
|
20
|
+
return { status: "missing", localExists, divergence: undefined };
|
|
18
21
|
}
|
|
19
|
-
if (!remoteExists)
|
|
20
|
-
return { status: "missing", localExists };
|
|
21
22
|
try {
|
|
22
23
|
if (dryRun) {
|
|
23
24
|
logger.step(`Would fetch origin/${normalized}`);
|
|
24
25
|
logger.detail(`Dry run: local '${normalized}' may be fast-forwarded after fetch if it is behind origin/${normalized}.`);
|
|
25
|
-
|
|
26
|
+
logger.detail("Dry run does not check local/remote divergence because it skips fetch; a real run may stop with a divergence error.");
|
|
27
|
+
return { status: "exists", localExists, divergence: undefined };
|
|
26
28
|
}
|
|
27
29
|
logger.step(`Fetching origin/${normalized} …`);
|
|
28
30
|
fetchOriginBranch(normalized);
|
|
@@ -30,12 +32,12 @@ export function fetchRemoteBranch(branch, options) {
|
|
|
30
32
|
catch (error) {
|
|
31
33
|
const diagnostic = extractDiagnosticLine(error);
|
|
32
34
|
logger.warn(`Failed to fetch origin/${normalized}: ${diagnostic}. Using existing local branch (origin status unknown).`);
|
|
33
|
-
return { status: "unknown", localExists };
|
|
35
|
+
return { status: "unknown", localExists, divergence: undefined };
|
|
34
36
|
}
|
|
35
37
|
const localHead = getLocalBranchHead(normalized);
|
|
36
38
|
const remoteHead = getRemoteBranchHead(normalized);
|
|
37
39
|
if (!localHead || !remoteHead || localHead === remoteHead) {
|
|
38
|
-
return { status: "exists", localExists };
|
|
40
|
+
return { status: "exists", localExists, divergence: undefined };
|
|
39
41
|
}
|
|
40
42
|
let counts;
|
|
41
43
|
try {
|
|
@@ -44,7 +46,7 @@ export function fetchRemoteBranch(branch, options) {
|
|
|
44
46
|
catch (error) {
|
|
45
47
|
const diagnostic = extractDiagnosticLine(error);
|
|
46
48
|
logger.warn(`Failed to compare local '${normalized}' with origin/${normalized}: ${diagnostic}. Using existing local branch as-is.`);
|
|
47
|
-
return { status: "exists", localExists };
|
|
49
|
+
return { status: "exists", localExists, divergence: undefined };
|
|
48
50
|
}
|
|
49
51
|
const { ahead, behind } = counts;
|
|
50
52
|
if (ahead === 0 && behind > 0) {
|
|
@@ -53,27 +55,28 @@ export function fetchRemoteBranch(branch, options) {
|
|
|
53
55
|
const activeWorktree = findWorktreeByBranchName(normalized);
|
|
54
56
|
if (activeWorktree) {
|
|
55
57
|
logger.warn(`Local branch '${normalized}' is checked out in ${activeWorktree}; skipping fast-forward.`);
|
|
56
|
-
return { status: "exists", localExists };
|
|
58
|
+
return { status: "exists", localExists, divergence: undefined };
|
|
57
59
|
}
|
|
58
60
|
logger.detail(`Fast-forwarding '${normalized}' from ${localHead} to ${remoteHead}.`);
|
|
59
61
|
logger.step(`Fast-forwarding local '${normalized}' to origin/${normalized} …`);
|
|
60
62
|
git("branch", "-f", "--", normalized, `origin/${normalized}`);
|
|
61
|
-
return { status: "exists", localExists };
|
|
63
|
+
return { status: "exists", localExists, divergence: undefined };
|
|
62
64
|
}
|
|
63
65
|
if (ahead === 0 && behind === 0) {
|
|
64
66
|
// Defensive: rev-list should report differences when heads differ,
|
|
65
67
|
// but shallow/corrupted refs can yield 0/0 counts.
|
|
66
68
|
logger.warn(`Local branch '${normalized}' differs from origin/${normalized}, but Git reports no ahead/behind differences. Using existing local branch; if you encounter issues, try re-cloning the repository.`);
|
|
67
|
-
return { status: "exists", localExists };
|
|
69
|
+
return { status: "exists", localExists, divergence: undefined };
|
|
70
|
+
}
|
|
71
|
+
if (ahead > 0 && behind > 0) {
|
|
72
|
+
return {
|
|
73
|
+
status: "diverged",
|
|
74
|
+
localExists,
|
|
75
|
+
divergence: { ahead, behind },
|
|
76
|
+
};
|
|
68
77
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
descriptors.push(`ahead by ${ahead}`);
|
|
72
|
-
if (behind > 0)
|
|
73
|
-
descriptors.push(`behind by ${behind}`);
|
|
74
|
-
const relationship = behind > 0 ? "has diverged from" : "is ahead of";
|
|
75
|
-
logger.warn(`Local branch '${normalized}' ${relationship} origin/${normalized} (${descriptors.join(" and ")}); using existing local branch as-is.`);
|
|
76
|
-
return { status: "exists", localExists };
|
|
78
|
+
logger.warn(`Local branch '${normalized}' is ahead of origin/${normalized} (ahead by ${ahead}); using existing local branch as-is.`);
|
|
79
|
+
return { status: "exists", localExists, divergence: undefined };
|
|
77
80
|
}
|
|
78
81
|
let remoteExists;
|
|
79
82
|
try {
|
|
@@ -82,14 +85,15 @@ export function fetchRemoteBranch(branch, options) {
|
|
|
82
85
|
catch (error) {
|
|
83
86
|
const diagnostic = extractDiagnosticLine(error);
|
|
84
87
|
logger.warn(`Failed to reach origin to check whether '${normalized}' exists: ${diagnostic}. (If you expected a remote branch, double-check your network connection and branch name.)`);
|
|
85
|
-
return { status: "unknown", localExists };
|
|
88
|
+
return { status: "unknown", localExists, divergence: undefined };
|
|
89
|
+
}
|
|
90
|
+
if (!remoteExists) {
|
|
91
|
+
return { status: "missing", localExists, divergence: undefined };
|
|
86
92
|
}
|
|
87
|
-
if (!remoteExists)
|
|
88
|
-
return { status: "missing", localExists };
|
|
89
93
|
try {
|
|
90
94
|
if (dryRun) {
|
|
91
95
|
logger.step(`Would fetch origin/${normalized}`);
|
|
92
|
-
return { status: "exists", localExists };
|
|
96
|
+
return { status: "exists", localExists, divergence: undefined };
|
|
93
97
|
}
|
|
94
98
|
logger.step(`Fetching origin/${normalized} …`);
|
|
95
99
|
fetchOriginBranch(normalized);
|
|
@@ -98,5 +102,5 @@ export function fetchRemoteBranch(branch, options) {
|
|
|
98
102
|
const diagnostic = extractDiagnosticLine(error);
|
|
99
103
|
throw new Error(`Failed to fetch origin/${normalized}: ${diagnostic}. Cannot proceed without a local branch.`);
|
|
100
104
|
}
|
|
101
|
-
return { status: "exists", localExists };
|
|
105
|
+
return { status: "exists", localExists, divergence: undefined };
|
|
102
106
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "worktree-add",
|
|
3
3
|
"author": "Łukasz Jerciński",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "1.3.
|
|
5
|
+
"version": "1.3.1",
|
|
6
6
|
"description": "Create a Git worktree next to your current repo for a branch, copy useful local files, install deps, and open it in your apps.",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|