vde-worktree 0.0.16 → 0.0.18
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 +9 -5
- package/README.md +8 -4
- package/completions/fish/vw.fish +15 -2
- package/completions/zsh/_vw +18 -2
- package/dist/index.mjs +164 -65
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
- Node.js 22+
|
|
23
23
|
- pnpm 10+
|
|
24
24
|
- `fzf`(`cd` に必須)
|
|
25
|
-
- `gh`(PR
|
|
25
|
+
- `gh`(PR 状態判定に任意)
|
|
26
26
|
|
|
27
27
|
## インストール / ビルド
|
|
28
28
|
|
|
@@ -111,7 +111,7 @@ autoload -Uz compinit && compinit
|
|
|
111
111
|
- `--verbose`: 詳細ログ
|
|
112
112
|
- `--no-hooks`: 今回のみ hook 無効化(`--allow-unsafe` 必須)
|
|
113
113
|
- `--allow-unsafe`: unsafe 操作の明示同意
|
|
114
|
-
- `--no-gh`: 今回の実行で `gh` による PR
|
|
114
|
+
- `--no-gh`: 今回の実行で `gh` による PR 状態判定を無効化
|
|
115
115
|
- `--hook-timeout-ms <ms>`: hook timeout 上書き
|
|
116
116
|
- `--lock-timeout-ms <ms>`: repo lock timeout 上書き
|
|
117
117
|
|
|
@@ -135,13 +135,16 @@ vw init
|
|
|
135
135
|
vw list
|
|
136
136
|
vw list --json
|
|
137
137
|
vw list --no-gh
|
|
138
|
+
vw list --full-path
|
|
138
139
|
```
|
|
139
140
|
|
|
140
141
|
機能:
|
|
141
142
|
|
|
142
143
|
- Git の porcelain 情報から worktree 一覧を取得
|
|
143
|
-
- branch/path/dirty/lock/merged/upstream を表示
|
|
144
|
-
-
|
|
144
|
+
- branch/path/dirty/lock/merged/PR/upstream を表示
|
|
145
|
+
- テーブル表示では長い `path` は端末幅に合わせて `…` で省略
|
|
146
|
+
- `--full-path` でテーブル表示の path 省略を無効化
|
|
147
|
+
- `--no-gh` 指定時は PR 状態判定をスキップ(`pr.status` は `unknown`、`merged.byPR` は `null`)
|
|
145
148
|
- 対話ターミナルでは Catppuccin 風の ANSI 色で表示
|
|
146
149
|
|
|
147
150
|
### `status`
|
|
@@ -424,6 +427,7 @@ vw completion zsh --install
|
|
|
424
427
|
- `merged.byAncestry`: ローカル履歴判定(`git merge-base --is-ancestor`)
|
|
425
428
|
- `merged.byPR`: GitHub PR merged 判定(`gh`)
|
|
426
429
|
- `merged.overall`: 最終判定
|
|
430
|
+
- `pr.status`: PR 状態(`none` / `open` / `merged` / `closed_unmerged` / `unknown`)
|
|
427
431
|
|
|
428
432
|
`overall` ポリシー:
|
|
429
433
|
|
|
@@ -436,7 +440,7 @@ vw completion zsh --install
|
|
|
436
440
|
- `byPR === false` または lifecycle が明示的に未取り込みなら `overall = false`
|
|
437
441
|
- それ以外は `overall = null`
|
|
438
442
|
|
|
439
|
-
`byPR` が `null` になる例:
|
|
443
|
+
`byPR` が `null` かつ `pr.status` が `unknown` になる例:
|
|
440
444
|
|
|
441
445
|
- `gh` 未導入
|
|
442
446
|
- `gh auth` 未設定
|
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ After `vw init`, the tool manages:
|
|
|
111
111
|
- `--verbose`: verbose logging
|
|
112
112
|
- `--no-hooks`: disable hooks for this run (requires `--allow-unsafe`)
|
|
113
113
|
- `--allow-unsafe`: explicit unsafe override
|
|
114
|
-
- `--no-gh`: disable GitHub CLI based PR
|
|
114
|
+
- `--no-gh`: disable GitHub CLI based PR status checks for this run
|
|
115
115
|
- `--hook-timeout-ms <ms>`: hook timeout override
|
|
116
116
|
- `--lock-timeout-ms <ms>`: repository lock timeout override
|
|
117
117
|
|
|
@@ -135,13 +135,16 @@ What it does:
|
|
|
135
135
|
vw list
|
|
136
136
|
vw list --json
|
|
137
137
|
vw list --no-gh
|
|
138
|
+
vw list --full-path
|
|
138
139
|
```
|
|
139
140
|
|
|
140
141
|
What it does:
|
|
141
142
|
|
|
142
143
|
- Lists all worktrees from Git porcelain output
|
|
143
|
-
- Includes metadata such as branch, path, dirty, lock, merged, and upstream status
|
|
144
|
-
-
|
|
144
|
+
- Includes metadata such as branch, path, dirty, lock, merged, PR status, and upstream status
|
|
145
|
+
- In table output, long `path` values are truncated with `…` to fit terminal width by default
|
|
146
|
+
- Use `--full-path` to disable path truncation in table output
|
|
147
|
+
- With `--no-gh`, skips PR status checks (`pr.status` becomes `unknown`, `merged.byPR` becomes `null`)
|
|
145
148
|
- In interactive terminal, uses Catppuccin-style ANSI colors
|
|
146
149
|
|
|
147
150
|
### `status`
|
|
@@ -424,6 +427,7 @@ Each worktree reports:
|
|
|
424
427
|
- `merged.byAncestry`: local ancestry check (`git merge-base --is-ancestor <branch> <baseBranch>`)
|
|
425
428
|
- `merged.byPR`: PR-based merged check via GitHub CLI
|
|
426
429
|
- `merged.overall`: final decision
|
|
430
|
+
- `pr.status`: PR state (`none` / `open` / `merged` / `closed_unmerged` / `unknown`)
|
|
427
431
|
|
|
428
432
|
Overall policy:
|
|
429
433
|
|
|
@@ -436,7 +440,7 @@ Overall policy:
|
|
|
436
440
|
- `byPR === false` or explicit lifecycle "not merged" evidence => `overall = false`
|
|
437
441
|
- otherwise `overall = null`
|
|
438
442
|
|
|
439
|
-
`byPR` becomes `null` when PR lookup is unavailable (for example: `gh` missing, auth missing, API error, `vde-worktree.enableGh=false`, or `--no-gh`).
|
|
443
|
+
`byPR` becomes `null` and `pr.status` becomes `unknown` when PR lookup is unavailable (for example: `gh` missing, auth missing, API error, `vde-worktree.enableGh=false`, or `--no-gh`).
|
|
440
444
|
|
|
441
445
|
## JSON Contract
|
|
442
446
|
|
package/completions/fish/vw.fish
CHANGED
|
@@ -68,6 +68,10 @@ const toFlag = (value) => {
|
|
|
68
68
|
if (value === false) return "no"
|
|
69
69
|
return "unknown"
|
|
70
70
|
}
|
|
71
|
+
const toPrStatus = (value) => {
|
|
72
|
+
if (typeof value !== "string" || value.length === 0) return "n/a"
|
|
73
|
+
return value
|
|
74
|
+
}
|
|
71
75
|
let payload
|
|
72
76
|
try {
|
|
73
77
|
payload = JSON.parse(fs.readFileSync(0, "utf8"))
|
|
@@ -78,10 +82,11 @@ const worktrees = Array.isArray(payload.worktrees) ? payload.worktrees : []
|
|
|
78
82
|
for (const worktree of worktrees) {
|
|
79
83
|
if (typeof worktree?.branch !== "string" || worktree.branch.length === 0) continue
|
|
80
84
|
const merged = toFlag(worktree?.merged?.overall)
|
|
85
|
+
const pr = toPrStatus(worktree?.pr?.status)
|
|
81
86
|
const dirty = worktree?.dirty === true ? "yes" : "no"
|
|
82
87
|
const locked = worktree?.locked?.value === true ? "yes" : "no"
|
|
83
88
|
const path = toDisplayPath(worktree?.path)
|
|
84
|
-
const summary = `merged=${merged} dirty=${dirty} locked=${locked}${path ? ` path=${path}` : ""}`
|
|
89
|
+
const summary = `merged=${merged} pr=${pr} dirty=${dirty} locked=${locked}${path ? ` path=${path}` : ""}`
|
|
85
90
|
const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
|
|
86
91
|
process.stdout.write(`${worktree.branch}\t${sanitized}\n`)
|
|
87
92
|
}
|
|
@@ -101,6 +106,10 @@ const toFlag = (value) => {
|
|
|
101
106
|
if (value === false) return "no"
|
|
102
107
|
return "unknown"
|
|
103
108
|
}
|
|
109
|
+
const toPrStatus = (value) => {
|
|
110
|
+
if (typeof value !== "string" || value.length === 0) return "n/a"
|
|
111
|
+
return value
|
|
112
|
+
}
|
|
104
113
|
let payload
|
|
105
114
|
try {
|
|
106
115
|
payload = JSON.parse(fs.readFileSync(0, "utf8"))
|
|
@@ -118,9 +127,10 @@ for (const worktree of worktrees) {
|
|
|
118
127
|
const name = rel.split(path.sep).join("/")
|
|
119
128
|
const branch = typeof worktree?.branch === "string" && worktree.branch.length > 0 ? worktree.branch : "(detached)"
|
|
120
129
|
const merged = toFlag(worktree?.merged?.overall)
|
|
130
|
+
const pr = toPrStatus(worktree?.pr?.status)
|
|
121
131
|
const dirty = worktree?.dirty === true ? "yes" : "no"
|
|
122
132
|
const locked = worktree?.locked?.value === true ? "yes" : "no"
|
|
123
|
-
const summary = `branch=${branch} merged=${merged} dirty=${dirty} locked=${locked}`
|
|
133
|
+
const summary = `branch=${branch} merged=${merged} pr=${pr} dirty=${dirty} locked=${locked}`
|
|
124
134
|
const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
|
|
125
135
|
process.stdout.write(`${name}\t${sanitized}\n`)
|
|
126
136
|
}
|
|
@@ -179,6 +189,8 @@ for __vw_bin in vw vde-worktree
|
|
|
179
189
|
complete -c $__vw_bin -l json -d "Output machine-readable JSON"
|
|
180
190
|
complete -c $__vw_bin -l verbose -d "Enable verbose logs"
|
|
181
191
|
complete -c $__vw_bin -l no-hooks -d "Disable hooks for this run (requires --allow-unsafe)"
|
|
192
|
+
complete -c $__vw_bin -l no-gh -d "Disable GitHub CLI based PR status checks for this run"
|
|
193
|
+
complete -c $__vw_bin -l full-path -d "Disable list table path truncation"
|
|
182
194
|
complete -c $__vw_bin -l allow-unsafe -d "Explicit unsafe override in non-TTY mode"
|
|
183
195
|
complete -c $__vw_bin -l strict-post-hooks -d "Fail when post hooks fail"
|
|
184
196
|
complete -c $__vw_bin -l hook-timeout-ms -r -d "Override hook timeout"
|
|
@@ -190,6 +202,7 @@ for __vw_bin in vw vde-worktree
|
|
|
190
202
|
complete -c $__vw_bin -n "__fish_seen_subcommand_from path" -a "(__vw_worktree_candidates_with_meta)"
|
|
191
203
|
complete -c $__vw_bin -n "__fish_seen_subcommand_from switch" -a "(__vw_worktree_candidates_with_meta)"
|
|
192
204
|
complete -c $__vw_bin -n "__fish_seen_subcommand_from del" -a "(__vw_worktree_candidates_with_meta)"
|
|
205
|
+
complete -c $__vw_bin -n "__fish_seen_subcommand_from list" -l full-path -d "Disable list table path truncation"
|
|
193
206
|
complete -c $__vw_bin -n "__fish_seen_subcommand_from get" -a "(__vw_remote_branches)"
|
|
194
207
|
complete -c $__vw_bin -n "__fish_seen_subcommand_from absorb" -a "(__vw_worktree_candidates_with_meta)"
|
|
195
208
|
complete -c $__vw_bin -n "__fish_seen_subcommand_from unabsorb" -a "(__vw_worktree_candidates_with_meta)"
|
package/completions/zsh/_vw
CHANGED
|
@@ -45,6 +45,10 @@ const toFlag = (value) => {
|
|
|
45
45
|
if (value === false) return "no"
|
|
46
46
|
return "unknown"
|
|
47
47
|
}
|
|
48
|
+
const toPrStatus = (value) => {
|
|
49
|
+
if (typeof value !== "string" || value.length === 0) return "n/a"
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
48
52
|
let payload
|
|
49
53
|
try {
|
|
50
54
|
payload = JSON.parse(fs.readFileSync(0, "utf8"))
|
|
@@ -55,10 +59,11 @@ const worktrees = Array.isArray(payload.worktrees) ? payload.worktrees : []
|
|
|
55
59
|
for (const worktree of worktrees) {
|
|
56
60
|
if (typeof worktree?.branch !== "string" || worktree.branch.length === 0) continue
|
|
57
61
|
const merged = toFlag(worktree?.merged?.overall)
|
|
62
|
+
const pr = toPrStatus(worktree?.pr?.status)
|
|
58
63
|
const dirty = worktree?.dirty === true ? "yes" : "no"
|
|
59
64
|
const locked = worktree?.locked?.value === true ? "yes" : "no"
|
|
60
65
|
const path = toDisplayPath(worktree?.path)
|
|
61
|
-
const summary = `merged=${merged} dirty=${dirty} locked=${locked}${path ? ` path=${path}` : ""}`
|
|
66
|
+
const summary = `merged=${merged} pr=${pr} dirty=${dirty} locked=${locked}${path ? ` path=${path}` : ""}`
|
|
62
67
|
const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
|
|
63
68
|
process.stdout.write(`${worktree.branch}\t${sanitized}\n`)
|
|
64
69
|
}
|
|
@@ -78,6 +83,10 @@ const toFlag = (value) => {
|
|
|
78
83
|
if (value === false) return "no"
|
|
79
84
|
return "unknown"
|
|
80
85
|
}
|
|
86
|
+
const toPrStatus = (value) => {
|
|
87
|
+
if (typeof value !== "string" || value.length === 0) return "n/a"
|
|
88
|
+
return value
|
|
89
|
+
}
|
|
81
90
|
let payload
|
|
82
91
|
try {
|
|
83
92
|
payload = JSON.parse(fs.readFileSync(0, "utf8"))
|
|
@@ -95,9 +104,10 @@ for (const worktree of worktrees) {
|
|
|
95
104
|
const name = rel.split(path.sep).join("/")
|
|
96
105
|
const branch = typeof worktree?.branch === "string" && worktree.branch.length > 0 ? worktree.branch : "(detached)"
|
|
97
106
|
const merged = toFlag(worktree?.merged?.overall)
|
|
107
|
+
const pr = toPrStatus(worktree?.pr?.status)
|
|
98
108
|
const dirty = worktree?.dirty === true ? "yes" : "no"
|
|
99
109
|
const locked = worktree?.locked?.value === true ? "yes" : "no"
|
|
100
|
-
const summary = `branch=${branch} merged=${merged} dirty=${dirty} locked=${locked}`
|
|
110
|
+
const summary = `branch=${branch} merged=${merged} pr=${pr} dirty=${dirty} locked=${locked}`
|
|
101
111
|
const sanitized = summary.replace(/[\t\r\n]+/g, " ").trim()
|
|
102
112
|
process.stdout.write(`${name}\t${sanitized}\n`)
|
|
103
113
|
}
|
|
@@ -253,6 +263,8 @@ _vw() {
|
|
|
253
263
|
"--json[Output machine-readable JSON]"
|
|
254
264
|
"--verbose[Enable verbose logs]"
|
|
255
265
|
"--no-hooks[Disable hooks for this run]"
|
|
266
|
+
"--no-gh[Disable GitHub CLI based PR status checks for this run]"
|
|
267
|
+
"--full-path[Disable list table path truncation]"
|
|
256
268
|
"--allow-unsafe[Allow unsafe behavior in non-TTY mode]"
|
|
257
269
|
"--strict-post-hooks[Fail when post hooks fail]"
|
|
258
270
|
"--hook-timeout-ms[Override hook timeout]:ms:"
|
|
@@ -274,6 +286,10 @@ _vw() {
|
|
|
274
286
|
arguments)
|
|
275
287
|
local current_command="${line[1]:-${words[2]}}"
|
|
276
288
|
case "${current_command}" in
|
|
289
|
+
list)
|
|
290
|
+
_arguments \
|
|
291
|
+
"--full-path[Disable list table path truncation]"
|
|
292
|
+
;;
|
|
277
293
|
status)
|
|
278
294
|
_arguments \
|
|
279
295
|
"1:branch:_vw_complete_worktree_branches_with_meta"
|
package/dist/index.mjs
CHANGED
|
@@ -853,43 +853,68 @@ const toTargetBranches = ({ branches, baseBranch }) => {
|
|
|
853
853
|
}
|
|
854
854
|
return [...uniqueBranches];
|
|
855
855
|
};
|
|
856
|
-
const
|
|
857
|
-
return new Map(branches.map((branch) => [branch,
|
|
856
|
+
const buildUnknownPrStatusMap = (branches) => {
|
|
857
|
+
return new Map(branches.map((branch) => [branch, "unknown"]));
|
|
858
858
|
};
|
|
859
|
-
const
|
|
859
|
+
const parseUpdatedAtMillis = (value) => {
|
|
860
|
+
if (typeof value !== "string" || value.length === 0) return Number.NEGATIVE_INFINITY;
|
|
861
|
+
const parsed = Date.parse(value);
|
|
862
|
+
if (Number.isNaN(parsed)) return Number.NEGATIVE_INFINITY;
|
|
863
|
+
return parsed;
|
|
864
|
+
};
|
|
865
|
+
const toPrStatus = (record) => {
|
|
866
|
+
if (typeof record.mergedAt === "string" && record.mergedAt.length > 0) return "merged";
|
|
867
|
+
const state = typeof record.state === "string" ? record.state.toUpperCase() : "";
|
|
868
|
+
if (state === "MERGED") return "merged";
|
|
869
|
+
if (state === "OPEN") return "open";
|
|
870
|
+
if (state === "CLOSED") return "closed_unmerged";
|
|
871
|
+
return "unknown";
|
|
872
|
+
};
|
|
873
|
+
const parsePrStatusByBranch = ({ raw, targetBranches }) => {
|
|
860
874
|
try {
|
|
861
875
|
const parsed = JSON.parse(raw);
|
|
862
876
|
if (Array.isArray(parsed) !== true) return null;
|
|
877
|
+
const targetBranchSet = new Set(targetBranches);
|
|
863
878
|
const records = parsed;
|
|
864
|
-
const
|
|
865
|
-
for (const record of records) {
|
|
879
|
+
const latestByBranch = /* @__PURE__ */ new Map();
|
|
880
|
+
for (const [index, record] of records.entries()) {
|
|
866
881
|
if (typeof record?.headRefName !== "string" || record.headRefName.length === 0) continue;
|
|
867
|
-
if (
|
|
868
|
-
|
|
869
|
-
|
|
882
|
+
if (targetBranchSet.has(record.headRefName) !== true) continue;
|
|
883
|
+
const updatedAtMillis = parseUpdatedAtMillis(record.updatedAt);
|
|
884
|
+
const status = toPrStatus(record);
|
|
885
|
+
const current = latestByBranch.get(record.headRefName);
|
|
886
|
+
if (current === void 0 || updatedAtMillis > current.updatedAtMillis || updatedAtMillis === current.updatedAtMillis && index > current.index) latestByBranch.set(record.headRefName, {
|
|
887
|
+
updatedAtMillis,
|
|
888
|
+
index,
|
|
889
|
+
status
|
|
890
|
+
});
|
|
870
891
|
}
|
|
871
|
-
|
|
892
|
+
const result = /* @__PURE__ */ new Map();
|
|
893
|
+
for (const branch of targetBranches) {
|
|
894
|
+
const latest = latestByBranch.get(branch);
|
|
895
|
+
result.set(branch, latest?.status ?? "none");
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
872
898
|
} catch {
|
|
873
899
|
return null;
|
|
874
900
|
}
|
|
875
901
|
};
|
|
876
|
-
const
|
|
877
|
-
if (enabled !== true) return /* @__PURE__ */ new Map();
|
|
902
|
+
const resolvePrStatusByBranchBatch = async ({ repoRoot, baseBranch, branches, enabled = true, runGh = defaultRunGh }) => {
|
|
878
903
|
if (baseBranch === null) return /* @__PURE__ */ new Map();
|
|
879
904
|
const targetBranches = toTargetBranches({
|
|
880
905
|
branches,
|
|
881
906
|
baseBranch
|
|
882
907
|
});
|
|
883
908
|
if (targetBranches.length === 0) return /* @__PURE__ */ new Map();
|
|
909
|
+
if (enabled !== true) return buildUnknownPrStatusMap(targetBranches);
|
|
884
910
|
try {
|
|
885
|
-
const targetBranchSet = new Set(targetBranches);
|
|
886
911
|
const result = await runGh({
|
|
887
912
|
cwd: repoRoot,
|
|
888
913
|
args: [
|
|
889
914
|
"pr",
|
|
890
915
|
"list",
|
|
891
916
|
"--state",
|
|
892
|
-
"
|
|
917
|
+
"all",
|
|
893
918
|
"--base",
|
|
894
919
|
baseBranch,
|
|
895
920
|
"--search",
|
|
@@ -897,21 +922,19 @@ const resolveMergedByPrBatch = async ({ repoRoot, baseBranch, branches, enabled
|
|
|
897
922
|
"--limit",
|
|
898
923
|
"1000",
|
|
899
924
|
"--json",
|
|
900
|
-
"headRefName,mergedAt"
|
|
925
|
+
"headRefName,state,mergedAt,updatedAt"
|
|
901
926
|
]
|
|
902
927
|
});
|
|
903
|
-
if (result.exitCode !== 0) return
|
|
904
|
-
const
|
|
928
|
+
if (result.exitCode !== 0) return buildUnknownPrStatusMap(targetBranches);
|
|
929
|
+
const prStatusByBranch = parsePrStatusByBranch({
|
|
905
930
|
raw: result.stdout,
|
|
906
|
-
targetBranches
|
|
931
|
+
targetBranches
|
|
907
932
|
});
|
|
908
|
-
if (
|
|
909
|
-
return
|
|
910
|
-
return [branch, mergedBranches.has(branch)];
|
|
911
|
-
}));
|
|
933
|
+
if (prStatusByBranch === null) return buildUnknownPrStatusMap(targetBranches);
|
|
934
|
+
return prStatusByBranch;
|
|
912
935
|
} catch (error) {
|
|
913
|
-
if (error.code === "ENOENT") return
|
|
914
|
-
return
|
|
936
|
+
if (error.code === "ENOENT") return buildUnknownPrStatusMap(targetBranches);
|
|
937
|
+
return buildUnknownPrStatusMap(targetBranches);
|
|
915
938
|
}
|
|
916
939
|
};
|
|
917
940
|
|
|
@@ -1110,7 +1133,7 @@ const resolveLifecycleFromReflog = async ({ repoRoot, branch, baseBranch }) => {
|
|
|
1110
1133
|
divergedHead: latestWorkHead
|
|
1111
1134
|
};
|
|
1112
1135
|
};
|
|
1113
|
-
const resolveMergedState = async ({ repoRoot, branch, head, baseBranch,
|
|
1136
|
+
const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStatusByBranch }) => {
|
|
1114
1137
|
if (branch === null) return {
|
|
1115
1138
|
byAncestry: null,
|
|
1116
1139
|
byPR: null,
|
|
@@ -1131,7 +1154,10 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, mergedBy
|
|
|
1131
1154
|
if (result.exitCode === 0) byAncestry = true;
|
|
1132
1155
|
else if (result.exitCode === 1) byAncestry = false;
|
|
1133
1156
|
}
|
|
1134
|
-
const
|
|
1157
|
+
const prStatus = branch === baseBranch ? null : prStatusByBranch.get(branch) ?? null;
|
|
1158
|
+
let byPR = null;
|
|
1159
|
+
if (prStatus === "merged") byPR = true;
|
|
1160
|
+
else if (prStatus === "none" || prStatus === "open" || prStatus === "closed_unmerged") byPR = false;
|
|
1135
1161
|
let byLifecycle = null;
|
|
1136
1162
|
if (baseBranch !== null) {
|
|
1137
1163
|
const lifecycle = await upsertWorktreeMergeLifecycle({
|
|
@@ -1182,6 +1208,10 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, mergedBy
|
|
|
1182
1208
|
})
|
|
1183
1209
|
};
|
|
1184
1210
|
};
|
|
1211
|
+
const resolvePrState = ({ branch, baseBranch, prStatusByBranch }) => {
|
|
1212
|
+
if (branch === null || branch === baseBranch) return { status: null };
|
|
1213
|
+
return { status: prStatusByBranch.get(branch) ?? null };
|
|
1214
|
+
};
|
|
1185
1215
|
const resolveMergedOverall = ({ byAncestry, byPR, byLifecycle }) => {
|
|
1186
1216
|
if (byPR === true || byLifecycle === true) return true;
|
|
1187
1217
|
if (byAncestry === false) return false;
|
|
@@ -1228,7 +1258,7 @@ const resolveUpstreamState = async (worktreePath) => {
|
|
|
1228
1258
|
remote: upstreamRef.stdout.trim()
|
|
1229
1259
|
};
|
|
1230
1260
|
};
|
|
1231
|
-
const enrichWorktree = async ({ repoRoot, worktree, baseBranch,
|
|
1261
|
+
const enrichWorktree = async ({ repoRoot, worktree, baseBranch, prStatusByBranch }) => {
|
|
1232
1262
|
const [dirty, locked, merged, upstream] = await Promise.all([
|
|
1233
1263
|
resolveDirty(worktree.path),
|
|
1234
1264
|
resolveLockState({
|
|
@@ -1240,10 +1270,15 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, mergedByPrByBran
|
|
|
1240
1270
|
branch: worktree.branch,
|
|
1241
1271
|
head: worktree.head,
|
|
1242
1272
|
baseBranch,
|
|
1243
|
-
|
|
1273
|
+
prStatusByBranch
|
|
1244
1274
|
}),
|
|
1245
1275
|
resolveUpstreamState(worktree.path)
|
|
1246
1276
|
]);
|
|
1277
|
+
const pr = resolvePrState({
|
|
1278
|
+
branch: worktree.branch,
|
|
1279
|
+
baseBranch,
|
|
1280
|
+
prStatusByBranch
|
|
1281
|
+
});
|
|
1247
1282
|
return {
|
|
1248
1283
|
branch: worktree.branch,
|
|
1249
1284
|
path: worktree.path,
|
|
@@ -1251,6 +1286,7 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, mergedByPrByBran
|
|
|
1251
1286
|
dirty,
|
|
1252
1287
|
locked,
|
|
1253
1288
|
merged,
|
|
1289
|
+
pr,
|
|
1254
1290
|
upstream
|
|
1255
1291
|
};
|
|
1256
1292
|
};
|
|
@@ -1260,7 +1296,7 @@ const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
|
|
|
1260
1296
|
listGitWorktrees(repoRoot),
|
|
1261
1297
|
resolveEnableGh(repoRoot)
|
|
1262
1298
|
]);
|
|
1263
|
-
const
|
|
1299
|
+
const prStatusByBranch = await resolvePrStatusByBranchBatch({
|
|
1264
1300
|
repoRoot,
|
|
1265
1301
|
baseBranch,
|
|
1266
1302
|
branches: worktrees.map((worktree) => worktree.branch),
|
|
@@ -1274,7 +1310,7 @@ const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
|
|
|
1274
1310
|
repoRoot,
|
|
1275
1311
|
worktree,
|
|
1276
1312
|
baseBranch,
|
|
1277
|
-
|
|
1313
|
+
prStatusByBranch
|
|
1278
1314
|
});
|
|
1279
1315
|
}))
|
|
1280
1316
|
};
|
|
@@ -1452,6 +1488,10 @@ const CD_FZF_EXTRA_ARGS = [
|
|
|
1452
1488
|
"--preview-window=right,60%,wrap",
|
|
1453
1489
|
"--ansi"
|
|
1454
1490
|
];
|
|
1491
|
+
const LIST_TABLE_COLUMN_COUNT = 8;
|
|
1492
|
+
const LIST_TABLE_PATH_COLUMN_INDEX = 7;
|
|
1493
|
+
const LIST_TABLE_PATH_MIN_WIDTH = 12;
|
|
1494
|
+
const LIST_TABLE_CELL_HORIZONTAL_PADDING = 2;
|
|
1455
1495
|
const COMPLETION_SHELLS = ["zsh", "fish"];
|
|
1456
1496
|
const COMPLETION_FILE_BY_SHELL = {
|
|
1457
1497
|
zsh: "zsh/_vw",
|
|
@@ -1530,9 +1570,9 @@ const colorizeListTableLine = ({ line, theme }) => {
|
|
|
1530
1570
|
const segments = line.split("│");
|
|
1531
1571
|
if (segments.length < 3) return line;
|
|
1532
1572
|
const cells = segments.slice(1, -1);
|
|
1533
|
-
if (cells.length !==
|
|
1573
|
+
if (cells.length !== 8) return line;
|
|
1534
1574
|
const headers = cells.map((cell) => cell.trim());
|
|
1535
|
-
if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "
|
|
1575
|
+
if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "pr" && headers[4] === "locked" && headers[5] === "ahead" && headers[6] === "behind" && headers[7] === "path") {
|
|
1536
1576
|
const nextCells = cells.map((cell) => colorizeCellContent({
|
|
1537
1577
|
cell,
|
|
1538
1578
|
color: theme.header
|
|
@@ -1546,15 +1586,18 @@ const colorizeListTableLine = ({ line, theme }) => {
|
|
|
1546
1586
|
const branchCell = cells[0];
|
|
1547
1587
|
const dirtyCell = cells[1];
|
|
1548
1588
|
const mergedCell = cells[2];
|
|
1549
|
-
const
|
|
1550
|
-
const
|
|
1551
|
-
const
|
|
1552
|
-
const
|
|
1589
|
+
const prCell = cells[3];
|
|
1590
|
+
const lockedCell = cells[4];
|
|
1591
|
+
const aheadCell = cells[5];
|
|
1592
|
+
const behindCell = cells[6];
|
|
1593
|
+
const pathCell = cells[7];
|
|
1553
1594
|
const branchColor = branchCell.includes("(detached)") === true ? theme.branchDetached : branchCell.trimStart().startsWith("*") ? theme.branchCurrent : theme.branch;
|
|
1554
1595
|
const dirtyTrimmed = dirtyCell.trim();
|
|
1555
1596
|
const dirtyColor = dirtyTrimmed === "dirty" ? theme.dirty : dirtyTrimmed === "clean" ? theme.clean : theme.value;
|
|
1556
1597
|
const mergedTrimmed = mergedCell.trim();
|
|
1557
1598
|
const mergedColor = mergedTrimmed === "merged" ? theme.merged : mergedTrimmed === "unmerged" ? theme.unmerged : mergedTrimmed === "-" ? theme.base : theme.unknown;
|
|
1599
|
+
const prTrimmed = prCell.trim();
|
|
1600
|
+
const prColor = prTrimmed === "merged" ? theme.merged : prTrimmed === "open" ? theme.value : prTrimmed === "closed_unmerged" ? theme.unmerged : prTrimmed === "none" ? theme.muted : prTrimmed === "-" ? theme.base : theme.unknown;
|
|
1558
1601
|
const lockedColor = lockedCell.trim() === "locked" ? theme.locked : theme.muted;
|
|
1559
1602
|
const aheadTrimmed = aheadCell.trim();
|
|
1560
1603
|
const aheadValue = Number.parseInt(aheadTrimmed, 10);
|
|
@@ -1575,6 +1618,10 @@ const colorizeListTableLine = ({ line, theme }) => {
|
|
|
1575
1618
|
cell: mergedCell,
|
|
1576
1619
|
color: mergedColor
|
|
1577
1620
|
}),
|
|
1621
|
+
colorizeCellContent({
|
|
1622
|
+
cell: prCell,
|
|
1623
|
+
color: prColor
|
|
1624
|
+
}),
|
|
1578
1625
|
colorizeCellContent({
|
|
1579
1626
|
cell: lockedCell,
|
|
1580
1627
|
color: lockedColor
|
|
@@ -1613,9 +1660,14 @@ const commandHelpEntries = [
|
|
|
1613
1660
|
},
|
|
1614
1661
|
{
|
|
1615
1662
|
name: "list",
|
|
1616
|
-
usage: "vw list [--json]",
|
|
1663
|
+
usage: "vw list [--json] [--full-path]",
|
|
1617
1664
|
summary: "List worktrees with status metadata.",
|
|
1618
|
-
details: [
|
|
1665
|
+
details: [
|
|
1666
|
+
"Table output includes branch, path, dirty, lock, merged, PR state, and ahead/behind vs base branch.",
|
|
1667
|
+
"By default, long path values are truncated to fit terminal width.",
|
|
1668
|
+
"JSON output includes PR and upstream metadata fields."
|
|
1669
|
+
],
|
|
1670
|
+
options: ["--full-path"]
|
|
1619
1671
|
},
|
|
1620
1672
|
{
|
|
1621
1673
|
name: "status",
|
|
@@ -2432,10 +2484,35 @@ const formatMergedColor = ({ mergedState, theme }) => {
|
|
|
2432
2484
|
if (normalized === "base") return theme.base(mergedState);
|
|
2433
2485
|
return theme.unknown(mergedState);
|
|
2434
2486
|
};
|
|
2487
|
+
const formatPrDisplayState = ({ prStatus, isBaseBranch }) => {
|
|
2488
|
+
if (isBaseBranch || prStatus === null) return "-";
|
|
2489
|
+
if (prStatus === "none" || prStatus === "open" || prStatus === "merged" || prStatus === "closed_unmerged" || prStatus === "unknown") return prStatus;
|
|
2490
|
+
return "unknown";
|
|
2491
|
+
};
|
|
2435
2492
|
const formatListUpstreamCount = (value) => {
|
|
2436
2493
|
if (value === null) return "-";
|
|
2437
2494
|
return String(value);
|
|
2438
2495
|
};
|
|
2496
|
+
const resolveListColumnContentWidth = ({ rows, columnIndex }) => {
|
|
2497
|
+
return rows.reduce((width, row) => {
|
|
2498
|
+
const cell = row[columnIndex] ?? "";
|
|
2499
|
+
return Math.max(width, stringWidth(cell));
|
|
2500
|
+
}, 0);
|
|
2501
|
+
};
|
|
2502
|
+
const resolveListPathColumnWidth = ({ rows, disablePathTruncation }) => {
|
|
2503
|
+
if (disablePathTruncation) return null;
|
|
2504
|
+
if (process.stdout.isTTY !== true) return null;
|
|
2505
|
+
const terminalColumns = process.stdout.columns;
|
|
2506
|
+
if (typeof terminalColumns !== "number" || Number.isFinite(terminalColumns) !== true || terminalColumns <= 0) return null;
|
|
2507
|
+
const measuredNonPathWidth = Array.from({ length: LIST_TABLE_PATH_COLUMN_INDEX }).map((_, index) => resolveListColumnContentWidth({
|
|
2508
|
+
rows,
|
|
2509
|
+
columnIndex: index
|
|
2510
|
+
})).reduce((sum, width) => sum + width, 0);
|
|
2511
|
+
const borderWidth = LIST_TABLE_COLUMN_COUNT + 1;
|
|
2512
|
+
const paddingWidth = LIST_TABLE_COLUMN_COUNT * LIST_TABLE_CELL_HORIZONTAL_PADDING;
|
|
2513
|
+
const availablePathWidth = Math.floor(terminalColumns) - borderWidth - paddingWidth - measuredNonPathWidth;
|
|
2514
|
+
return Math.max(LIST_TABLE_PATH_MIN_WIDTH, availablePathWidth);
|
|
2515
|
+
};
|
|
2439
2516
|
const resolveAheadBehindAgainstBaseBranch = async ({ repoRoot, baseBranch, worktree }) => {
|
|
2440
2517
|
if (baseBranch === null) return {
|
|
2441
2518
|
ahead: null,
|
|
@@ -2593,7 +2670,8 @@ const renderGeneralHelpText = ({ version }) => {
|
|
|
2593
2670
|
" --json Output machine-readable JSON.",
|
|
2594
2671
|
" --verbose Enable verbose logs.",
|
|
2595
2672
|
" --no-hooks Disable hooks for this run (requires --allow-unsafe).",
|
|
2596
|
-
" --no-gh Disable GitHub CLI based PR
|
|
2673
|
+
" --no-gh Disable GitHub CLI based PR status checks for this run.",
|
|
2674
|
+
" --full-path Disable list table path truncation.",
|
|
2597
2675
|
" --allow-unsafe Explicitly allow unsafe behavior in non-TTY mode.",
|
|
2598
2676
|
" --hook-timeout-ms <ms> Override hook timeout.",
|
|
2599
2677
|
" --lock-timeout-ms <ms> Override repository lock timeout.",
|
|
@@ -2696,9 +2774,13 @@ const createCli = (options = {}) => {
|
|
|
2696
2774
|
},
|
|
2697
2775
|
gh: {
|
|
2698
2776
|
type: "boolean",
|
|
2699
|
-
description: "Enable GitHub CLI based PR
|
|
2777
|
+
description: "Enable GitHub CLI based PR status checks (disable with --no-gh)",
|
|
2700
2778
|
default: true
|
|
2701
2779
|
},
|
|
2780
|
+
fullPath: {
|
|
2781
|
+
type: "boolean",
|
|
2782
|
+
description: "Disable list table path truncation"
|
|
2783
|
+
},
|
|
2702
2784
|
allowUnsafe: {
|
|
2703
2785
|
type: "boolean",
|
|
2704
2786
|
description: "Allow unsafe operations"
|
|
@@ -3025,36 +3107,53 @@ const createCli = (options = {}) => {
|
|
|
3025
3107
|
return EXIT_CODE.OK;
|
|
3026
3108
|
}
|
|
3027
3109
|
const theme = createCatppuccinTheme({ enabled: shouldUseAnsiColors({ interactive: runtime.isInteractive }) });
|
|
3110
|
+
const rows = [[
|
|
3111
|
+
"branch",
|
|
3112
|
+
"dirty",
|
|
3113
|
+
"merged",
|
|
3114
|
+
"pr",
|
|
3115
|
+
"locked",
|
|
3116
|
+
"ahead",
|
|
3117
|
+
"behind",
|
|
3118
|
+
"path"
|
|
3119
|
+
], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
|
|
3120
|
+
const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
|
|
3121
|
+
repoRoot,
|
|
3122
|
+
baseBranch: snapshot.baseBranch,
|
|
3123
|
+
worktree
|
|
3124
|
+
});
|
|
3125
|
+
const isBaseBranch = worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch;
|
|
3126
|
+
const mergedState = isBaseBranch === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
|
|
3127
|
+
const prState = formatPrDisplayState({
|
|
3128
|
+
prStatus: worktree.pr.status,
|
|
3129
|
+
isBaseBranch
|
|
3130
|
+
});
|
|
3131
|
+
return [
|
|
3132
|
+
`${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
|
|
3133
|
+
worktree.dirty ? "dirty" : "clean",
|
|
3134
|
+
mergedState,
|
|
3135
|
+
prState,
|
|
3136
|
+
worktree.locked.value ? "locked" : "-",
|
|
3137
|
+
formatListUpstreamCount(distanceFromBase.ahead),
|
|
3138
|
+
formatListUpstreamCount(distanceFromBase.behind),
|
|
3139
|
+
formatDisplayPath(worktree.path)
|
|
3140
|
+
];
|
|
3141
|
+
}))];
|
|
3142
|
+
const pathColumnWidth = resolveListPathColumnWidth({
|
|
3143
|
+
rows,
|
|
3144
|
+
disablePathTruncation: parsedArgs.fullPath === true
|
|
3145
|
+
});
|
|
3146
|
+
const columnsConfig = pathColumnWidth === null ? void 0 : { [LIST_TABLE_PATH_COLUMN_INDEX]: {
|
|
3147
|
+
width: pathColumnWidth,
|
|
3148
|
+
truncate: pathColumnWidth
|
|
3149
|
+
} };
|
|
3028
3150
|
const colorized = colorizeListTable({
|
|
3029
|
-
rendered: table(
|
|
3030
|
-
"branch",
|
|
3031
|
-
"dirty",
|
|
3032
|
-
"merged",
|
|
3033
|
-
"locked",
|
|
3034
|
-
"ahead",
|
|
3035
|
-
"behind",
|
|
3036
|
-
"path"
|
|
3037
|
-
], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
|
|
3038
|
-
const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
|
|
3039
|
-
repoRoot,
|
|
3040
|
-
baseBranch: snapshot.baseBranch,
|
|
3041
|
-
worktree
|
|
3042
|
-
});
|
|
3043
|
-
const mergedState = (worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch) === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
|
|
3044
|
-
return [
|
|
3045
|
-
`${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
|
|
3046
|
-
worktree.dirty ? "dirty" : "clean",
|
|
3047
|
-
mergedState,
|
|
3048
|
-
worktree.locked.value ? "locked" : "-",
|
|
3049
|
-
formatListUpstreamCount(distanceFromBase.ahead),
|
|
3050
|
-
formatListUpstreamCount(distanceFromBase.behind),
|
|
3051
|
-
formatDisplayPath(worktree.path)
|
|
3052
|
-
];
|
|
3053
|
-
}))], {
|
|
3151
|
+
rendered: table(rows, {
|
|
3054
3152
|
border: getBorderCharacters("norc"),
|
|
3055
3153
|
drawHorizontalLine: (lineIndex, rowCount) => {
|
|
3056
3154
|
return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount;
|
|
3057
|
-
}
|
|
3155
|
+
},
|
|
3156
|
+
columns: columnsConfig
|
|
3058
3157
|
}),
|
|
3059
3158
|
theme
|
|
3060
3159
|
});
|