vde-worktree 0.0.9 → 0.0.10
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.ja.md +40 -0
- package/README.md +40 -0
- package/completions/fish/vw.fish +55 -13
- package/completions/zsh/_vw +77 -17
- package/dist/index.mjs +517 -36
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -41,6 +41,8 @@ const COMMAND_NAMES = {
|
|
|
41
41
|
GONE: "gone",
|
|
42
42
|
GET: "get",
|
|
43
43
|
EXTRACT: "extract",
|
|
44
|
+
ABSORB: "absorb",
|
|
45
|
+
UNABSORB: "unabsorb",
|
|
44
46
|
USE: "use",
|
|
45
47
|
EXEC: "exec",
|
|
46
48
|
INVOKE: "invoke",
|
|
@@ -60,6 +62,8 @@ const WRITE_COMMANDS = new Set([
|
|
|
60
62
|
COMMAND_NAMES.GONE,
|
|
61
63
|
COMMAND_NAMES.GET,
|
|
62
64
|
COMMAND_NAMES.EXTRACT,
|
|
65
|
+
COMMAND_NAMES.ABSORB,
|
|
66
|
+
COMMAND_NAMES.UNABSORB,
|
|
63
67
|
COMMAND_NAMES.USE,
|
|
64
68
|
COMMAND_NAMES.LOCK,
|
|
65
69
|
COMMAND_NAMES.UNLOCK
|
|
@@ -1383,6 +1387,30 @@ const commandHelpEntries = [
|
|
|
1383
1387
|
"--from <path>"
|
|
1384
1388
|
]
|
|
1385
1389
|
},
|
|
1390
|
+
{
|
|
1391
|
+
name: "absorb",
|
|
1392
|
+
usage: "vw absorb <branch> [--from <worktree-name>] [--keep-stash] [--allow-agent --allow-unsafe]",
|
|
1393
|
+
summary: "Bring non-primary worktree changes (including uncommitted) into primary worktree.",
|
|
1394
|
+
details: ["Stashes source worktree changes, checks out target branch in primary, then applies stash.", "Non-TTY execution requires --allow-agent and --allow-unsafe."],
|
|
1395
|
+
options: [
|
|
1396
|
+
"--from <worktree-name>",
|
|
1397
|
+
"--keep-stash",
|
|
1398
|
+
"--allow-agent",
|
|
1399
|
+
"--allow-unsafe"
|
|
1400
|
+
]
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
name: "unabsorb",
|
|
1404
|
+
usage: "vw unabsorb <branch> [--to <worktree-name>] [--keep-stash] [--allow-agent --allow-unsafe]",
|
|
1405
|
+
summary: "Push primary worktree changes (including uncommitted) into non-primary worktree.",
|
|
1406
|
+
details: ["Stashes primary worktree changes, applies them in target non-primary worktree, then optionally drops stash.", "Non-TTY execution requires --allow-agent and --allow-unsafe."],
|
|
1407
|
+
options: [
|
|
1408
|
+
"--to <worktree-name>",
|
|
1409
|
+
"--keep-stash",
|
|
1410
|
+
"--allow-agent",
|
|
1411
|
+
"--allow-unsafe"
|
|
1412
|
+
]
|
|
1413
|
+
},
|
|
1386
1414
|
{
|
|
1387
1415
|
name: "use",
|
|
1388
1416
|
usage: "vw use <branch> [--allow-shared] [--allow-agent --allow-unsafe]",
|
|
@@ -1878,6 +1906,212 @@ const ensureBranchIsNotPrimary = ({ branch, baseBranch }) => {
|
|
|
1878
1906
|
}
|
|
1879
1907
|
});
|
|
1880
1908
|
};
|
|
1909
|
+
const toManagedWorktreeName = ({ repoRoot, worktreePath }) => {
|
|
1910
|
+
const relativePath = relative(getWorktreeRootPath(repoRoot), worktreePath);
|
|
1911
|
+
if (relativePath.length === 0 || relativePath === "." || relativePath === ".." || relativePath.startsWith(`..${sep}`)) return null;
|
|
1912
|
+
return relativePath.split(sep).join("/");
|
|
1913
|
+
};
|
|
1914
|
+
const resolveManagedWorktreePathFromName = ({ repoRoot, optionName, worktreeName }) => {
|
|
1915
|
+
const normalized = worktreeName.trim();
|
|
1916
|
+
if (normalized.length === 0) throw createCliError("INVALID_ARGUMENT", {
|
|
1917
|
+
message: `${optionName} requires non-empty worktree name`,
|
|
1918
|
+
details: {
|
|
1919
|
+
optionName,
|
|
1920
|
+
worktreeName
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
if (normalized === ".worktree" || normalized.startsWith(".worktree/") || normalized.startsWith(".worktree\\")) throw createCliError("INVALID_ARGUMENT", {
|
|
1924
|
+
message: `${optionName} expects vw-managed worktree name (without .worktree/ prefix)`,
|
|
1925
|
+
details: {
|
|
1926
|
+
optionName,
|
|
1927
|
+
worktreeName
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
const worktreeRoot = getWorktreeRootPath(repoRoot);
|
|
1931
|
+
let resolvedPath;
|
|
1932
|
+
try {
|
|
1933
|
+
resolvedPath = resolveRepoRelativePath({
|
|
1934
|
+
repoRoot: worktreeRoot,
|
|
1935
|
+
relativePath: normalized
|
|
1936
|
+
});
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
throw createCliError("INVALID_ARGUMENT", {
|
|
1939
|
+
message: `${optionName} expects vw-managed worktree name`,
|
|
1940
|
+
details: {
|
|
1941
|
+
optionName,
|
|
1942
|
+
worktreeName
|
|
1943
|
+
},
|
|
1944
|
+
cause: error
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
if (resolvedPath === worktreeRoot) throw createCliError("INVALID_ARGUMENT", {
|
|
1948
|
+
message: `${optionName} expects vw-managed worktree name`,
|
|
1949
|
+
details: {
|
|
1950
|
+
optionName,
|
|
1951
|
+
worktreeName
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
return resolvedPath;
|
|
1955
|
+
};
|
|
1956
|
+
const resolveManagedNonPrimaryWorktreeByBranch = ({ repoRoot, branch, worktrees, optionName, worktreeName, role }) => {
|
|
1957
|
+
const managedCandidates = worktrees.filter((worktree) => {
|
|
1958
|
+
return worktree.branch === branch && worktree.path !== repoRoot && toManagedWorktreeName({
|
|
1959
|
+
repoRoot,
|
|
1960
|
+
worktreePath: worktree.path
|
|
1961
|
+
}) !== null;
|
|
1962
|
+
});
|
|
1963
|
+
if (typeof worktreeName === "string") {
|
|
1964
|
+
const resolvedPath = resolveManagedWorktreePathFromName({
|
|
1965
|
+
repoRoot,
|
|
1966
|
+
optionName,
|
|
1967
|
+
worktreeName
|
|
1968
|
+
});
|
|
1969
|
+
const selected = managedCandidates.find((worktree) => worktree.path === resolvedPath);
|
|
1970
|
+
if (selected === void 0) throw createCliError("WORKTREE_NOT_FOUND", {
|
|
1971
|
+
message: `${role} worktree not found for branch '${branch}' and name '${worktreeName}'`,
|
|
1972
|
+
details: {
|
|
1973
|
+
branch,
|
|
1974
|
+
worktreeName,
|
|
1975
|
+
optionName,
|
|
1976
|
+
role
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
return selected;
|
|
1980
|
+
}
|
|
1981
|
+
if (managedCandidates.length === 0) throw createCliError("WORKTREE_NOT_FOUND", {
|
|
1982
|
+
message: `No managed ${role} worktree found for branch: ${branch}`,
|
|
1983
|
+
details: {
|
|
1984
|
+
branch,
|
|
1985
|
+
role
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
if (managedCandidates.length > 1) throw createCliError("INVALID_ARGUMENT", {
|
|
1989
|
+
message: `Multiple managed ${role} worktrees found; use ${optionName} <worktree-name>`,
|
|
1990
|
+
details: {
|
|
1991
|
+
branch,
|
|
1992
|
+
role,
|
|
1993
|
+
optionName,
|
|
1994
|
+
candidates: managedCandidates.map((worktree) => {
|
|
1995
|
+
return toManagedWorktreeName({
|
|
1996
|
+
repoRoot,
|
|
1997
|
+
worktreePath: worktree.path
|
|
1998
|
+
}) ?? worktree.path;
|
|
1999
|
+
})
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
return managedCandidates[0];
|
|
2003
|
+
};
|
|
2004
|
+
const createStashEntry = async ({ cwd, message }) => {
|
|
2005
|
+
await runGitCommand({
|
|
2006
|
+
cwd,
|
|
2007
|
+
args: [
|
|
2008
|
+
"stash",
|
|
2009
|
+
"push",
|
|
2010
|
+
"-u",
|
|
2011
|
+
"-m",
|
|
2012
|
+
message
|
|
2013
|
+
]
|
|
2014
|
+
});
|
|
2015
|
+
const stashTop = await runGitCommand({
|
|
2016
|
+
cwd,
|
|
2017
|
+
args: [
|
|
2018
|
+
"rev-parse",
|
|
2019
|
+
"--verify",
|
|
2020
|
+
"-q",
|
|
2021
|
+
"stash@{0}"
|
|
2022
|
+
],
|
|
2023
|
+
reject: false
|
|
2024
|
+
});
|
|
2025
|
+
const stashOid = stashTop.stdout.trim();
|
|
2026
|
+
if (stashTop.exitCode === 0 && stashOid.length > 0) return stashOid;
|
|
2027
|
+
throw createCliError("INTERNAL_ERROR", {
|
|
2028
|
+
message: "Failed to resolve created stash entry",
|
|
2029
|
+
details: {
|
|
2030
|
+
cwd,
|
|
2031
|
+
message
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
};
|
|
2035
|
+
const restoreStashedChanges = async ({ cwd, stashOid }) => {
|
|
2036
|
+
if ((await runGitCommand({
|
|
2037
|
+
cwd,
|
|
2038
|
+
args: [
|
|
2039
|
+
"stash",
|
|
2040
|
+
"apply",
|
|
2041
|
+
stashOid
|
|
2042
|
+
],
|
|
2043
|
+
reject: false
|
|
2044
|
+
})).exitCode !== 0) throw createCliError("STASH_APPLY_FAILED", {
|
|
2045
|
+
message: "Failed to auto-restore stashed changes after pre-hook failure",
|
|
2046
|
+
details: {
|
|
2047
|
+
cwd,
|
|
2048
|
+
stashOid
|
|
2049
|
+
}
|
|
2050
|
+
});
|
|
2051
|
+
await dropStashByOid({
|
|
2052
|
+
cwd,
|
|
2053
|
+
stashOid
|
|
2054
|
+
});
|
|
2055
|
+
};
|
|
2056
|
+
const runPreHookWithAutoRestore = async ({ name, context, restore }) => {
|
|
2057
|
+
try {
|
|
2058
|
+
await runPreHook({
|
|
2059
|
+
name,
|
|
2060
|
+
context
|
|
2061
|
+
});
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
if (restore !== void 0) try {
|
|
2064
|
+
await restore();
|
|
2065
|
+
} catch (restoreError) {
|
|
2066
|
+
const hookError = ensureCliError(error);
|
|
2067
|
+
const restoreCliError = ensureCliError(restoreError);
|
|
2068
|
+
throw createCliError(hookError.code, {
|
|
2069
|
+
message: `${hookError.message} (auto-restore failed)`,
|
|
2070
|
+
details: {
|
|
2071
|
+
...hookError.details,
|
|
2072
|
+
autoRestoreFailed: true,
|
|
2073
|
+
autoRestoreError: {
|
|
2074
|
+
code: restoreCliError.code,
|
|
2075
|
+
message: restoreCliError.message,
|
|
2076
|
+
details: restoreCliError.details
|
|
2077
|
+
}
|
|
2078
|
+
},
|
|
2079
|
+
cause: error
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
throw error;
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
const resolveStashRefByOid = async ({ cwd, stashOid }) => {
|
|
2086
|
+
const lines = (await runGitCommand({
|
|
2087
|
+
cwd,
|
|
2088
|
+
args: [
|
|
2089
|
+
"stash",
|
|
2090
|
+
"list",
|
|
2091
|
+
"--format=%gd%x09%H"
|
|
2092
|
+
]
|
|
2093
|
+
})).stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2094
|
+
for (const line of lines) {
|
|
2095
|
+
const [ref, oid] = line.split(" ");
|
|
2096
|
+
if (typeof ref === "string" && typeof oid === "string" && ref.length > 0 && oid === stashOid) return ref;
|
|
2097
|
+
}
|
|
2098
|
+
return null;
|
|
2099
|
+
};
|
|
2100
|
+
const dropStashByOid = async ({ cwd, stashOid }) => {
|
|
2101
|
+
const stashRef = await resolveStashRefByOid({
|
|
2102
|
+
cwd,
|
|
2103
|
+
stashOid
|
|
2104
|
+
});
|
|
2105
|
+
if (stashRef === null) return;
|
|
2106
|
+
await runGitCommand({
|
|
2107
|
+
cwd,
|
|
2108
|
+
args: [
|
|
2109
|
+
"stash",
|
|
2110
|
+
"drop",
|
|
2111
|
+
stashRef
|
|
2112
|
+
]
|
|
2113
|
+
});
|
|
2114
|
+
};
|
|
1881
2115
|
const formatDisplayPath = (absolutePath) => {
|
|
1882
2116
|
const homeDirectory = homedir();
|
|
1883
2117
|
if (homeDirectory.length === 0) return absolutePath;
|
|
@@ -2201,13 +2435,22 @@ const createCli = (options = {}) => {
|
|
|
2201
2435
|
},
|
|
2202
2436
|
from: {
|
|
2203
2437
|
type: "string",
|
|
2204
|
-
valueHint: "
|
|
2205
|
-
description: "
|
|
2438
|
+
valueHint: "value",
|
|
2439
|
+
description: "For extract: filesystem path. For absorb: managed worktree name without .worktree/ prefix."
|
|
2440
|
+
},
|
|
2441
|
+
to: {
|
|
2442
|
+
type: "string",
|
|
2443
|
+
valueHint: "worktree-name",
|
|
2444
|
+
description: "Worktree name used by unabsorb --to"
|
|
2206
2445
|
},
|
|
2207
2446
|
stash: {
|
|
2208
2447
|
type: "boolean",
|
|
2209
2448
|
description: "Allow stash for extract"
|
|
2210
2449
|
},
|
|
2450
|
+
keepStash: {
|
|
2451
|
+
type: "boolean",
|
|
2452
|
+
description: "Keep stash entry after absorb/unabsorb"
|
|
2453
|
+
},
|
|
2211
2454
|
fallback: {
|
|
2212
2455
|
type: "boolean",
|
|
2213
2456
|
description: "Enable fallback behavior (disable with --no-fallback)",
|
|
@@ -3089,29 +3332,11 @@ const createCli = (options = {}) => {
|
|
|
3089
3332
|
reject: false
|
|
3090
3333
|
})).stdout.trim().length > 0;
|
|
3091
3334
|
if (dirty && parsedArgs.stash !== true) throw createCliError("DIRTY_WORKTREE", { message: "extract requires clean worktree unless --stash is specified" });
|
|
3092
|
-
let
|
|
3093
|
-
if (dirty && parsedArgs.stash === true) {
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
"stash",
|
|
3098
|
-
"push",
|
|
3099
|
-
"-u",
|
|
3100
|
-
"-m",
|
|
3101
|
-
`vde-worktree extract ${branch}`
|
|
3102
|
-
]
|
|
3103
|
-
});
|
|
3104
|
-
const stashTop = await runGitCommand({
|
|
3105
|
-
cwd: repoRoot,
|
|
3106
|
-
args: [
|
|
3107
|
-
"stash",
|
|
3108
|
-
"list",
|
|
3109
|
-
"--max-count=1",
|
|
3110
|
-
"--format=%gd"
|
|
3111
|
-
]
|
|
3112
|
-
});
|
|
3113
|
-
stashRef = stashTop.stdout.trim().length > 0 ? stashTop.stdout.trim() : null;
|
|
3114
|
-
}
|
|
3335
|
+
let stashOid = null;
|
|
3336
|
+
if (dirty && parsedArgs.stash === true) stashOid = await createStashEntry({
|
|
3337
|
+
cwd: repoRoot,
|
|
3338
|
+
message: `vde-worktree extract ${branch}`
|
|
3339
|
+
});
|
|
3115
3340
|
const hookContext = createHookContext({
|
|
3116
3341
|
runtime,
|
|
3117
3342
|
repoRoot,
|
|
@@ -3120,9 +3345,15 @@ const createCli = (options = {}) => {
|
|
|
3120
3345
|
worktreePath: targetPath,
|
|
3121
3346
|
stderr
|
|
3122
3347
|
});
|
|
3123
|
-
await
|
|
3348
|
+
await runPreHookWithAutoRestore({
|
|
3124
3349
|
name: "extract",
|
|
3125
|
-
context: hookContext
|
|
3350
|
+
context: hookContext,
|
|
3351
|
+
restore: stashOid !== null ? async () => {
|
|
3352
|
+
await restoreStashedChanges({
|
|
3353
|
+
cwd: repoRoot,
|
|
3354
|
+
stashOid
|
|
3355
|
+
});
|
|
3356
|
+
} : void 0
|
|
3126
3357
|
});
|
|
3127
3358
|
await runGitCommand({
|
|
3128
3359
|
cwd: repoRoot,
|
|
@@ -3137,30 +3368,26 @@ const createCli = (options = {}) => {
|
|
|
3137
3368
|
branch
|
|
3138
3369
|
]
|
|
3139
3370
|
});
|
|
3140
|
-
if (
|
|
3371
|
+
if (stashOid !== null) {
|
|
3141
3372
|
if ((await runGitCommand({
|
|
3142
3373
|
cwd: targetPath,
|
|
3143
3374
|
args: [
|
|
3144
3375
|
"stash",
|
|
3145
3376
|
"apply",
|
|
3146
|
-
|
|
3377
|
+
stashOid
|
|
3147
3378
|
],
|
|
3148
3379
|
reject: false
|
|
3149
3380
|
})).exitCode !== 0) throw createCliError("STASH_APPLY_FAILED", {
|
|
3150
3381
|
message: "Failed to apply stash to extracted worktree",
|
|
3151
3382
|
details: {
|
|
3152
|
-
|
|
3383
|
+
stashOid,
|
|
3153
3384
|
branch,
|
|
3154
3385
|
path: targetPath
|
|
3155
3386
|
}
|
|
3156
3387
|
});
|
|
3157
|
-
await
|
|
3388
|
+
await dropStashByOid({
|
|
3158
3389
|
cwd: repoRoot,
|
|
3159
|
-
|
|
3160
|
-
"stash",
|
|
3161
|
-
"drop",
|
|
3162
|
-
stashRef
|
|
3163
|
-
]
|
|
3390
|
+
stashOid
|
|
3164
3391
|
});
|
|
3165
3392
|
}
|
|
3166
3393
|
await runPostHook({
|
|
@@ -3184,6 +3411,260 @@ const createCli = (options = {}) => {
|
|
|
3184
3411
|
stdout(result.path);
|
|
3185
3412
|
return EXIT_CODE.OK;
|
|
3186
3413
|
}
|
|
3414
|
+
if (command === "absorb") {
|
|
3415
|
+
ensureArgumentCount({
|
|
3416
|
+
command,
|
|
3417
|
+
args: commandArgs,
|
|
3418
|
+
min: 1,
|
|
3419
|
+
max: 1
|
|
3420
|
+
});
|
|
3421
|
+
const branch = commandArgs[0];
|
|
3422
|
+
const fromWorktreeName = typeof parsedArgs.from === "string" ? parsedArgs.from : void 0;
|
|
3423
|
+
const keepStash = parsedArgs.keepStash === true;
|
|
3424
|
+
if (runtime.isInteractive !== true) {
|
|
3425
|
+
if (parsedArgs.allowAgent !== true) throw createCliError("UNSAFE_FLAG_REQUIRED", { message: "UNSAFE_FLAG_REQUIRED: absorb in non-TTY requires --allow-agent" });
|
|
3426
|
+
ensureUnsafeForNonTty({
|
|
3427
|
+
runtime,
|
|
3428
|
+
reason: "absorb in non-TTY mode requires --allow-unsafe"
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3431
|
+
const result = await runWriteOperation(async () => {
|
|
3432
|
+
if ((await runGitCommand({
|
|
3433
|
+
cwd: repoRoot,
|
|
3434
|
+
args: ["status", "--porcelain"],
|
|
3435
|
+
reject: false
|
|
3436
|
+
})).stdout.trim().length > 0) throw createCliError("DIRTY_WORKTREE", {
|
|
3437
|
+
message: "absorb requires clean primary worktree",
|
|
3438
|
+
details: { repoRoot }
|
|
3439
|
+
});
|
|
3440
|
+
const sourceWorktree = resolveManagedNonPrimaryWorktreeByBranch({
|
|
3441
|
+
repoRoot,
|
|
3442
|
+
branch,
|
|
3443
|
+
worktrees: (await collectWorktreeSnapshot(repoRoot)).worktrees,
|
|
3444
|
+
optionName: "--from",
|
|
3445
|
+
worktreeName: fromWorktreeName,
|
|
3446
|
+
role: "source"
|
|
3447
|
+
});
|
|
3448
|
+
const sourceDirty = (await runGitCommand({
|
|
3449
|
+
cwd: sourceWorktree.path,
|
|
3450
|
+
args: ["status", "--porcelain"],
|
|
3451
|
+
reject: false
|
|
3452
|
+
})).stdout.trim().length > 0;
|
|
3453
|
+
let stashOid = null;
|
|
3454
|
+
if (sourceDirty) stashOid = await createStashEntry({
|
|
3455
|
+
cwd: sourceWorktree.path,
|
|
3456
|
+
message: `vde-worktree absorb ${branch}`
|
|
3457
|
+
});
|
|
3458
|
+
const hookContext = createHookContext({
|
|
3459
|
+
runtime,
|
|
3460
|
+
repoRoot,
|
|
3461
|
+
action: "absorb",
|
|
3462
|
+
branch,
|
|
3463
|
+
worktreePath: repoRoot,
|
|
3464
|
+
stderr,
|
|
3465
|
+
extraEnv: { WT_SOURCE_WORKTREE_PATH: sourceWorktree.path }
|
|
3466
|
+
});
|
|
3467
|
+
await runPreHookWithAutoRestore({
|
|
3468
|
+
name: "absorb",
|
|
3469
|
+
context: hookContext,
|
|
3470
|
+
restore: stashOid !== null ? async () => {
|
|
3471
|
+
await restoreStashedChanges({
|
|
3472
|
+
cwd: sourceWorktree.path,
|
|
3473
|
+
stashOid
|
|
3474
|
+
});
|
|
3475
|
+
} : void 0
|
|
3476
|
+
});
|
|
3477
|
+
await runGitCommand({
|
|
3478
|
+
cwd: repoRoot,
|
|
3479
|
+
args: [
|
|
3480
|
+
"checkout",
|
|
3481
|
+
"--ignore-other-worktrees",
|
|
3482
|
+
branch
|
|
3483
|
+
]
|
|
3484
|
+
});
|
|
3485
|
+
if (stashOid !== null) {
|
|
3486
|
+
if ((await runGitCommand({
|
|
3487
|
+
cwd: repoRoot,
|
|
3488
|
+
args: [
|
|
3489
|
+
"stash",
|
|
3490
|
+
"apply",
|
|
3491
|
+
stashOid
|
|
3492
|
+
],
|
|
3493
|
+
reject: false
|
|
3494
|
+
})).exitCode !== 0) throw createCliError("STASH_APPLY_FAILED", {
|
|
3495
|
+
message: "Failed to apply stash to primary worktree",
|
|
3496
|
+
details: {
|
|
3497
|
+
stashOid,
|
|
3498
|
+
branch,
|
|
3499
|
+
sourcePath: sourceWorktree.path,
|
|
3500
|
+
path: repoRoot
|
|
3501
|
+
}
|
|
3502
|
+
});
|
|
3503
|
+
if (!keepStash) await dropStashByOid({
|
|
3504
|
+
cwd: repoRoot,
|
|
3505
|
+
stashOid
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
await runPostHook({
|
|
3509
|
+
name: "absorb",
|
|
3510
|
+
context: hookContext
|
|
3511
|
+
});
|
|
3512
|
+
const stashOutputRef = keepStash && stashOid !== null ? await resolveStashRefByOid({
|
|
3513
|
+
cwd: repoRoot,
|
|
3514
|
+
stashOid
|
|
3515
|
+
}) ?? stashOid : null;
|
|
3516
|
+
return {
|
|
3517
|
+
branch,
|
|
3518
|
+
path: repoRoot,
|
|
3519
|
+
sourcePath: sourceWorktree.path,
|
|
3520
|
+
stashed: sourceDirty,
|
|
3521
|
+
stashRef: stashOutputRef
|
|
3522
|
+
};
|
|
3523
|
+
});
|
|
3524
|
+
if (runtime.json) {
|
|
3525
|
+
stdout(JSON.stringify(buildJsonSuccess({
|
|
3526
|
+
command,
|
|
3527
|
+
status: "ok",
|
|
3528
|
+
repoRoot,
|
|
3529
|
+
details: result
|
|
3530
|
+
})));
|
|
3531
|
+
return EXIT_CODE.OK;
|
|
3532
|
+
}
|
|
3533
|
+
stdout(result.path);
|
|
3534
|
+
return EXIT_CODE.OK;
|
|
3535
|
+
}
|
|
3536
|
+
if (command === "unabsorb") {
|
|
3537
|
+
ensureArgumentCount({
|
|
3538
|
+
command,
|
|
3539
|
+
args: commandArgs,
|
|
3540
|
+
min: 1,
|
|
3541
|
+
max: 1
|
|
3542
|
+
});
|
|
3543
|
+
const branch = commandArgs[0];
|
|
3544
|
+
const targetWorktreeName = typeof parsedArgs.to === "string" ? parsedArgs.to : void 0;
|
|
3545
|
+
const keepStash = parsedArgs.keepStash === true;
|
|
3546
|
+
if (runtime.isInteractive !== true) {
|
|
3547
|
+
if (parsedArgs.allowAgent !== true) throw createCliError("UNSAFE_FLAG_REQUIRED", { message: "UNSAFE_FLAG_REQUIRED: unabsorb in non-TTY requires --allow-agent" });
|
|
3548
|
+
ensureUnsafeForNonTty({
|
|
3549
|
+
runtime,
|
|
3550
|
+
reason: "unabsorb in non-TTY mode requires --allow-unsafe"
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3553
|
+
const result = await runWriteOperation(async () => {
|
|
3554
|
+
const currentBranch = (await runGitCommand({
|
|
3555
|
+
cwd: repoRoot,
|
|
3556
|
+
args: ["branch", "--show-current"],
|
|
3557
|
+
reject: false
|
|
3558
|
+
})).stdout.trim();
|
|
3559
|
+
if (currentBranch !== branch) throw createCliError("INVALID_ARGUMENT", {
|
|
3560
|
+
message: "unabsorb requires primary worktree to be on the target branch",
|
|
3561
|
+
details: {
|
|
3562
|
+
branch,
|
|
3563
|
+
currentBranch
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
if ((await runGitCommand({
|
|
3567
|
+
cwd: repoRoot,
|
|
3568
|
+
args: ["status", "--porcelain"],
|
|
3569
|
+
reject: false
|
|
3570
|
+
})).stdout.trim().length === 0) throw createCliError("DIRTY_WORKTREE", {
|
|
3571
|
+
message: "unabsorb requires dirty primary worktree",
|
|
3572
|
+
details: { repoRoot }
|
|
3573
|
+
});
|
|
3574
|
+
const targetWorktree = resolveManagedNonPrimaryWorktreeByBranch({
|
|
3575
|
+
repoRoot,
|
|
3576
|
+
branch,
|
|
3577
|
+
worktrees: (await collectWorktreeSnapshot(repoRoot)).worktrees,
|
|
3578
|
+
optionName: "--to",
|
|
3579
|
+
worktreeName: targetWorktreeName,
|
|
3580
|
+
role: "target"
|
|
3581
|
+
});
|
|
3582
|
+
if ((await runGitCommand({
|
|
3583
|
+
cwd: targetWorktree.path,
|
|
3584
|
+
args: ["status", "--porcelain"],
|
|
3585
|
+
reject: false
|
|
3586
|
+
})).stdout.trim().length > 0) throw createCliError("DIRTY_WORKTREE", {
|
|
3587
|
+
message: "unabsorb requires clean target worktree",
|
|
3588
|
+
details: {
|
|
3589
|
+
branch,
|
|
3590
|
+
path: targetWorktree.path
|
|
3591
|
+
}
|
|
3592
|
+
});
|
|
3593
|
+
const stashOid = await createStashEntry({
|
|
3594
|
+
cwd: repoRoot,
|
|
3595
|
+
message: `vde-worktree unabsorb ${branch}`
|
|
3596
|
+
});
|
|
3597
|
+
const hookContext = createHookContext({
|
|
3598
|
+
runtime,
|
|
3599
|
+
repoRoot,
|
|
3600
|
+
action: "unabsorb",
|
|
3601
|
+
branch,
|
|
3602
|
+
worktreePath: targetWorktree.path,
|
|
3603
|
+
stderr,
|
|
3604
|
+
extraEnv: {
|
|
3605
|
+
WT_SOURCE_WORKTREE_PATH: repoRoot,
|
|
3606
|
+
WT_TARGET_WORKTREE_PATH: targetWorktree.path
|
|
3607
|
+
}
|
|
3608
|
+
});
|
|
3609
|
+
await runPreHookWithAutoRestore({
|
|
3610
|
+
name: "unabsorb",
|
|
3611
|
+
context: hookContext,
|
|
3612
|
+
restore: async () => {
|
|
3613
|
+
await restoreStashedChanges({
|
|
3614
|
+
cwd: repoRoot,
|
|
3615
|
+
stashOid
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
});
|
|
3619
|
+
if ((await runGitCommand({
|
|
3620
|
+
cwd: targetWorktree.path,
|
|
3621
|
+
args: [
|
|
3622
|
+
"stash",
|
|
3623
|
+
"apply",
|
|
3624
|
+
stashOid
|
|
3625
|
+
],
|
|
3626
|
+
reject: false
|
|
3627
|
+
})).exitCode !== 0) throw createCliError("STASH_APPLY_FAILED", {
|
|
3628
|
+
message: "Failed to apply stash to target worktree",
|
|
3629
|
+
details: {
|
|
3630
|
+
stashOid,
|
|
3631
|
+
branch,
|
|
3632
|
+
sourcePath: repoRoot,
|
|
3633
|
+
targetPath: targetWorktree.path
|
|
3634
|
+
}
|
|
3635
|
+
});
|
|
3636
|
+
if (!keepStash) await dropStashByOid({
|
|
3637
|
+
cwd: repoRoot,
|
|
3638
|
+
stashOid
|
|
3639
|
+
});
|
|
3640
|
+
await runPostHook({
|
|
3641
|
+
name: "unabsorb",
|
|
3642
|
+
context: hookContext
|
|
3643
|
+
});
|
|
3644
|
+
const stashOutputRef = keepStash ? await resolveStashRefByOid({
|
|
3645
|
+
cwd: repoRoot,
|
|
3646
|
+
stashOid
|
|
3647
|
+
}) ?? stashOid : null;
|
|
3648
|
+
return {
|
|
3649
|
+
branch,
|
|
3650
|
+
path: targetWorktree.path,
|
|
3651
|
+
sourcePath: repoRoot,
|
|
3652
|
+
stashed: true,
|
|
3653
|
+
stashRef: stashOutputRef
|
|
3654
|
+
};
|
|
3655
|
+
});
|
|
3656
|
+
if (runtime.json) {
|
|
3657
|
+
stdout(JSON.stringify(buildJsonSuccess({
|
|
3658
|
+
command,
|
|
3659
|
+
status: "ok",
|
|
3660
|
+
repoRoot,
|
|
3661
|
+
details: result
|
|
3662
|
+
})));
|
|
3663
|
+
return EXIT_CODE.OK;
|
|
3664
|
+
}
|
|
3665
|
+
stdout(result.path);
|
|
3666
|
+
return EXIT_CODE.OK;
|
|
3667
|
+
}
|
|
3187
3668
|
if (command === "use") {
|
|
3188
3669
|
ensureArgumentCount({
|
|
3189
3670
|
command,
|