vde-worktree 0.0.6 → 0.0.7

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 CHANGED
@@ -267,6 +267,7 @@ vw extract --current --stash
267
267
 
268
268
  ```bash
269
269
  vw use feature/foo
270
+ vw use feature/foo --allow-shared
270
271
  vw use feature/foo --allow-agent --allow-unsafe
271
272
  ```
272
273
 
@@ -278,6 +279,7 @@ vw use feature/foo --allow-agent --allow-unsafe
278
279
  安全条件:
279
280
 
280
281
  - primary が dirty なら拒否
282
+ - 対象 branch が他 worktree で使用中なら `--allow-shared` が必要(指定時は警告を表示)
281
283
  - 非TTYでは `--allow-agent` と `--allow-unsafe` の両方が必要
282
284
 
283
285
  ### `exec`
package/README.md CHANGED
@@ -267,6 +267,7 @@ Current limitation:
267
267
 
268
268
  ```bash
269
269
  vw use feature/foo
270
+ vw use feature/foo --allow-shared
270
271
  vw use feature/foo --allow-agent --allow-unsafe
271
272
  ```
272
273
 
@@ -278,6 +279,7 @@ What it does:
278
279
  Safety:
279
280
 
280
281
  - Rejects dirty primary worktree
282
+ - If target branch is attached by another worktree, requires `--allow-shared` and prints a warning
281
283
  - In non-TTY mode, requires `--allow-agent` and `--allow-unsafe`
282
284
 
283
285
  ### `exec`
@@ -132,7 +132,7 @@ for __vw_bin in vw vde-worktree
132
132
  complete -c $__vw_bin -n "__fish_seen_subcommand_from mv" -a "(__vw_local_branches)"
133
133
  complete -c $__vw_bin -n "__fish_seen_subcommand_from del" -a "(__vw_worktree_candidates_with_meta)"
134
134
  complete -c $__vw_bin -n "__fish_seen_subcommand_from get" -a "(__vw_remote_branches)"
135
- complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -a "(__vw_switch_branches)"
135
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -a "(__vw_worktree_candidates_with_meta)"
136
136
  complete -c $__vw_bin -n "__fish_seen_subcommand_from exec" -a "(__vw_worktree_candidates_with_meta)"
137
137
  complete -c $__vw_bin -n "__fish_seen_subcommand_from invoke" -a "(__vw_hook_names)"
138
138
  complete -c $__vw_bin -n "__fish_seen_subcommand_from lock" -a "(__vw_worktree_candidates_with_meta)"
@@ -153,6 +153,7 @@ for __vw_bin in vw vde-worktree
153
153
  complete -c $__vw_bin -n "__fish_seen_subcommand_from extract" -l stash -d "Allow stash when dirty"
154
154
 
155
155
  complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -l allow-agent -d "Allow non-TTY execution for use"
156
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -l allow-shared -d "Allow checkout when branch is attached by another worktree"
156
157
  complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -l allow-unsafe -d "Allow unsafe behavior in non-TTY mode"
157
158
 
158
159
  complete -c $__vw_bin -n "__fish_seen_subcommand_from link" -l no-fallback -d "Disable copy fallback when symlink fails"
