@danmoisan/drm-copilot-mcp 1.1.14 → 1.1.16
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/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +4 -5
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget-route.ps1 +137 -0
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +95 -66
- package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
- package/resources/claude-customizations/.claude/rules/powershell.md +3 -3
- package/resources/claude-customizations/.claude/rules/shell.md +8 -7
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +3 -4
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +6 -6
- package/resources/claude-customizations/pack-manifests/core.json +11 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +1 -0
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -2
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +285 -80
- package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +7 -7
- package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +8 -10
- package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +2 -2
- package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +3 -3
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +6 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cleanup_worktrees_lib.sh: sourceable function library for the cleanup-worktrees
|
|
3
|
+
# tool. Provides the full classification ladder (ancestry -> content-neutral ->
|
|
4
|
+
# cherry-equivalence -> rename-aware blob fallback -> unique-residual selection) and
|
|
5
|
+
# the report driver. The git-binary test seam (cleanup_wt_git), branch/worktree
|
|
6
|
+
# enumeration (enumerate_branches, parse_worktree_list), path normalization
|
|
7
|
+
# (normalize_wt_path), the current-worktree/branch protection set (compute_protected),
|
|
8
|
+
# and the main-freshness warning (check_main_freshness) now live in the sibling file
|
|
9
|
+
# cleanup_worktrees_enumerate_lib.sh, which MUST be sourced before this file.
|
|
10
|
+
# Consolidation, deletion, and the CLI live in cleanup_worktrees_actions_lib.sh and
|
|
11
|
+
# cleanup-worktrees.sh. Splitting the library keeps every file within the 500-line cap.
|
|
12
|
+
#
|
|
13
|
+
# Sourcing contract: this library defines functions only; it never runs work at
|
|
14
|
+
# source time, so the wrapper and the bats suites can source it without side effects.
|
|
15
|
+
#
|
|
16
|
+
# It depends on cleanup_worktrees_enumerate_lib.sh (source that first). All git
|
|
17
|
+
# commands go through cleanup_wt_git so tests can stub the git binary via
|
|
18
|
+
# CLEANUP_WT_GIT_BIN.
|
|
19
|
+
#
|
|
20
|
+
# Git exit-code capture rule: a git command with an EXPECTED non-zero exit in the
|
|
21
|
+
# ladder (merge-base --is-ancestor, diff --quiet) is captured at the function-body
|
|
22
|
+
# level with `|| rc=$?` and mapped to a verdict token. A git command whose output is
|
|
23
|
+
# ITERATED or whose exit code is authoritative (cherry, diff-tree, ls-tree, rev-list)
|
|
24
|
+
# is captured in the PARENT shell (`out=$(cmd) || rc=$?`, then iterated over
|
|
25
|
+
# `<<<"$out"`) so a non-zero exit is observed rather than lost inside a process
|
|
26
|
+
# substitution or a pipeline. Internal hard-error verdict tokens (echo-verdict
|
|
27
|
+
# contract, the callee returns 0; classify_branch maps each to the report state):
|
|
28
|
+
# CHERRY_ERROR - non-zero `git cherry` exit (classify_cherry_equivalent).
|
|
29
|
+
# DIFF_TREE_ERROR - non-zero `git diff-tree` exit while probing a `+` commit's diff
|
|
30
|
+
# for the empty-residual case (classify_cherry_equivalent).
|
|
31
|
+
# RESIDUAL_ERROR - non-zero exit of the name-status `git diff-tree` read or the
|
|
32
|
+
# D-rung `git ls-tree` probe (classify_residual_commit).
|
|
33
|
+
# MINUS_PRESENT - a `- <sha>` cherry line was seen (partial-merge signal); read by
|
|
34
|
+
# classify_branch from the captured verdict instead of a second
|
|
35
|
+
# `git cherry` invocation whose exit code would be discarded.
|
|
36
|
+
# A hard failure of any enumeration/protection/cherry/diff-tree/ls-tree/rev-list read
|
|
37
|
+
# maps to the BRANCH|<name>|ANCESTRY_ERROR report state and a non-zero return. A hard
|
|
38
|
+
# git failure never resolves to a MERGED_* verdict.
|
|
39
|
+
#
|
|
40
|
+
# Report line contract (LC_ALL=C ordered, pipe-delimited, one record per line):
|
|
41
|
+
# BRANCH|<name>|<state>
|
|
42
|
+
# COMMIT|<branch>|<sha>|<state>|<paths-csv>|<author>|<author-date>
|
|
43
|
+
# WORKTREE|<path>|<branch-or-DETACHED>|<flags>
|
|
44
|
+
# WORKTREE|<path>|DETACHED|<state>|<flags> (detached registration; replaces the above)
|
|
45
|
+
# WARN|main-divergence|<local-sha>|<origin-sha>
|
|
46
|
+
# DIRTY|<worktree-path>|<status-porcelain-line>
|
|
47
|
+
# DIRTFILE|<worktree-path>|<verdict>|<detail>|<status-code>|<file-path>
|
|
48
|
+
# DIRTSUM|<worktree-path>|<aggregate>|<detail> (aggregate gates --clear-disposable)
|
|
49
|
+
# ACTION|<verb>|<target>|<result> (apply mode only; emitted by the actions lib)
|
|
50
|
+
# ORPHAN_DIR|<path>|<size> advisory, read-only records defined in
|
|
51
|
+
# STALE_REF|<refname> cleanup_worktrees_report_records_lib.sh; none of
|
|
52
|
+
# CHILD_OF|<branch>|<ancestor> these four unlocks a destructive action.
|
|
53
|
+
# WARN|registration-lost|<path>
|
|
54
|
+
# Branch states: NOT_MERGED | MERGED_CLEAN | MERGED_CONTENT_NEUTRAL |
|
|
55
|
+
# MERGED_EQUIVALENT | HAS_UNIQUE_RESIDUALS | PROTECTED_CURRENT; ANCESTRY_ERROR is a
|
|
56
|
+
# hard failure. Per-commit states: EQUIVALENT | CONTENT_ON_MAIN | EMPTY | UNIQUE |
|
|
57
|
+
# CONFLICT.
|
|
58
|
+
|
|
59
|
+
classify_ancestry() {
|
|
60
|
+
# First ladder rung: is <tip> an ancestor of main?
|
|
61
|
+
#
|
|
62
|
+
# Wraps `git merge-base --is-ancestor <tip> main`, capturing the exit code with
|
|
63
|
+
# `|| rc=$?` so set -e does not abort on the expected non-zero. Echoes exactly one
|
|
64
|
+
# verdict token and always returns 0:
|
|
65
|
+
# exit 0 -> MERGED_CLEAN (delete-eligible; zero residual commits)
|
|
66
|
+
# exit 1 -> NOT_ANCESTOR (continue the ladder)
|
|
67
|
+
# exit > 1 -> ANCESTRY_ERROR (hard failure for the branch; never "not merged")
|
|
68
|
+
#
|
|
69
|
+
# Args: $1 = branch tip (name or sha).
|
|
70
|
+
local tip="$1" rc=0
|
|
71
|
+
cleanup_wt_git merge-base --is-ancestor "$tip" main >/dev/null 2>&1 || rc=$?
|
|
72
|
+
if ((rc == 0)); then
|
|
73
|
+
printf 'MERGED_CLEAN\n'
|
|
74
|
+
elif ((rc == 1)); then
|
|
75
|
+
printf 'NOT_ANCESTOR\n'
|
|
76
|
+
else
|
|
77
|
+
printf 'ANCESTRY_ERROR\n'
|
|
78
|
+
fi
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
classify_content_neutral() {
|
|
83
|
+
# Second ladder rung: does the branch add no net content versus main?
|
|
84
|
+
#
|
|
85
|
+
# Runs `git diff --quiet main...<branch>` (three-dot: merge-base(main,branch) ->
|
|
86
|
+
# branch tip) with `|| rc=$?`. This short-circuit runs BEFORE any per-commit
|
|
87
|
+
# `git cherry` analysis and catches revert-pairs (a commit and its later revert
|
|
88
|
+
# on the same branch net to nothing). Echoes exactly one verdict token and always
|
|
89
|
+
# returns 0:
|
|
90
|
+
# exit 0 -> MERGED_CONTENT_NEUTRAL (delete-eligible)
|
|
91
|
+
# exit 1 -> NOT_NEUTRAL (continue the ladder)
|
|
92
|
+
# exit > 1 -> CONTENT_NEUTRAL_ERROR (hard failure for the branch)
|
|
93
|
+
#
|
|
94
|
+
# Args: $1 = branch name.
|
|
95
|
+
local branch="$1" rc=0
|
|
96
|
+
cleanup_wt_git diff --quiet "main...$branch" >/dev/null 2>&1 || rc=$?
|
|
97
|
+
if ((rc == 0)); then
|
|
98
|
+
printf 'MERGED_CONTENT_NEUTRAL\n'
|
|
99
|
+
elif ((rc == 1)); then
|
|
100
|
+
printf 'NOT_NEUTRAL\n'
|
|
101
|
+
else
|
|
102
|
+
printf 'CONTENT_NEUTRAL_ERROR\n'
|
|
103
|
+
fi
|
|
104
|
+
return 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
classify_cherry_equivalent() {
|
|
108
|
+
# Third ladder rung: patch-id equivalence via `git cherry main <branch>`.
|
|
109
|
+
#
|
|
110
|
+
# `git cherry` emits `- <sha>` when an equivalent patch already exists on main and
|
|
111
|
+
# `+ <sha>` when none does. A `+` commit whose diff is empty (verified with
|
|
112
|
+
# `git diff-tree --no-commit-id -r <sha>` producing no output) is also counted as
|
|
113
|
+
# equivalent (droppable empty commit). When every residual is equivalent the branch
|
|
114
|
+
# is content-equivalent-merged.
|
|
115
|
+
#
|
|
116
|
+
# Output contract (one token per line; always returns 0):
|
|
117
|
+
# - if all residuals are equivalent: a single line `MERGED_EQUIVALENT`
|
|
118
|
+
# - otherwise: a `MINUS_PRESENT` line first (only when any `- <sha>` cherry line
|
|
119
|
+
# was seen), then one `RESIDUAL <sha>` line per remaining `+` commit for the
|
|
120
|
+
# blob-level tier (classify_residual_commit). classify_branch reads MINUS_PRESENT
|
|
121
|
+
# from this captured verdict instead of re-invoking `git cherry`.
|
|
122
|
+
# - on a hard `git cherry` failure (non-zero exit): a single line `CHERRY_ERROR`.
|
|
123
|
+
# `git cherry` has no expected non-zero in this ladder, so any non-zero exit is
|
|
124
|
+
# a hard failure; the caller (classify_branch) maps CHERRY_ERROR to the
|
|
125
|
+
# ANCESTRY_ERROR report state and never to MERGED_EQUIVALENT.
|
|
126
|
+
# - on a hard `git diff-tree` failure while probing a `+` commit's diff (non-zero
|
|
127
|
+
# exit): a single line `DIFF_TREE_ERROR`. diff-tree has no expected non-zero
|
|
128
|
+
# here, so any non-zero exit is a hard failure; the caller maps DIFF_TREE_ERROR
|
|
129
|
+
# to ANCESTRY_ERROR and never to MERGED_EQUIVALENT. An empty diff-tree output
|
|
130
|
+
# with exit 0 remains the legitimate droppable empty-commit case.
|
|
131
|
+
#
|
|
132
|
+
# Args: $1 = branch name.
|
|
133
|
+
local branch="$1" rc=0 line marker sha out minus_seen=0
|
|
134
|
+
local -a residuals=()
|
|
135
|
+
# Guarded parent-shell capture: a non-zero `git cherry` exit is observed here and
|
|
136
|
+
# reported as CHERRY_ERROR before any residual processing, so an empty/failed cherry
|
|
137
|
+
# result cannot be mistaken for "all residuals equivalent".
|
|
138
|
+
out=$(cleanup_wt_git cherry main "$branch") || rc=$?
|
|
139
|
+
if ((rc != 0)); then
|
|
140
|
+
printf 'CHERRY_ERROR\n'
|
|
141
|
+
return 0
|
|
142
|
+
fi
|
|
143
|
+
while IFS= read -r line || [[ -n $line ]]; do
|
|
144
|
+
[[ -z $line ]] && continue
|
|
145
|
+
marker=${line%% *}
|
|
146
|
+
sha=${line#* }
|
|
147
|
+
# `-` lines are patch-id equivalent: nothing to carry forward, but record that a
|
|
148
|
+
# partial-merge signal exists so classify_branch need not re-invoke `git cherry`.
|
|
149
|
+
if [[ $marker == "-" ]]; then
|
|
150
|
+
minus_seen=1
|
|
151
|
+
continue
|
|
152
|
+
fi
|
|
153
|
+
if [[ $marker == "+" ]]; then
|
|
154
|
+
local dt="" dtrc=0
|
|
155
|
+
# Guarded parent-shell capture: a non-zero diff-tree exit is a hard failure
|
|
156
|
+
# reported as DIFF_TREE_ERROR, never mistaken for an empty (droppable) diff.
|
|
157
|
+
dt=$(cleanup_wt_git diff-tree --no-commit-id -r "$sha") || dtrc=$?
|
|
158
|
+
if ((dtrc != 0)); then
|
|
159
|
+
printf 'DIFF_TREE_ERROR\n'
|
|
160
|
+
return 0
|
|
161
|
+
fi
|
|
162
|
+
# An empty-diff residual commit is droppable; treat as equivalent.
|
|
163
|
+
[[ -z $dt ]] && continue
|
|
164
|
+
residuals+=("$sha")
|
|
165
|
+
fi
|
|
166
|
+
done <<<"$out"
|
|
167
|
+
if ((${#residuals[@]} == 0)); then
|
|
168
|
+
printf 'MERGED_EQUIVALENT\n'
|
|
169
|
+
else
|
|
170
|
+
((minus_seen == 1)) && printf 'MINUS_PRESENT\n'
|
|
171
|
+
for sha in "${residuals[@]}"; do
|
|
172
|
+
printf 'RESIDUAL %s\n' "$sha"
|
|
173
|
+
done
|
|
174
|
+
fi
|
|
175
|
+
return 0
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_blob_equal() {
|
|
179
|
+
# Return 0 iff <ref-a>:<path> and <ref-b>:<path> resolve to the same blob OID.
|
|
180
|
+
#
|
|
181
|
+
# A rev-parse failure for either side (e.g. the path is absent on that ref) is
|
|
182
|
+
# treated as "not equal" (return 1) so an added/removed path is never mistaken for
|
|
183
|
+
# equivalent content.
|
|
184
|
+
#
|
|
185
|
+
# Args: $1 = ref-a, $2 = ref-b, $3 = path.
|
|
186
|
+
local ref_a="$1" ref_b="$2" path="$3" oid_a oid_b
|
|
187
|
+
oid_a=$(cleanup_wt_git rev-parse "$ref_a:$path" 2>/dev/null) || return 1
|
|
188
|
+
oid_b=$(cleanup_wt_git rev-parse "$ref_b:$path" 2>/dev/null) || return 1
|
|
189
|
+
[[ -n $oid_a && $oid_a == "$oid_b" ]]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
classify_residual_commit() {
|
|
193
|
+
# Fourth ladder rung: rename-aware blob-OID comparison for one `+` commit.
|
|
194
|
+
#
|
|
195
|
+
# Enumerates touched paths via
|
|
196
|
+
# git diff-tree --no-commit-id --name-status -r -M <sha>
|
|
197
|
+
# and decides per path whether the branch's content already exists on main:
|
|
198
|
+
# A / M : compare blob OIDs of <branch>:<path> vs main:<path>; differ or absent
|
|
199
|
+
# on main -> unique.
|
|
200
|
+
# D : probe `git ls-tree main -- <path>` (guarded capture). Non-empty output
|
|
201
|
+
# means the path is still present on main -> the branch's deletion is
|
|
202
|
+
# unique work; empty output (exit 0) means the path is also absent on main
|
|
203
|
+
# -> droppable. A non-zero ls-tree exit is a hard failure -> RESIDUAL_ERROR.
|
|
204
|
+
# Rnnn : compare blob OIDs at the NEW path (rename target).
|
|
205
|
+
# All touched paths equivalent -> CONTENT_ON_MAIN; any unique path -> UNIQUE with
|
|
206
|
+
# the comma-separated unique path list.
|
|
207
|
+
#
|
|
208
|
+
# Output contract (single line; always returns 0):
|
|
209
|
+
# CONTENT_ON_MAIN
|
|
210
|
+
# UNIQUE|<path1,path2,...>
|
|
211
|
+
# RESIDUAL_ERROR (hard git failure of the name-status diff-tree read or the
|
|
212
|
+
# D-rung ls-tree probe; never resolved to CONTENT_ON_MAIN/UNIQUE)
|
|
213
|
+
#
|
|
214
|
+
# Args: $1 = branch name, $2 = commit sha.
|
|
215
|
+
local branch="$1" sha="$2" status p1 p2 relpath out dtrc=0
|
|
216
|
+
local -a unique_paths=()
|
|
217
|
+
# Guarded parent-shell capture: a non-zero name-status diff-tree exit is a hard
|
|
218
|
+
# failure reported as RESIDUAL_ERROR before any path processing, never resolved to
|
|
219
|
+
# CONTENT_ON_MAIN or UNIQUE.
|
|
220
|
+
out=$(cleanup_wt_git diff-tree --no-commit-id --name-status -r -M "$sha") || dtrc=$?
|
|
221
|
+
if ((dtrc != 0)); then
|
|
222
|
+
printf 'RESIDUAL_ERROR\n'
|
|
223
|
+
return 0
|
|
224
|
+
fi
|
|
225
|
+
while IFS=$'\t' read -r status p1 p2 || [[ -n $status ]]; do
|
|
226
|
+
[[ -z $status ]] && continue
|
|
227
|
+
case "$status" in
|
|
228
|
+
A | M)
|
|
229
|
+
relpath="$p1"
|
|
230
|
+
_blob_equal "$branch" main "$relpath" || unique_paths+=("$relpath")
|
|
231
|
+
;;
|
|
232
|
+
D)
|
|
233
|
+
relpath="$p1"
|
|
234
|
+
# Guarded ls-tree probe: a hard git failure (RESIDUAL_ERROR) is distinct from a
|
|
235
|
+
# path legitimately absent on main (droppable). rev-parse's exit code cannot
|
|
236
|
+
# make this distinction, so use ls-tree's stdout: non-empty -> present on main
|
|
237
|
+
# (the deletion is unique work); empty with exit 0 -> absent (droppable).
|
|
238
|
+
local lsout="" lsrc=0
|
|
239
|
+
lsout=$(cleanup_wt_git ls-tree main -- "$relpath") || lsrc=$?
|
|
240
|
+
if ((lsrc != 0)); then
|
|
241
|
+
printf 'RESIDUAL_ERROR\n'
|
|
242
|
+
return 0
|
|
243
|
+
fi
|
|
244
|
+
[[ -n $lsout ]] && unique_paths+=("$relpath")
|
|
245
|
+
;;
|
|
246
|
+
R*)
|
|
247
|
+
relpath="$p2"
|
|
248
|
+
_blob_equal "$branch" main "$relpath" || unique_paths+=("$relpath")
|
|
249
|
+
;;
|
|
250
|
+
esac
|
|
251
|
+
done <<<"$out"
|
|
252
|
+
if ((${#unique_paths[@]} == 0)); then
|
|
253
|
+
printf 'CONTENT_ON_MAIN\n'
|
|
254
|
+
else
|
|
255
|
+
local IFS=,
|
|
256
|
+
printf 'UNIQUE|%s\n' "${unique_paths[*]}"
|
|
257
|
+
fi
|
|
258
|
+
return 0
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
select_cherry_pick_candidates() {
|
|
262
|
+
# Emit one COMMIT record per UNIQUE residual commit, oldest-first.
|
|
263
|
+
#
|
|
264
|
+
# The non-equivalent `+` residual set comes from classify_cherry_equivalent; the
|
|
265
|
+
# oldest-first order and per-commit author/author-date come from
|
|
266
|
+
# git rev-list --reverse --no-merges --format='%H|%an|%aI' main..<branch>
|
|
267
|
+
# (`--reverse` = application order for cherry-picking; `--no-merges` excludes merge
|
|
268
|
+
# commits; the `commit <sha>` header lines from --format are dropped). Emits, per
|
|
269
|
+
# UNIQUE residual: COMMIT|<branch>|<sha>|UNIQUE|<paths-csv>|<author>|<author-date>.
|
|
270
|
+
#
|
|
271
|
+
# Args: $1 = branch name.
|
|
272
|
+
local branch="$1" line sha author date verdict paths restline rc=0
|
|
273
|
+
local ceout cerc=0 rlout
|
|
274
|
+
local -A is_residual=()
|
|
275
|
+
# Defensive re-check of the cherry verdict: a CHERRY_ERROR (or a non-zero return)
|
|
276
|
+
# must abort with a non-zero status rather than proceed with an empty residual map.
|
|
277
|
+
ceout=$(classify_cherry_equivalent "$branch") || cerc=$?
|
|
278
|
+
if ((cerc != 0)) || [[ $ceout == "CHERRY_ERROR" || $ceout == "DIFF_TREE_ERROR" ]]; then
|
|
279
|
+
return 2
|
|
280
|
+
fi
|
|
281
|
+
while IFS= read -r line; do
|
|
282
|
+
[[ $line == RESIDUAL\ * ]] || continue
|
|
283
|
+
is_residual[${line#RESIDUAL }]=1
|
|
284
|
+
done <<<"$ceout"
|
|
285
|
+
# Guarded parent-shell capture: a rev-list hard failure returns git's exit code
|
|
286
|
+
# with no COMMIT line emitted, so a candidate is never silently fabricated or
|
|
287
|
+
# dropped as a success.
|
|
288
|
+
rlout=$(cleanup_wt_git rev-list --reverse --no-merges --format='%H|%an|%aI' "main..$branch") || rc=$?
|
|
289
|
+
if ((rc != 0)); then
|
|
290
|
+
printf 'cleanup-worktrees: git rev-list failed for %s (rc=%s)\n' "$branch" "$rc" >&2
|
|
291
|
+
return "$rc"
|
|
292
|
+
fi
|
|
293
|
+
while IFS= read -r line || [[ -n $line ]]; do
|
|
294
|
+
[[ $line == commit\ * ]] && continue
|
|
295
|
+
[[ -z $line ]] && continue
|
|
296
|
+
sha=${line%%|*}
|
|
297
|
+
restline=${line#*|}
|
|
298
|
+
author=${restline%%|*}
|
|
299
|
+
date=${restline#*|}
|
|
300
|
+
[[ -n ${is_residual[$sha]:-} ]] || continue
|
|
301
|
+
verdict=$(classify_residual_commit "$branch" "$sha")
|
|
302
|
+
if [[ $verdict == "RESIDUAL_ERROR" ]]; then
|
|
303
|
+
# A residual hard error must abort candidate selection with a non-zero status
|
|
304
|
+
# rather than silently drop or fabricate a COMMIT record.
|
|
305
|
+
return 2
|
|
306
|
+
fi
|
|
307
|
+
if [[ $verdict == UNIQUE\|* ]]; then
|
|
308
|
+
paths=${verdict#UNIQUE|}
|
|
309
|
+
printf 'COMMIT|%s|%s|UNIQUE|%s|%s|%s\n' "$branch" "$sha" "$paths" "$author" "$date"
|
|
310
|
+
fi
|
|
311
|
+
done <<<"$rlout"
|
|
312
|
+
return "$rc"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
classify_branch() {
|
|
316
|
+
# Orchestrate the full classification ladder for one branch and emit its pinned
|
|
317
|
+
# report lines (BRANCH first, then COMMIT records for unique residuals oldest-first).
|
|
318
|
+
# No commit-message text is ever consulted. Ladder order (spec):
|
|
319
|
+
# 1. PROTECTED_CURRENT exclusion (branch-name OR worktree-path match; main worktree
|
|
320
|
+
# and base branch CLEANUP_WT_BASE_BRANCH always protected).
|
|
321
|
+
# 2. ancestry -> MERGED_CLEAN, or ANCESTRY_ERROR (hard fail).
|
|
322
|
+
# 3. content-neutral short-circuit -> MERGED_CONTENT_NEUTRAL.
|
|
323
|
+
# 4. cherry patch-id equivalence -> MERGED_EQUIVALENT when all residuals equiv.
|
|
324
|
+
# 5. rename-aware blob fallback: residuals all CONTENT_ON_MAIN -> MERGED_EQUIVALENT.
|
|
325
|
+
# 6. remaining unique residuals -> HAS_UNIQUE_RESIDUALS when the branch was
|
|
326
|
+
# partially incorporated on main (a cherry `-` line or a CONTENT_ON_MAIN
|
|
327
|
+
# residual), else NOT_MERGED (purely unmerged code).
|
|
328
|
+
#
|
|
329
|
+
# Args: $1 = branch name. Returns 0 normally; 2 on any hard error surfaced by the
|
|
330
|
+
# enumeration/protection reads (worktree-list, protected-set), the cherry rung
|
|
331
|
+
# (CHERRY_ERROR), the empty-residual diff-tree probe (DIFF_TREE_ERROR), the
|
|
332
|
+
# name-status/ls-tree residual reads (RESIDUAL_ERROR), or rev-list candidate
|
|
333
|
+
# selection. A hard git failure always maps to the ANCESTRY_ERROR report state and a
|
|
334
|
+
# non-zero return, never a MERGED verdict.
|
|
335
|
+
local name="$1"
|
|
336
|
+
local -A prot_branch=() prot_path=()
|
|
337
|
+
local pline cpout cprc=0
|
|
338
|
+
# Guarded parent-shell capture of the protected set: a compute_protected hard
|
|
339
|
+
# failure (a git worktree-list failure underneath) must map to ANCESTRY_ERROR, not
|
|
340
|
+
# a weakened/empty protected set that could let a real branch reach a MERGED verdict.
|
|
341
|
+
cpout=$(compute_protected) || cprc=$?
|
|
342
|
+
if ((cprc != 0)); then
|
|
343
|
+
printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
|
|
344
|
+
return 2
|
|
345
|
+
fi
|
|
346
|
+
while IFS= read -r pline; do
|
|
347
|
+
case "$pline" in
|
|
348
|
+
protected-branch\|*) prot_branch[${pline#protected-branch|}]=1 ;;
|
|
349
|
+
protected-path\|*) prot_path[${pline#protected-path|}]=1 ;;
|
|
350
|
+
esac
|
|
351
|
+
done <<<"$cpout"
|
|
352
|
+
# Locate this branch's worktree path (normalized), if any, from a guarded capture
|
|
353
|
+
# of the worktree list; a hard failure here likewise maps to ANCESTRY_ERROR.
|
|
354
|
+
local wt_norm="" record wpath wbranch wlout wlrc=0
|
|
355
|
+
wlout=$(parse_worktree_list) || wlrc=$?
|
|
356
|
+
if ((wlrc != 0)); then
|
|
357
|
+
printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
|
|
358
|
+
return 2
|
|
359
|
+
fi
|
|
360
|
+
while IFS= read -r record; do
|
|
361
|
+
[[ -z $record ]] && continue
|
|
362
|
+
IFS='|' read -r wpath _ wbranch _ <<<"$record"
|
|
363
|
+
if [[ $wbranch == "$name" ]]; then
|
|
364
|
+
wt_norm=$(normalize_wt_path "$wpath")
|
|
365
|
+
break
|
|
366
|
+
fi
|
|
367
|
+
done <<<"$wlout"
|
|
368
|
+
if [[ -n ${prot_branch[$name]:-} ]] || { [[ -n $wt_norm && -n ${prot_path[$wt_norm]:-} ]]; }; then
|
|
369
|
+
printf 'BRANCH|%s|PROTECTED_CURRENT\n' "$name"
|
|
370
|
+
return 0
|
|
371
|
+
fi
|
|
372
|
+
local v
|
|
373
|
+
v=$(classify_ancestry "$name")
|
|
374
|
+
case "$v" in
|
|
375
|
+
MERGED_CLEAN)
|
|
376
|
+
printf 'BRANCH|%s|MERGED_CLEAN\n' "$name"
|
|
377
|
+
return 0
|
|
378
|
+
;;
|
|
379
|
+
ANCESTRY_ERROR)
|
|
380
|
+
printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
|
|
381
|
+
return 2
|
|
382
|
+
;;
|
|
383
|
+
esac
|
|
384
|
+
v=$(classify_content_neutral "$name")
|
|
385
|
+
case "$v" in
|
|
386
|
+
MERGED_CONTENT_NEUTRAL)
|
|
387
|
+
printf 'BRANCH|%s|MERGED_CONTENT_NEUTRAL\n' "$name"
|
|
388
|
+
return 0
|
|
389
|
+
;;
|
|
390
|
+
CONTENT_NEUTRAL_ERROR)
|
|
391
|
+
printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
|
|
392
|
+
return 2
|
|
393
|
+
;;
|
|
394
|
+
esac
|
|
395
|
+
local ce
|
|
396
|
+
ce=$(classify_cherry_equivalent "$name")
|
|
397
|
+
if [[ $ce == "CHERRY_ERROR" || $ce == "DIFF_TREE_ERROR" ]]; then
|
|
398
|
+
# Hard `git cherry` or `git diff-tree` failure: same mapping as
|
|
399
|
+
# CONTENT_NEUTRAL_ERROR (ANCESTRY_ERROR report state, hard-fail return 2).
|
|
400
|
+
printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
|
|
401
|
+
return 2
|
|
402
|
+
fi
|
|
403
|
+
if [[ $ce == "MERGED_EQUIVALENT" ]]; then
|
|
404
|
+
printf 'BRANCH|%s|MERGED_EQUIVALENT\n' "$name"
|
|
405
|
+
return 0
|
|
406
|
+
fi
|
|
407
|
+
# Residual `+` commits exist; resolve each via the blob tier.
|
|
408
|
+
local sha verdict unique_count=0 content_count=0
|
|
409
|
+
while IFS= read -r pline; do
|
|
410
|
+
[[ $pline == RESIDUAL\ * ]] || continue
|
|
411
|
+
sha=${pline#RESIDUAL }
|
|
412
|
+
verdict=$(classify_residual_commit "$name" "$sha")
|
|
413
|
+
if [[ $verdict == "RESIDUAL_ERROR" ]]; then
|
|
414
|
+
# Hard failure of the name-status diff-tree read or the D-rung ls-tree probe:
|
|
415
|
+
# map to ANCESTRY_ERROR before any state line is emitted.
|
|
416
|
+
printf 'BRANCH|%s|ANCESTRY_ERROR\n' "$name"
|
|
417
|
+
return 2
|
|
418
|
+
fi
|
|
419
|
+
if [[ $verdict == UNIQUE\|* ]]; then
|
|
420
|
+
unique_count=$((unique_count + 1))
|
|
421
|
+
elif [[ $verdict == "CONTENT_ON_MAIN" ]]; then
|
|
422
|
+
content_count=$((content_count + 1))
|
|
423
|
+
fi
|
|
424
|
+
done < <(printf '%s\n' "$ce")
|
|
425
|
+
if ((unique_count == 0)); then
|
|
426
|
+
# Every residual was content-on-main; the branch is content-equivalent-merged.
|
|
427
|
+
printf 'BRANCH|%s|MERGED_EQUIVALENT\n' "$name"
|
|
428
|
+
return 0
|
|
429
|
+
fi
|
|
430
|
+
# Partial-merge signal: was any part of this branch already incorporated on main?
|
|
431
|
+
# Derived from the already-captured cherry verdict (MINUS_PRESENT token) instead of a
|
|
432
|
+
# second `git cherry` invocation whose exit code would be discarded in a pipeline.
|
|
433
|
+
local minus_present=0
|
|
434
|
+
[[ $ce == *MINUS_PRESENT* ]] && minus_present=1
|
|
435
|
+
local state="NOT_MERGED"
|
|
436
|
+
if ((minus_present == 1)) || ((content_count > 0)); then
|
|
437
|
+
state="HAS_UNIQUE_RESIDUALS"
|
|
438
|
+
fi
|
|
439
|
+
printf 'BRANCH|%s|%s\n' "$name" "$state"
|
|
440
|
+
if [[ $state == "HAS_UNIQUE_RESIDUALS" ]]; then
|
|
441
|
+
# Propagate a rev-list/cherry hard failure: the BRANCH line already names a
|
|
442
|
+
# non-delete-eligible state, and the non-zero return signals the hard error.
|
|
443
|
+
local scrc=0
|
|
444
|
+
select_cherry_pick_candidates "$name" || scrc=$?
|
|
445
|
+
if ((scrc != 0)); then
|
|
446
|
+
return 2
|
|
447
|
+
fi
|
|
448
|
+
fi
|
|
449
|
+
return 0
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
run_report() {
|
|
453
|
+
# Report-mode driver: emit the deterministic report with no mutation of any kind.
|
|
454
|
+
# Emission order: WARN (freshness), then the pre-branch advisory scans via the single
|
|
455
|
+
# run_report_scans call, which emits STALE_REF, ORPHAN_DIR, and WARN|registration-lost
|
|
456
|
+
# in that order from ONE filesystem scan (none depends on per-branch classification,
|
|
457
|
+
# so all three are colocated with the freshness warning), then WORKTREE registrations,
|
|
458
|
+
# then the per-branch BRANCH/CHILD_OF/COMMIT lines in enumerate_branches' LC_ALL=C
|
|
459
|
+
# order. Those come from classify_all_branches, the shared driver apply mode also
|
|
460
|
+
# uses, so both modes derive one identical classification. Returns the maximum return
|
|
461
|
+
# code observed. A worktree-list OR enumerate-branches hard failure aborts the report
|
|
462
|
+
# before any line is emitted and returns git's non-zero exit code, so a git failure
|
|
463
|
+
# never resolves to a partial, misleading report.
|
|
464
|
+
local rc=0 crc record wpath wbranch wflags wlout wlrc=0 ebrc=0
|
|
465
|
+
# Guarded parent-shell captures up front, before any output: a hard failure of either
|
|
466
|
+
# read aborts the report before any WARN/WORKTREE/BRANCH line. The branch list itself
|
|
467
|
+
# is re-read by classify_all_branches; this probe exists only for the early abort.
|
|
468
|
+
wlout=$(parse_worktree_list) || wlrc=$?
|
|
469
|
+
if ((wlrc != 0)); then
|
|
470
|
+
return "$wlrc"
|
|
471
|
+
fi
|
|
472
|
+
enumerate_branches >/dev/null || ebrc=$?
|
|
473
|
+
if ((ebrc != 0)); then
|
|
474
|
+
return "$ebrc"
|
|
475
|
+
fi
|
|
476
|
+
check_main_freshness
|
|
477
|
+
run_report_scans || rc=$?
|
|
478
|
+
while IFS= read -r record; do
|
|
479
|
+
[[ -z $record ]] && continue
|
|
480
|
+
IFS='|' read -r wpath _ wbranch wflags <<<"$record"
|
|
481
|
+
# A detached registration is emitted by report_detached_worktrees instead.
|
|
482
|
+
is_detached_candidate "$wflags" && continue
|
|
483
|
+
printf 'WORKTREE|%s|%s|%s\n' "$wpath" "$wbranch" "$wflags"
|
|
484
|
+
# Dirt verdicts for a candidate registration only; main and bare are never candidates.
|
|
485
|
+
if [[ ,$wflags, != *,main,* && ,$wflags, != *,bare,* ]]; then
|
|
486
|
+
classify_worktree_dirt "$wpath" || rc=$?
|
|
487
|
+
fi
|
|
488
|
+
done <<<"$wlout"
|
|
489
|
+
report_detached_worktrees "$wlout" || rc=$?
|
|
490
|
+
crc=0
|
|
491
|
+
classify_all_branches || crc=$?
|
|
492
|
+
if ((crc > rc)); then
|
|
493
|
+
rc=$crc
|
|
494
|
+
fi
|
|
495
|
+
return "$rc"
|
|
496
|
+
}
|