vde-worktree 0.0.8 → 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 CHANGED
@@ -263,6 +263,46 @@ vw extract --current --stash
263
263
 
264
264
  - 実装は primary worktree の抽出フローが中心
265
265
 
266
+ ### `absorb`
267
+
268
+ ```bash
269
+ vw absorb feature/foo --allow-agent --allow-unsafe
270
+ vw absorb feature/foo --from feature/foo --keep-stash --allow-agent --allow-unsafe
271
+ ```
272
+
273
+ 機能:
274
+
275
+ - 非 primary worktree の変更(未コミット含む)を primary worktree に移す
276
+ - source worktree を stash し、primary で checkout 後に stash を apply する
277
+ - `--from` は vw 管理 worktree 名のみ指定可能(`.worktree/` プレフィックスは不可)
278
+
279
+ 安全条件:
280
+
281
+ - primary が dirty なら拒否
282
+ - 非TTYでは `--allow-agent` と `--allow-unsafe` の両方が必要
283
+ - `--keep-stash` を付けると apply 後も stash を残す
284
+
285
+ ### `unabsorb`
286
+
287
+ ```bash
288
+ vw unabsorb feature/foo --allow-agent --allow-unsafe
289
+ vw unabsorb feature/foo --to feature/foo --keep-stash --allow-agent --allow-unsafe
290
+ ```
291
+
292
+ 機能:
293
+
294
+ - primary worktree の変更(未コミット含む)を非 primary worktree に戻す
295
+ - primary の変更を stash し、target worktree に stash を apply する
296
+ - `--to` は vw 管理 worktree 名のみ指定可能(`.worktree/` プレフィックスは不可)
297
+
298
+ 安全条件:
299
+
300
+ - primary worktree が対象 branch 上である必要がある
301
+ - primary が clean なら拒否
302
+ - target worktree が dirty なら拒否
303
+ - 非TTYでは `--allow-agent` と `--allow-unsafe` の両方が必要
304
+ - `--keep-stash` を付けると apply 後も stash を残す
305
+
266
306
  ### `use`
267
307
 
268
308
  ```bash
package/README.md CHANGED
@@ -263,6 +263,46 @@ Current limitation:
263
263
 
264
264
  - Implementation currently supports primary worktree extraction flow.
265
265
 
266
+ ### `absorb`
267
+
268
+ ```bash
269
+ vw absorb feature/foo --allow-agent --allow-unsafe
270
+ vw absorb feature/foo --from feature/foo --keep-stash --allow-agent --allow-unsafe
271
+ ```
272
+
273
+ What it does:
274
+
275
+ - Moves changes from non-primary worktree to primary worktree, including uncommitted files
276
+ - Stashes source worktree changes, checks out branch in primary, then applies stash
277
+ - `--from` accepts vw-managed worktree name only (`.worktree/` prefix is rejected)
278
+
279
+ Safety:
280
+
281
+ - Rejects dirty primary worktree
282
+ - In non-TTY mode, requires `--allow-agent` and `--allow-unsafe`
283
+ - `--keep-stash` keeps the stash entry after apply for rollback/debugging
284
+
285
+ ### `unabsorb`
286
+
287
+ ```bash
288
+ vw unabsorb feature/foo --allow-agent --allow-unsafe
289
+ vw unabsorb feature/foo --to feature/foo --keep-stash --allow-agent --allow-unsafe
290
+ ```
291
+
292
+ What it does:
293
+
294
+ - Pushes changes from primary worktree to non-primary worktree, including uncommitted files
295
+ - Stashes primary worktree changes, applies stash in target worktree
296
+ - `--to` accepts vw-managed worktree name only (`.worktree/` prefix is rejected)
297
+
298
+ Safety:
299
+
300
+ - Requires primary worktree to be on target branch
301
+ - Rejects clean primary worktree
302
+ - Rejects dirty target worktree
303
+ - In non-TTY mode, requires `--allow-agent` and `--allow-unsafe`
304
+ - `--keep-stash` keeps the stash entry after apply for rollback/debugging
305
+
266
306
  ### `use`
267
307
 
268
308
  ```bash