@@ -118,7 +118,11 @@ _vw_complete_local_branches() {
118
118
 
119
119
  _vw_complete_switch_branches() {
120
120
  local -a values
121
- values=("${(@u)${(@f)$(_vw_worktree_branches_raw)} ${(@f)$(_vw_local_branches_raw)}}")
121
+ values=(
122
+ "${(@f)$(_vw_worktree_branches_raw)}"
123
+ "${(@f)$(_vw_local_branches_raw)}"
124
+ )
125
+ values=("${(@u)values}")
122
126
  _vw_describe_values "branch" "${values[@]}"
123
127
  }
124
128
 
@@ -231,7 +235,8 @@ _vw() {
231
235
  ;;
232
236
  use)
233
237
  _arguments \
234
- "1:branch:_vw_complete_switch_branches" \
238
+ "1:branch:_vw_complete_worktree_branches_with_meta" \
239
+ "--allow-shared[Allow checkout when branch is attached by another worktree]" \
235
240
  "--allow-agent[Allow non-TTY execution for use]" \
236
241
  "--allow-unsafe[Allow unsafe behavior in non-TTY mode]"
237
242
  ;;
package/dist/index.mjs CHANGED
@@ -10,6 +10,7 @@ import { parseArgs } from "citty";
10
10
  import { execa } from "execa";
11
11
  import stringWidth from "string-width";
12
12
  import { getBorderCharacters, table } from "table";
13
+ import { createHash } from "node:crypto";
13
14
 
14
15
  //#region src/core/constants.ts
15
16
  const SCHEMA_VERSION = 1;
@@ -170,6 +171,8 @@ const doesGitRefExist = async (cwd, ref) => {
170
171
  //#endregion
171
172
  //#region src/core/paths.ts
172
173
  const GIT_DIR_NAME = ".git";
174
+ const WORKTREE_ID_HASH_LENGTH = 12;
175
+ const WORKTREE_ID_SLUG_MAX_LENGTH = 48;
173
176
  const resolveRepoRootFromCommonDir = ({ currentWorktreeRoot, gitCommonDir }) => {
174
177
  if (gitCommonDir.endsWith(`/${GIT_DIR_NAME}`)) return dirname(gitCommonDir);
175
178
  if (gitCommonDir.endsWith(`\\${GIT_DIR_NAME}`)) return dirname(gitCommonDir);
@@ -224,10 +227,14 @@ const getStateDirectoryPath = (repoRoot) => {
224
227
  return join(getWorktreeMetaRootPath(repoRoot), "state");
225
228
  };
226
229
  const branchToWorktreeId = (branch) => {
227
- return encodeURIComponent(branch);
230
+ return `${branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, WORKTREE_ID_SLUG_MAX_LENGTH) || "branch"}--${createHash("sha256").update(branch).digest("hex").slice(0, WORKTREE_ID_HASH_LENGTH)}`;
228
231
  };
229
232
  const branchToWorktreePath = (repoRoot, branch) => {
230
- return join(getWorktreeRootPath(repoRoot), branchToWorktreeId(branch));
233
+ const worktreeRoot = getWorktreeRootPath(repoRoot);
234
+ return ensurePathInsideRepo({
235
+ repoRoot: worktreeRoot,
236
+ path: join(worktreeRoot, ...branch.split("/"))
237
+ });
231
238
  };
232
239
  const ensurePathInsideRepo = ({ repoRoot, path }) => {
233
240
  const rel = relative(repoRoot, path);
@@ -1378,9 +1385,14 @@ const commandHelpEntries = [
1378
1385
  },
1379
1386
  {
1380
1387
  name: "use",
1381
- usage: "vw use <branch> [--allow-agent --allow-unsafe]",
1388
+ usage: "vw use <branch> [--allow-shared] [--allow-agent --allow-unsafe]",
1382
1389
  summary: "Checkout target branch in primary worktree.",
1383
- details: ["Non-TTY execution requires --allow-agent and --allow-unsafe."]
1390
+ details: ["If target branch is attached by another worktree, --allow-shared is required.", "Non-TTY execution requires --allow-agent and --allow-unsafe."],
1391
+ options: [
1392
+ "--allow-shared",
1393
+ "--allow-agent",
1394
+ "--allow-unsafe"
1395
+ ]
1384
1396
  },
1385
1397
  {
1386
1398
  name: "exec",
@@ -1654,6 +1666,7 @@ const ensureTargetPathWritable = async (targetPath) => {
1654
1666
  try {
1655
1667
  await access(targetPath, constants.F_OK);
1656
1668
  } catch {
1669
+ await mkdir(dirname(targetPath), { recursive: true });
1657
1670
  return;
1658
1671
  }
1659
1672
  if ((await readdir(targetPath)).length > 0) throw createCliError("TARGET_PATH_NOT_EMPTY", {
@@ -2140,6 +2153,10 @@ const createCli = (options = {}) => {
2140
2153
  type: "boolean",
2141
2154
  description: "Allow non-TTY execution for use command"
2142
2155
  },
2156
+ allowShared: {
2157
+ type: "boolean",
2158
+ description: "Allow use checkout when target branch is attached by another worktree"
2159
+ },
2143
2160
  reason: {
2144
2161
  type: "string",
2145
2162
  valueHint: "text",
@@ -3175,6 +3192,7 @@ const createCli = (options = {}) => {
3175
3192
  max: 1
3176
3193
  });
3177
3194
  const branch = commandArgs[0];
3195
+ const allowShared = parsedArgs.allowShared === true;
3178
3196
  if (runtime.isInteractive !== true) {
3179
3197
  if (parsedArgs.allowAgent !== true) throw createCliError("UNSAFE_FLAG_REQUIRED", { message: "UNSAFE_FLAG_REQUIRED: use in non-TTY requires --allow-agent" });
3180
3198
  ensureUnsafeForNonTty({
@@ -3191,6 +3209,38 @@ const createCli = (options = {}) => {
3191
3209
  message: "use requires clean primary worktree",
3192
3210
  details: { repoRoot }
3193
3211
  });
3212
+ const branchCheckedOutInOtherWorktree = (await collectWorktreeSnapshot(repoRoot)).worktrees.find((worktree) => {
3213
+ return worktree.branch === branch && worktree.path !== repoRoot;
3214
+ });
3215
+ if (branchCheckedOutInOtherWorktree !== void 0 && allowShared !== true) throw createCliError("BRANCH_IN_USE", {
3216
+ message: [
3217
+ `branch '${branch}' is already checked out in another worktree.`,
3218
+ ` path: ${branchCheckedOutInOtherWorktree.path}`,
3219
+ "",
3220
+ "To continue (unsafe), re-run with:",
3221
+ ` vw use ${branch} --allow-shared`,
3222
+ "",
3223
+ "Risk:",
3224
+ " multiple worktrees will share the same branch."
3225
+ ].join("\n"),
3226
+ details: {
3227
+ branch,
3228
+ path: branchCheckedOutInOtherWorktree.path,
3229
+ hint: "re-run with --allow-shared to continue",
3230
+ risk: "unsafe: multiple worktrees will share the same branch"
3231
+ }
3232
+ });
3233
+ if (branchCheckedOutInOtherWorktree !== void 0 && allowShared === true) stderr([
3234
+ "warning: --allow-shared enabled.",
3235
+ ` branch: ${branch}`,
3236
+ ` path: ${branchCheckedOutInOtherWorktree.path}`,
3237
+ " risk (unsafe): multiple worktrees will share the same branch."
3238
+ ].join("\n"));
3239
+ const checkoutArgs = branchCheckedOutInOtherWorktree ? [
3240
+ "checkout",
3241
+ "--ignore-other-worktrees",
3242
+ branch
3243
+ ] : ["checkout", branch];
3194
3244
  const hookContext = createHookContext({
3195
3245
  runtime,
3196
3246
  repoRoot,
@@ -3205,7 +3255,7 @@ const createCli = (options = {}) => {
3205
3255
  });
3206
3256
  await runGitCommand({
3207
3257
  cwd: repoRoot,
3208
- args: ["checkout", branch]
3258
+ args: checkoutArgs
3209
3259
  });
3210
3260
  await runPostHook({
3211
3261
  name: "use",