vde-worktree 0.0.6 → 0.0.8

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`
@@ -6,6 +6,28 @@ function __vw_worktree_branches
6
6
  | sort -u
7
7
  end
8
8
 
9
+ function __vw_default_branch
10
+ command git rev-parse --is-inside-work-tree >/dev/null 2>/dev/null; or return 0
11
+
12
+ set -l configured (command git config --get vde-worktree.baseBranch 2>/dev/null)
13
+ if test -n "$configured"
14
+ echo $configured
15
+ return
16
+ end
17
+
18
+ command git show-ref --verify --quiet refs/heads/main >/dev/null 2>/dev/null
19
+ and begin
20
+ echo main
21
+ return
22
+ end
23
+
24
+ command git show-ref --verify --quiet refs/heads/master >/dev/null 2>/dev/null
25
+ and begin
26
+ echo master
27
+ return
28
+ end
29
+ end
30
+
9
31
  function __vw_current_bin
10
32
  set -l tokens (commandline -opc)
11
33
  if test (count $tokens) -ge 1
@@ -66,6 +88,13 @@ for (const worktree of worktrees) {
66
88
  ' 2>/dev/null
67
89
  end
68
90
 
91
+ function __vw_use_candidates_with_meta
92
+ begin
93
+ __vw_worktree_branches
94
+ __vw_default_branch
95
+ end | sort -u
96
+ end
97
+
69
98
  function __vw_local_branches
70
99
  command git rev-parse --is-inside-work-tree >/dev/null 2>/dev/null; or return 0
71
100
  command git for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null | sort -u
@@ -132,7 +161,7 @@ for __vw_bin in vw vde-worktree
132
161
  complete -c $__vw_bin -n "__fish_seen_subcommand_from mv" -a "(__vw_local_branches)"
133
162
  complete -c $__vw_bin -n "__fish_seen_subcommand_from del" -a "(__vw_worktree_candidates_with_meta)"
134
163
  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)"
164
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -a "(__vw_use_candidates_with_meta)"
136
165
  complete -c $__vw_bin -n "__fish_seen_subcommand_from exec" -a "(__vw_worktree_candidates_with_meta)"
137
166
  complete -c $__vw_bin -n "__fish_seen_subcommand_from invoke" -a "(__vw_hook_names)"
138
167
  complete -c $__vw_bin -n "__fish_seen_subcommand_from lock" -a "(__vw_worktree_candidates_with_meta)"
@@ -153,6 +182,7 @@ for __vw_bin in vw vde-worktree
153
182
  complete -c $__vw_bin -n "__fish_seen_subcommand_from extract" -l stash -d "Allow stash when dirty"
154
183
 
155
184
  complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -l allow-agent -d "Allow non-TTY execution for use"
185
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -l allow-shared -d "Allow checkout when branch is attached by another worktree"
156
186
  complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -l allow-unsafe -d "Allow unsafe behavior in non-TTY mode"
157
187
 
158
188
  complete -c $__vw_bin -n "__fish_seen_subcommand_from link" -l no-fallback -d "Disable copy fallback when symlink fails"
@@ -7,6 +7,24 @@ _vw_worktree_branches_raw() {
7
7
  | command sort -u
8
8
  }
9
9
 
10
+ _vw_default_branch_raw() {
11
+ command git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
12
+ local configured
13
+ configured="$(command git config --get vde-worktree.baseBranch 2>/dev/null)"
14
+ if [[ -n "${configured}" ]]; then
15
+ print -r -- "${configured}"
16
+ return 0
17
+ fi
18
+ if command git show-ref --verify --quiet refs/heads/main 2>/dev/null; then
19
+ print -r -- "main"
20
+ return 0
21
+ fi
22
+ if command git show-ref --verify --quiet refs/heads/master 2>/dev/null; then
23
+ print -r -- "master"
24
+ return 0
25
+ fi
26
+ }
27
+
10
28
  _vw_worktree_candidate_rows_raw() {
11
29
  command git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
12
30
  local vw_bin="${words[1]:-vw}"
@@ -110,6 +128,25 @@ _vw_complete_worktree_branches_with_meta() {
110
128
  _vw_complete_worktree_branches
111
129
  }
112
130
 
131
+ _vw_complete_use_branches() {
132
+ local -a branches
133
+ local default_branch
134
+ branches=("${(@f)$(_vw_worktree_branches_raw)}")
135
+ default_branch="$(_vw_default_branch_raw | command head -n 1)"
136
+ if [[ -n "${default_branch}" ]]; then
137
+ branches+=("${default_branch}")
138
+ fi
139
+
140
+ branches=("${(@u)branches}")
141
+
142
+ if (( ${#branches} > 0 )); then
143
+ _vw_describe_values "branch" "${branches[@]}"
144
+ return 0
145
+ fi
146
+
147
+ _vw_complete_worktree_branches
148
+ }
149
+
113
150
  _vw_complete_local_branches() {
114
151
  local -a values
115
152
  values=("${(@f)$(_vw_local_branches_raw)}")
@@ -118,7 +155,11 @@ _vw_complete_local_branches() {
118
155
 
119
156
  _vw_complete_switch_branches() {
120
157
  local -a values
121
- values=("${(@u)${(@f)$(_vw_worktree_branches_raw)} ${(@f)$(_vw_local_branches_raw)}}")
158
+ values=(
159
+ "${(@f)$(_vw_worktree_branches_raw)}"
160
+ "${(@f)$(_vw_local_branches_raw)}"
161
+ )
162
+ values=("${(@u)values}")
122
163
  _vw_describe_values "branch" "${values[@]}"
123
164
  }
124
165
 
@@ -231,7 +272,8 @@ _vw() {
231
272
  ;;
232
273
  use)
233
274
  _arguments \
234
- "1:branch:_vw_complete_switch_branches" \
275
+ "1:branch:_vw_complete_use_branches" \
276
+ "--allow-shared[Allow checkout when branch is attached by another worktree]" \
235
277
  "--allow-agent[Allow non-TTY execution for use]" \
236
278
  "--allow-unsafe[Allow unsafe behavior in non-TTY mode]"
237
279
  ;;
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",