@@ -88,6 +88,45 @@ for (const worktree of worktrees) {
88
88
  ' 2>/dev/null
89
89
  end
90
90
 
91
+ function __vw_managed_worktree_names_with_meta
92
+ command git rev-parse --is-inside-work-tree >/dev/null 2>/dev/null; or return 0
93
+ set -l vw_bin (__vw_current_bin)
94
+ test -n "$vw_bin"; or return 0
95
+
96
+ command $vw_bin list --json 2>/dev/null | command node -e '
97
+ const fs = require("fs")
98
+ const path = require("path")
99
+ const toFlag = (value) => {
100
+ if (value === true) return "yes"
101
+ if (value === false) return "no"
102
+ return "unknown"
103
+ }
104
+ let payload
105
+ try {
106
+ payload = JSON.parse(fs.readFileSync(0, "utf8"))
107
+ } catch {
108
+ process.exit(0)
109
+ }
110
+ const repoRoot = typeof payload?.repoRoot === "string" ? payload.repoRoot : ""
111
+ if (repoRoot.length === 0) process.exit(0)
112
+ const worktreeRoot = path.join(repoRoot, ".worktree")
113
+ const worktrees = Array.isArray(payload.worktrees) ? payload.worktrees : []
114
+ for (const worktree of worktrees) {
115
+ if (typeof worktree?.path !== "string" || worktree.path.length === 0) continue
116
+ const rel = path.relative(worktreeRoot, worktree.path)
117
+ if (!rel || rel === "." || rel === ".." || rel.startsWith(`..${path.sep}`)) continue
118
+ const name = rel.split(path.sep).join("/")
119
+ const branch = typeof worktree?.branch === "string" && worktree.branch.length > 0 ? worktree.branch : "(detached)"
120
+ const merged = toFlag(worktree?.merged?.overall)
121
+ const dirty = worktree?.dirty === true ? "yes" : "no"
122
+ const locked = worktree?.locked?.value === true ? "yes" : "no"
123
+ const summary = `branch=${branch} merged=${merged} dirty=${dirty} locked=${locked}`
124
+ const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
125
+ process.stdout.write(`${name}\t${sanitized}\n`)
126
+ }
127
+ ' 2>/dev/null
128
+ end
129
+
91
130
  function __vw_use_candidates_with_meta
92
131
  begin
93
132
  __vw_worktree_branches
@@ -95,16 +134,6 @@ function __vw_use_candidates_with_meta
95
134
  end | sort -u
96
135
  end
97
136
 
98
- function __vw_local_branches
99
- command git rev-parse --is-inside-work-tree >/dev/null 2>/dev/null; or return 0
100
- command git for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null | sort -u
101
- end
102
-
103
- function __vw_switch_branches
104
- __vw_worktree_branches
105
- __vw_local_branches
106
- end
107
-
108
137
  function __vw_remote_branches
109
138
  command git rev-parse --is-inside-work-tree >/dev/null 2>/dev/null; or return 0
110
139
  command git for-each-ref --format='%(refname:short)' refs/remotes 2>/dev/null \
@@ -120,7 +149,7 @@ function __vw_hook_names
120
149
  end
121
150
  end
122
151
 
123
- set -l __vw_commands init list status path new switch mv del gone get extract use exec invoke copy link lock unlock cd completion help
152
+ set -l __vw_commands init list status path new switch mv del gone get extract absorb unabsorb use exec invoke copy link lock unlock cd completion help
124
153
 
125
154
  for __vw_bin in vw vde-worktree
126
155
  complete -c $__vw_bin -f -n "not __fish_seen_subcommand_from $__vw_commands" -a init -d "Initialize directories, hooks, and managed exclude entries"
@@ -134,6 +163,8 @@ for __vw_bin in vw vde-worktree
134
163
  complete -c $__vw_bin -f -n "not __fish_seen_subcommand_from $__vw_commands" -a gone -d "Bulk cleanup by safety-filtered candidate selection"
135
164
  complete -c $__vw_bin -f -n "not __fish_seen_subcommand_from $__vw_commands" -a get -d "Fetch remote branch and attach worktree"
136
165
  complete -c $__vw_bin -f -n "not __fish_seen_subcommand_from $__vw_commands" -a extract -d "Extract current primary branch into .worktree"
166
+ complete -c $__vw_bin -f -n "not __fish_seen_subcommand_from $__vw_commands" -a absorb -d "Bring non-primary worktree changes into primary worktree"
167
+ complete -c $__vw_bin -f -n "not __fish_seen_subcommand_from $__vw_commands" -a unabsorb -d "Push primary worktree changes into non-primary worktree"
137
168
  complete -c $__vw_bin -f -n "not __fish_seen_subcommand_from $__vw_commands" -a use -d "Checkout target branch in primary worktree"
138
169
  complete -c $__vw_bin -f -n "not __fish_seen_subcommand_from $__vw_commands" -a exec -d "Run command in target branch worktree"
139
170
  complete -c $__vw_bin -f -n "not __fish_seen_subcommand_from $__vw_commands" -a invoke -d "Manually run hook script"
@@ -157,10 +188,11 @@ for __vw_bin in vw vde-worktree
157
188
 
158
189
  complete -c $__vw_bin -n "__fish_seen_subcommand_from status" -a "(__vw_worktree_candidates_with_meta)"
159
190
  complete -c $__vw_bin -n "__fish_seen_subcommand_from path" -a "(__vw_worktree_candidates_with_meta)"
160
- complete -c $__vw_bin -n "__fish_seen_subcommand_from switch" -a "(__vw_switch_branches)"
161
- complete -c $__vw_bin -n "__fish_seen_subcommand_from mv" -a "(__vw_local_branches)"
191
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from switch" -a "(__vw_worktree_candidates_with_meta)"
162
192
  complete -c $__vw_bin -n "__fish_seen_subcommand_from del" -a "(__vw_worktree_candidates_with_meta)"
163
193
  complete -c $__vw_bin -n "__fish_seen_subcommand_from get" -a "(__vw_remote_branches)"
194
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from absorb" -a "(__vw_worktree_candidates_with_meta)"
195
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from unabsorb" -a "(__vw_worktree_candidates_with_meta)"
164
196
  complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -a "(__vw_use_candidates_with_meta)"
165
197
  complete -c $__vw_bin -n "__fish_seen_subcommand_from exec" -a "(__vw_worktree_candidates_with_meta)"
166
198
  complete -c $__vw_bin -n "__fish_seen_subcommand_from invoke" -a "(__vw_hook_names)"
@@ -181,6 +213,16 @@ for __vw_bin in vw vde-worktree
181
213
  complete -c $__vw_bin -n "__fish_seen_subcommand_from extract" -l from -r -d "Path used by extract --from"
182
214
  complete -c $__vw_bin -n "__fish_seen_subcommand_from extract" -l stash -d "Allow stash when dirty"
183
215
 
216
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from absorb" -l from -r -a "(__vw_managed_worktree_names_with_meta)" -d "Source managed worktree name"
217
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from absorb" -l keep-stash -d "Keep stash entry after absorb"
218
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from absorb" -l allow-agent -d "Allow non-TTY execution for absorb"
219
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from absorb" -l allow-unsafe -d "Allow unsafe behavior in non-TTY mode"
220
+
221
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from unabsorb" -l to -r -a "(__vw_managed_worktree_names_with_meta)" -d "Target managed worktree name"
222
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from unabsorb" -l keep-stash -d "Keep stash entry after unabsorb"
223
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from unabsorb" -l allow-agent -d "Allow non-TTY execution for unabsorb"
224
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from unabsorb" -l allow-unsafe -d "Allow unsafe behavior in non-TTY mode"
225
+
184
226
  complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -l allow-agent -d "Allow non-TTY execution for use"
185
227
  complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -l allow-shared -d "Allow checkout when branch is attached by another worktree"
186
228
  complete -c $__vw_bin -n "__fish_seen_subcommand_from use" -l allow-unsafe -d "Allow unsafe behavior in non-TTY mode"
@@ -65,9 +65,43 @@ for (const worktree of worktrees) {
65
65
  ' 2>/dev/null
66
66
  }
67
67
 
68
- _vw_local_branches_raw() {
68
+ _vw_managed_worktree_name_rows_raw() {
69
69
  command git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
70
- command git for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null | command sort -u
70
+ local vw_bin="${words[1]:-vw}"
71
+ command -v "$vw_bin" >/dev/null 2>&1 || return 0
72
+
73
+ command "$vw_bin" list --json 2>/dev/null | command node -e '
74
+ const fs = require("fs")
75
+ const path = require("path")
76
+ const toFlag = (value) => {
77
+ if (value === true) return "yes"
78
+ if (value === false) return "no"
79
+ return "unknown"
80
+ }
81
+ let payload
82
+ try {
83
+ payload = JSON.parse(fs.readFileSync(0, "utf8"))
84
+ } catch {
85
+ process.exit(0)
86
+ }
87
+ const repoRoot = typeof payload?.repoRoot === "string" ? payload.repoRoot : ""
88
+ if (repoRoot.length === 0) process.exit(0)
89
+ const worktreeRoot = path.join(repoRoot, ".worktree")
90
+ const worktrees = Array.isArray(payload.worktrees) ? payload.worktrees : []
91
+ for (const worktree of worktrees) {
92
+ if (typeof worktree?.path !== "string" || worktree.path.length === 0) continue
93
+ const rel = path.relative(worktreeRoot, worktree.path)
94
+ if (!rel || rel === "." || rel === ".." || rel.startsWith(`..${path.sep}`)) continue
95
+ const name = rel.split(path.sep).join("/")
96
+ const branch = typeof worktree?.branch === "string" && worktree.branch.length > 0 ? worktree.branch : "(detached)"
97
+ const merged = toFlag(worktree?.merged?.overall)
98
+ const dirty = worktree?.dirty === true ? "yes" : "no"
99
+ const locked = worktree?.locked?.value === true ? "yes" : "no"
100
+ const summary = `branch=${branch} merged=${merged} dirty=${dirty} locked=${locked}`
101
+ const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
102
+ process.stdout.write(`${name}\t${sanitized}\n`)
103
+ }
104
+ ' 2>/dev/null
71
105
  }
72
106
 
73
107
  _vw_remote_branches_raw() {
@@ -147,20 +181,28 @@ _vw_complete_use_branches() {
147
181
  _vw_complete_worktree_branches
148
182
  }
149
183
 
150
- _vw_complete_local_branches() {
151
- local -a values
152
- values=("${(@f)$(_vw_local_branches_raw)}")
153
- _vw_describe_values "local-branch" "${values[@]}"
154
- }
184
+ _vw_complete_managed_worktree_names() {
185
+ local -a rows names descriptions
186
+ local row name summary
187
+ rows=("${(@f)$(_vw_managed_worktree_name_rows_raw)}")
155
188
 
156
- _vw_complete_switch_branches() {
157
- local -a values
158
- values=(
159
- "${(@f)$(_vw_worktree_branches_raw)}"
160
- "${(@f)$(_vw_local_branches_raw)}"
161
- )
162
- values=("${(@u)values}")
163
- _vw_describe_values "branch" "${values[@]}"
189
+ for row in "${rows[@]}"; do
190
+ name="${row%%$'\t'*}"
191
+ summary="${row#*$'\t'}"
192
+ if [[ -z "${name}" ]]; then
193
+ continue
194
+ fi
195
+
196
+ names+=("${name}")
197
+ descriptions+=("${summary}")
198
+ done
199
+
200
+ if (( ${#names} > 0 )); then
201
+ compadd -Ql -d descriptions -a names
202
+ return 0
203
+ fi
204
+
205
+ _message "no managed-worktree-name candidates"
164
206
  }
165
207
 
166
208
  _vw_complete_remote_branches() {
@@ -193,6 +235,8 @@ _vw() {
193
235
  "gone:Bulk cleanup by safety-filtered candidate selection"
194
236
  "get:Fetch remote branch and attach worktree"
195
237
  "extract:Extract current primary branch into .worktree"
238
+ "absorb:Bring non-primary worktree changes into primary worktree"
239
+ "unabsorb:Push primary worktree changes into non-primary worktree"
196
240
  "use:Checkout target branch in primary worktree"
197
241
  "exec:Run command in target branch worktree"
198
242
  "invoke:Manually run hook script"
@@ -240,11 +284,11 @@ _vw() {
240
284
  ;;
241
285
  switch)
242
286
  _arguments \
243
- "1:branch:_vw_complete_switch_branches"
287
+ "1:branch:_vw_complete_worktree_branches_with_meta"
244
288
  ;;
245
289
  mv)
246
290
  _arguments \
247
- "1:new-branch:_vw_complete_local_branches"
291
+ "1:new-branch:"
248
292
  ;;
249
293
  del)
250
294
  _arguments \
@@ -270,6 +314,22 @@ _vw() {
270
314
  "--from[Path used by extract --from]:path:_files" \
271
315
  "--stash[Allow stash when dirty]"
272
316
  ;;
317
+ absorb)
318
+ _arguments \
319
+ "1:branch:_vw_complete_worktree_branches_with_meta" \
320
+ "--from[Source managed worktree name]:worktree-name:_vw_complete_managed_worktree_names" \
321
+ "--keep-stash[Keep stash entry after absorb]" \
322
+ "--allow-agent[Allow non-TTY execution for absorb]" \
323
+ "--allow-unsafe[Allow unsafe behavior in non-TTY mode]"
324
+ ;;
325
+ unabsorb)
326
+ _arguments \
327
+ "1:branch:_vw_complete_worktree_branches_with_meta" \
328
+ "--to[Target managed worktree name]:worktree-name:_vw_complete_managed_worktree_names" \
329
+ "--keep-stash[Keep stash entry after unabsorb]" \
330
+ "--allow-agent[Allow non-TTY execution for unabsorb]" \
331
+ "--allow-unsafe[Allow unsafe behavior in non-TTY mode]"
332
+ ;;
273
333
  use)
274
334
  _arguments \
275
335
  "1:branch:_vw_complete_use_branches" \