@danmoisan/drm-copilot-mcp 1.1.14 → 1.1.15
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/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
- 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/orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/pack-manifests/core.json +11 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +5 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cleanup_worktrees_dirt_lib.sh: sourceable dirt-classification function group for the
|
|
3
|
+
# cleanup-worktrees tool. Provides the bounded staged-tree probe
|
|
4
|
+
# (dirt_staged_tree_commit), the per-entry precedence ladder (classify_dirt_entry), the
|
|
5
|
+
# per-worktree driver that emits the DIRTFILE|/DIRTSUM| records
|
|
6
|
+
# (classify_worktree_dirt), and the opt-in clearing sequence (clear_disposable_dirt).
|
|
7
|
+
#
|
|
8
|
+
# Sourcing contract: this library defines functions and constants only; it never runs
|
|
9
|
+
# work at source time, so the wrapper and the bats suites can source it without side
|
|
10
|
+
# effects. It depends on cleanup_wt_git from cleanup_worktrees_enumerate_lib.sh, which
|
|
11
|
+
# MUST be sourced before this file. All git commands go through cleanup_wt_git so tests
|
|
12
|
+
# can stub the git binary via CLEANUP_WT_GIT_BIN.
|
|
13
|
+
#
|
|
14
|
+
# READ-ONLY GUARANTEE. Classification performs no index write and no object-database
|
|
15
|
+
# write. It reaches its staged-tree answer by comparing the existing index against
|
|
16
|
+
# candidate commits with `diff-index --cached --quiet`, never by materialising a tree,
|
|
17
|
+
# and it never redirects the index to an alternate file through the environment. Report
|
|
18
|
+
# mode is therefore non-mutating, which is the property
|
|
19
|
+
# tests/shell/test_cleanup_worktrees_dirt_clear.bats asserts by searching the stub's
|
|
20
|
+
# argv and environment log. Do not add a probe that writes.
|
|
21
|
+
#
|
|
22
|
+
# ONE-SIDED BOUND ASYMMETRY. Every bound and every match in this file fails in the safe
|
|
23
|
+
# direction. A missed match degrades an entry to UNIQUE, which makes the worktree
|
|
24
|
+
# HAS_UNIQUE and REFUSES the clear; the worktree is then left in place for manual
|
|
25
|
+
# triage, which is the pre-existing behavior. A spurious match would do the opposite and
|
|
26
|
+
# clear content that exists nowhere else. Every non-zero exit that carries no defined
|
|
27
|
+
# negative answer is therefore mapped to UNIQUE rather than advanced past.
|
|
28
|
+
#
|
|
29
|
+
# NO --ignored ON THE STATUS READ. The status read must not be given --ignored. Ignored
|
|
30
|
+
# files are never classified, so they are never cleared, and `clean -fd` without -x
|
|
31
|
+
# leaves them in place. Adding --ignored would bring build output and local tooling
|
|
32
|
+
# state into the classified set and, for any entry that then matched a disposable rung,
|
|
33
|
+
# into the cleared set. Do not add it.
|
|
34
|
+
#
|
|
35
|
+
# Git exit-code capture rule, following the sibling libraries: every git-backed read
|
|
36
|
+
# whose output or exit code is authoritative is captured in the PARENT shell
|
|
37
|
+
# (`out=$(cmd) || rc=$?`), never inside a pipeline or a process substitution, so a
|
|
38
|
+
# non-zero exit is observed rather than lost.
|
|
39
|
+
#
|
|
40
|
+
# TWO-WAY CLASSIFICATION OF NON-ZERO EXITS. A probe whose non-zero exit is its DEFINED
|
|
41
|
+
# NEGATIVE ANSWER advances the ladder: `rev-parse main:<path>` exiting non-zero means
|
|
42
|
+
# the path is absent from main, and `diff --quiet main -- <path>` exiting 1 means the
|
|
43
|
+
# contents differ. Both are rung-4 misses. A probe whose non-zero exit carries NO
|
|
44
|
+
# verdict is a hard read failure and maps the entry to UNIQUE: `status --porcelain`,
|
|
45
|
+
# `hash-object`, `log --find-object`, and `diff-index --cached --quiet` exiting above 1.
|
|
46
|
+
# Collapsing the two into one blanket rule would make CONTENT_IN_HISTORY unreachable.
|
|
47
|
+
#
|
|
48
|
+
# EMPTY PATHSPEC IS NOT A MATCH. `diff --quiet main -- <path>` exits 0 for two distinct
|
|
49
|
+
# reasons: the compared content is identical, and the pathspec selected no paths at all.
|
|
50
|
+
# Only the first supports a disposable verdict, so rung 4's tracked positive answer is
|
|
51
|
+
# conditional. The reachable case is an AD entry, added to the index and then removed
|
|
52
|
+
# from the working tree, whose content exists only as a staged blob; reading its exit 0
|
|
53
|
+
# as a match would clear content held nowhere else. The answer is therefore gated on
|
|
54
|
+
# `rev-parse --verify --quiet main:<path>`, which asks whether main has content at that
|
|
55
|
+
# path to be equal to. A non-zero answer emits nothing and advances the ladder to the
|
|
56
|
+
# hash-object guard. This is the vacuous confinement rung 3 already guards against.
|
|
57
|
+
#
|
|
58
|
+
# INDEX AND WORKING TREE ARE TWO LOCATIONS. M, A, R, C, T, U are content-bearing: each
|
|
59
|
+
# says that side holds a blob (T is a typechange, U holds content at index stages 2 and
|
|
60
|
+
# 3); a space, ?, ! or D does not. Two content-bearing columns mean TWO distinct blobs,
|
|
61
|
+
# and rung 4's tracked half and rung 5 read only the working-tree one, so both fail
|
|
62
|
+
# closed there. Rung 3 is not gated: dirt_is_build_artifact reads the cached diff too.
|
|
63
|
+
#
|
|
64
|
+
# Record contract (pipe-delimited, one record per line; the file path is LAST so a path
|
|
65
|
+
# containing the delimiter is still recoverable):
|
|
66
|
+
# DIRTFILE|<worktree-path>|<verdict>|<detail>|<status-code>|<file-path>
|
|
67
|
+
# DIRTSUM|<worktree-path>|ALL_DISPOSABLE|HAS_UNIQUE|<detail>
|
|
68
|
+
# <verdict> is one of DISPOSABLE_BUILD_ARTIFACT, DISPOSABLE_SESSION_ARTIFACT,
|
|
69
|
+
# CONTENT_ON_MAIN, CONTENT_IN_HISTORY, STAGED_TREE_IS_COMMIT, UNIQUE. <detail> carries a
|
|
70
|
+
# commit sha for STAGED_TREE_IS_COMMIT and CONTENT_IN_HISTORY and is empty otherwise.
|
|
71
|
+
# ACTION|dirt-clear|<worktree-path>|OK|REFUSED-UNIQUE|FAILED
|
|
72
|
+
|
|
73
|
+
# Opt-in gate for the destructive clearing path. Defaulted through parameter expansion
|
|
74
|
+
# so the bats suites can drive these functions without the CLI wrapper.
|
|
75
|
+
CLEANUP_WT_CLEAR_DISPOSABLE=${CLEANUP_WT_CLEAR_DISPOSABLE:-0}
|
|
76
|
+
|
|
77
|
+
# Upper bound on how far back the staged-tree probe walks from HEAD. One worktree in the
|
|
78
|
+
# 2026-09-06 run carried 86 staged changes that were exactly an earlier commit.
|
|
79
|
+
CLEANUP_WT_STAGED_TREE_DEPTH=200
|
|
80
|
+
|
|
81
|
+
# Upper bound on the history window scanned for a blob. Exceeding the available history
|
|
82
|
+
# is not an error: the probe falls back to the full `main` ref.
|
|
83
|
+
CLEANUP_WT_HISTORY_SCAN_DEPTH=1000
|
|
84
|
+
|
|
85
|
+
# The authorization list for an irreversible action, hard-coded with no environment
|
|
86
|
+
# override, following the CLEANUP_WT_CONSOLIDATION_BRANCH precedent in
|
|
87
|
+
# cleanup_worktrees_actions_lib.sh. A runtime override would let any path be declared
|
|
88
|
+
# disposable and would defeat the UNIQUE gate this feature's safety rests on. Matching
|
|
89
|
+
# is EXACT and never by prefix: artifacts/** at large must not be auto-cleared.
|
|
90
|
+
CLEANUP_WT_SESSION_ARTIFACT_PATHS=(
|
|
91
|
+
"artifacts/pr_context.summary.txt"
|
|
92
|
+
"artifacts/pr_context.appendix.txt"
|
|
93
|
+
"artifacts/orchestration/orchestrator-state.json"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
dirt_staged_tree_commit() {
|
|
97
|
+
# Bounded staged-tree probe: is the current index exactly the tree of some recent
|
|
98
|
+
# commit on this worktree's history?
|
|
99
|
+
#
|
|
100
|
+
# Walks CLEANUP_WT_STAGED_TREE_DEPTH commits back from HEAD and compares the index
|
|
101
|
+
# against each with `diff-index --cached --quiet`. The bound is passed as depth plus
|
|
102
|
+
# one because the FIRST line rev-list returns is HEAD itself, which is dropped: HEAD's
|
|
103
|
+
# tree is what an unmodified index already equals, so probing it would match for every
|
|
104
|
+
# worktree and label every staged entry disposable.
|
|
105
|
+
#
|
|
106
|
+
# Args: $1 = worktree path.
|
|
107
|
+
# Echoes the matching commit sha and returns 0 on a match; returns 1 with no output
|
|
108
|
+
# when no candidate matched; returns 2 on a hard read failure (a rev-list failure, or
|
|
109
|
+
# a diff-index exit above 1, neither of which carries a verdict).
|
|
110
|
+
local wt="$1" out rc=0 first=1 sha drc
|
|
111
|
+
out=$(cleanup_wt_git --no-optional-locks -C "$wt" rev-list \
|
|
112
|
+
--max-count=$((CLEANUP_WT_STAGED_TREE_DEPTH + 1)) HEAD) || rc=$?
|
|
113
|
+
if ((rc != 0)); then # guard:staged-probe-revlist-hard-fail
|
|
114
|
+
return 2
|
|
115
|
+
fi
|
|
116
|
+
while IFS= read -r sha || [[ -n $sha ]]; do
|
|
117
|
+
[[ -z $sha ]] && continue
|
|
118
|
+
if ((first == 1)); then # guard:staged-probe-skip-head
|
|
119
|
+
first=0
|
|
120
|
+
continue
|
|
121
|
+
fi
|
|
122
|
+
drc=0
|
|
123
|
+
cleanup_wt_git --no-optional-locks -C "$wt" diff-index --cached --quiet \
|
|
124
|
+
"$sha" -- >/dev/null || drc=$?
|
|
125
|
+
if ((drc == 0)); then # guard:staged-probe-tree-match
|
|
126
|
+
printf '%s\n' "$sha"
|
|
127
|
+
return 0
|
|
128
|
+
fi
|
|
129
|
+
if ((drc > 1)); then # guard:staged-probe-diffindex-hard-fail
|
|
130
|
+
return 2
|
|
131
|
+
fi
|
|
132
|
+
done <<<"$out"
|
|
133
|
+
return 1
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
dirt_is_session_artifact() {
|
|
137
|
+
# Rung 2. Pure string comparison against the hard-coded array; no git call, so no
|
|
138
|
+
# invocation ever names a session-artifact path.
|
|
139
|
+
#
|
|
140
|
+
# Args: $1 = repo-relative path. Returns 0 on an exact match, 1 otherwise.
|
|
141
|
+
local rel="$1" p
|
|
142
|
+
for p in "${CLEANUP_WT_SESSION_ARTIFACT_PATHS[@]}"; do
|
|
143
|
+
[[ $rel == "$p" ]] && return 0
|
|
144
|
+
done
|
|
145
|
+
return 1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
dirt_diff_is_hintpath_confined() {
|
|
149
|
+
# Read one diff and decide whether every changed content line in it is an analyzer
|
|
150
|
+
# HintPath rewrite.
|
|
151
|
+
#
|
|
152
|
+
# The `---` and `+++` file headers are excluded before the test: both begin with a
|
|
153
|
+
# changed-line marker and neither is content, so counting them would make every diff
|
|
154
|
+
# unconfined. Hunk headers and the `diff --git`/`index` lines are not changed lines
|
|
155
|
+
# and are ignored.
|
|
156
|
+
#
|
|
157
|
+
# The header skip is ANCHORED to the four forms the diff emits, not written as a
|
|
158
|
+
# bare `+++ `/`--- ` prefix test. A space does not end a diff line's content, so an
|
|
159
|
+
# ADDED line whose own text begins `+++ ` matches a bare prefix test and is dropped
|
|
160
|
+
# before it is counted and before it is tested for HintPath. A project file carrying
|
|
161
|
+
# one such hand edit then reads as confined and resolves DISPOSABLE_BUILD_ARTIFACT,
|
|
162
|
+
# which is the fail-open direction and makes the entry clearable.
|
|
163
|
+
#
|
|
164
|
+
# The `a/` and `b/` prefixes are assumed because this library issues
|
|
165
|
+
# `diff --no-color -U0` without `--no-prefix`, so the default prefixes are always
|
|
166
|
+
# present; the two `/dev/null` forms cover an added or a deleted file. A header form
|
|
167
|
+
# the pattern does not match is counted as a changed content line, which fails the
|
|
168
|
+
# HintPath test and resolves the entry UNIQUE — the safe direction.
|
|
169
|
+
#
|
|
170
|
+
# Args: $1 = worktree path, $2 = repo-relative path, $3 = "cached" for the staged
|
|
171
|
+
# diff or the empty string for the worktree diff.
|
|
172
|
+
# Echoes the number of changed content lines seen and returns 0 when every one of
|
|
173
|
+
# them is a HintPath line; returns 1 when any is not; returns 2 on a hard read
|
|
174
|
+
# failure.
|
|
175
|
+
local wt="$1" rel="$2" mode="${3:-}" out rc=0 line changed=0
|
|
176
|
+
if [[ $mode == "cached" ]]; then
|
|
177
|
+
out=$(cleanup_wt_git --no-optional-locks -C "$wt" diff --no-color -U0 \
|
|
178
|
+
--cached -- "$rel") || rc=$?
|
|
179
|
+
else
|
|
180
|
+
out=$(cleanup_wt_git --no-optional-locks -C "$wt" diff --no-color -U0 \
|
|
181
|
+
-- "$rel") || rc=$?
|
|
182
|
+
fi
|
|
183
|
+
if ((rc != 0)); then # guard:hintpath-diff-read-hard-fail
|
|
184
|
+
return 2
|
|
185
|
+
fi
|
|
186
|
+
while IFS= read -r line || [[ -n $line ]]; do
|
|
187
|
+
case "$line" in
|
|
188
|
+
"--- a/"* | "+++ b/"* | "--- /dev/null" | "+++ /dev/null") continue ;; # guard:diff-header-skip
|
|
189
|
+
"+"* | "-"*) ;;
|
|
190
|
+
*) continue ;;
|
|
191
|
+
esac
|
|
192
|
+
changed=$((changed + 1))
|
|
193
|
+
if [[ $line != *"HintPath"* ]]; then
|
|
194
|
+
printf '%s\n' "$changed"
|
|
195
|
+
return 1
|
|
196
|
+
fi
|
|
197
|
+
done <<<"$out"
|
|
198
|
+
printf '%s\n' "$changed"
|
|
199
|
+
return 0
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
dirt_is_build_artifact() {
|
|
203
|
+
# Rung 3. A tracked, modified project file whose every changed content line, in BOTH
|
|
204
|
+
# the worktree diff and the cached diff, is an analyzer HintPath rewrite.
|
|
205
|
+
#
|
|
206
|
+
# The rule is deliberately NOT path-pattern-only. In a legacy non-SDK C# project every
|
|
207
|
+
# added source file is an explicit <Compile Include=...> edit to the project file, and
|
|
208
|
+
# a path-only rule would label that hand edit disposable and destroy it. The content
|
|
209
|
+
# confinement is the definition of the class, not extra scope.
|
|
210
|
+
#
|
|
211
|
+
# A diff pair with zero changed content lines is NOT a build artifact. Vacuous
|
|
212
|
+
# confinement would let a read that returned nothing resolve to a disposable verdict,
|
|
213
|
+
# which is the fail-open direction.
|
|
214
|
+
#
|
|
215
|
+
# Args: $1 = worktree path, $2 = repo-relative path.
|
|
216
|
+
# Returns 0 for a build artifact, 1 for not one, 2 on a hard read failure.
|
|
217
|
+
local wt="$1" rel="$2" wout crc=0 wrc=0 cout total=0
|
|
218
|
+
case "$rel" in
|
|
219
|
+
*.csproj | packages.config | */packages.config | app.config | */app.config) ;;
|
|
220
|
+
*) return 1 ;;
|
|
221
|
+
esac
|
|
222
|
+
wout=$(dirt_diff_is_hintpath_confined "$wt" "$rel" "") || wrc=$?
|
|
223
|
+
((wrc == 2)) && return 2 # guard:build-artifact-worktree-diff-hard-fail
|
|
224
|
+
((wrc == 1)) && return 1 # guard:build-artifact-worktree-diff-unconfined
|
|
225
|
+
cout=$(dirt_diff_is_hintpath_confined "$wt" "$rel" "cached") || crc=$?
|
|
226
|
+
((crc == 2)) && return 2 # guard:build-artifact-cached-diff-hard-fail
|
|
227
|
+
((crc == 1)) && return 1 # guard:build-artifact-cached-diff-unconfined
|
|
228
|
+
total=$((wout + cout))
|
|
229
|
+
((total == 0)) && return 1 # guard:build-artifact-vacuous-confinement
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
classify_dirt_entry() {
|
|
234
|
+
# The six-rung, first-match-wins precedence ladder for one status entry.
|
|
235
|
+
#
|
|
236
|
+
# Rung order: staged-tree, session artifact, build artifact, content-on-main,
|
|
237
|
+
# content-in-history, unique. Rung 1 is answered from the once-per-worktree probe
|
|
238
|
+
# result passed in by the caller rather than re-probed here, so the probe is issued at
|
|
239
|
+
# most once per worktree however many entries it has.
|
|
240
|
+
#
|
|
241
|
+
# Args: $1 = worktree path, $2 = two-character status code, $3 = repo-relative path,
|
|
242
|
+
# $4 = the staged-tree probe result: a commit sha on a match, the empty string on no
|
|
243
|
+
# match, or the literal ERROR on a hard probe failure.
|
|
244
|
+
# Echoes exactly one `<verdict>|<detail>` line and always returns 0.
|
|
245
|
+
local wt="$1" xy="$2" rel="$3" staged="$4"
|
|
246
|
+
local x="${xy:0:1}" y="${xy:1:1}" untracked=0 blob="" hrc=0 mrc=0 mainblob="" brc=0
|
|
247
|
+
local drc=0 erc=0 vrc=0 lrc=0 bothloc=0 range found
|
|
248
|
+
|
|
249
|
+
# A C-quoted payload is returned UNIQUE immediately with no unquoting attempt. One
|
|
250
|
+
# branch, deterministic, and failing in the safe direction; attempting to unquote
|
|
251
|
+
# would add an escape-decoding surface whose only use is to widen what can be
|
|
252
|
+
# cleared.
|
|
253
|
+
if [[ $rel == \"* ]]; then
|
|
254
|
+
printf 'UNIQUE|\n'
|
|
255
|
+
return 0
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# Rung 1. Staged entries only: the X column is the index status, and a space,
|
|
259
|
+
# question mark, or exclamation mark there means the entry is not staged.
|
|
260
|
+
#
|
|
261
|
+
# THE Y COLUMN GATE IS LOAD-BEARING. The X column answers a question about the
|
|
262
|
+
# INDEX, and the probe that answers rung 1 is `diff-index --cached --quiet`, which
|
|
263
|
+
# compares the index against a candidate commit and says nothing whatever about a
|
|
264
|
+
# non-space Y column. An MM entry has a staged change that matches an ancestor tree
|
|
265
|
+
# AND an unstaged working-tree modification on top of it; that unstaged delta exists
|
|
266
|
+
# in no commit and not in the index, so nothing recovers it once
|
|
267
|
+
# --clear-disposable runs reset --hard. Rung 1 therefore fires only when the Y
|
|
268
|
+
# column is a single space.
|
|
269
|
+
#
|
|
270
|
+
# An entry with a non-space Y column falls through to the rungs that compare
|
|
271
|
+
# WORKING-TREE content, which is the comparison its unstaged delta actually needs.
|
|
272
|
+
# See INDEX AND WORKING TREE ARE TWO LOCATIONS in the header: when the Y column is
|
|
273
|
+
# itself content-bearing that comparison alone is not enough and rungs 4 and 5 fail
|
|
274
|
+
# closed. An MM entry on a HintPath-confined project file can still reach
|
|
275
|
+
# DISPOSABLE_BUILD_ARTIFACT, because dirt_is_build_artifact reads both the worktree
|
|
276
|
+
# diff and the cached diff; an MM entry on any other path reaches rung 6 and is UNIQUE.
|
|
277
|
+
if [[ $x != " " && $x != "?" && $x != "!" && $y == " " ]]; then # guard:rung1-y-column-gate
|
|
278
|
+
if [[ $staged == "ERROR" ]]; then
|
|
279
|
+
printf 'UNIQUE|\n'
|
|
280
|
+
return 0
|
|
281
|
+
fi
|
|
282
|
+
if [[ -n $staged ]]; then
|
|
283
|
+
printf 'STAGED_TREE_IS_COMMIT|%s\n' "$staged"
|
|
284
|
+
return 0
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
# Rung 2.
|
|
289
|
+
if dirt_is_session_artifact "$rel"; then
|
|
290
|
+
printf 'DISPOSABLE_SESSION_ARTIFACT|\n'
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
[[ $xy == "??" ]] && untracked=1
|
|
295
|
+
# See INDEX AND WORKING TREE ARE TWO LOCATIONS in the header: two content-bearing
|
|
296
|
+
# columns mean the index blob and the working-tree blob hold different content.
|
|
297
|
+
[[ $x == [MARCTU] && $y == [MARCTU] ]] && bothloc=1 # guard:index-and-worktree-both-hold-content
|
|
298
|
+
|
|
299
|
+
# Rung 3. Tracked, modified entries only; an untracked file has no diff to confine.
|
|
300
|
+
if ((untracked == 0)); then # guard:rung3-tracked-only-gate
|
|
301
|
+
dirt_is_build_artifact "$wt" "$rel" || brc=$?
|
|
302
|
+
if ((brc == 0)); then # guard:rung3-build-artifact-match
|
|
303
|
+
printf 'DISPOSABLE_BUILD_ARTIFACT|\n'
|
|
304
|
+
return 0
|
|
305
|
+
fi
|
|
306
|
+
if ((brc > 1)); then # guard:rung3-build-artifact-hard-fail
|
|
307
|
+
printf 'UNIQUE|\n'
|
|
308
|
+
return 0
|
|
309
|
+
fi
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
# Rung 4, tracked half. Exit 1 is this probe's defined negative answer (the contents
|
|
313
|
+
# differ) and advances the ladder; an exit above 1 carries no verdict.
|
|
314
|
+
if ((untracked == 0)); then # guard:rung4-tracked-gate
|
|
315
|
+
cleanup_wt_git --no-optional-locks -C "$wt" diff --quiet main -- "$rel" \
|
|
316
|
+
>/dev/null || drc=$?
|
|
317
|
+
if ((drc == 0)); then # guard:rung4-tracked-content-equal
|
|
318
|
+
# See EMPTY PATHSPEC IS NOT A MATCH in the header. Only stdout is
|
|
319
|
+
# discarded: the stub logs its argv to stderr, and redirecting that
|
|
320
|
+
# stream too would hide this read from the argv assertions. --quiet
|
|
321
|
+
# already suppresses git's own diagnostic on the negative answer.
|
|
322
|
+
cleanup_wt_git --no-optional-locks -C "$wt" rev-parse --verify --quiet \
|
|
323
|
+
"main:$rel" >/dev/null || erc=$?
|
|
324
|
+
if ((erc == 0)); then # guard:rung4-tracked-path-in-main
|
|
325
|
+
if ((bothloc == 0)); then # guard:rung4-index-blob-unaccounted
|
|
326
|
+
printf 'CONTENT_ON_MAIN|\n'
|
|
327
|
+
return 0
|
|
328
|
+
fi
|
|
329
|
+
fi
|
|
330
|
+
fi
|
|
331
|
+
if ((drc > 1)); then # guard:rung4-tracked-hard-fail
|
|
332
|
+
printf 'UNIQUE|\n'
|
|
333
|
+
return 0
|
|
334
|
+
fi
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
# The working-tree blob id, needed by rung 4's untracked half and by rung 5. A
|
|
338
|
+
# hash-object failure carries no verdict, so it fails closed.
|
|
339
|
+
blob=$(cleanup_wt_git --no-optional-locks -C "$wt" hash-object -- "$rel") || hrc=$?
|
|
340
|
+
blob=${blob%%$'\n'*}
|
|
341
|
+
if ((hrc != 0)) || [[ -z $blob ]]; then # guard:hash-object-hard-fail
|
|
342
|
+
printf 'UNIQUE|\n'
|
|
343
|
+
return 0
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# Rung 4, untracked half. A rev-parse failure means the path is absent from main,
|
|
347
|
+
# which is this probe's defined negative answer, so the ladder continues.
|
|
348
|
+
if ((untracked == 1)); then # guard:rung4-untracked-gate
|
|
349
|
+
mainblob=$(cleanup_wt_git --no-optional-locks -C "$wt" rev-parse "main:$rel") || mrc=$?
|
|
350
|
+
mainblob=${mainblob%%$'\n'*}
|
|
351
|
+
if ((mrc == 0)) && [[ -n $mainblob && $mainblob == "$blob" ]]; then # guard:rung4-untracked-main-present
|
|
352
|
+
printf 'CONTENT_ON_MAIN|\n'
|
|
353
|
+
return 0
|
|
354
|
+
fi
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# Rung 5. The bounded range endpoint is probed first; a repository with less history
|
|
358
|
+
# than the bound cannot resolve it, and that is not an error, so the scan falls back
|
|
359
|
+
# to the full ref rather than reporting a read failure.
|
|
360
|
+
range="main"
|
|
361
|
+
cleanup_wt_git --no-optional-locks -C "$wt" rev-parse --verify --quiet \
|
|
362
|
+
"main~$CLEANUP_WT_HISTORY_SCAN_DEPTH" >/dev/null 2>&1 || vrc=$?
|
|
363
|
+
((vrc == 0)) && range="main~$CLEANUP_WT_HISTORY_SCAN_DEPTH..main" # guard:history-scan-bounded-range
|
|
364
|
+
found=$(cleanup_wt_git --no-optional-locks -C "$wt" log "$range" \
|
|
365
|
+
--find-object="$blob" --format=%H --max-count=1) || lrc=$?
|
|
366
|
+
if ((lrc != 0)); then # guard:find-object-hard-fail
|
|
367
|
+
printf 'UNIQUE|\n'
|
|
368
|
+
return 0
|
|
369
|
+
fi
|
|
370
|
+
found=${found%%$'\n'*}
|
|
371
|
+
if [[ -n $found ]]; then # guard:history-hit-nonempty
|
|
372
|
+
if ((bothloc == 0)); then # guard:rung5-index-blob-unaccounted
|
|
373
|
+
printf 'CONTENT_IN_HISTORY|%s\n' "$found"
|
|
374
|
+
return 0
|
|
375
|
+
fi
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
# Rung 6.
|
|
379
|
+
printf 'UNIQUE|\n'
|
|
380
|
+
return 0
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
classify_worktree_dirt() {
|
|
384
|
+
# Per-worktree driver: one guarded status read, one DIRTFILE record per entry in
|
|
385
|
+
# porcelain order, then exactly one DIRTSUM aggregate.
|
|
386
|
+
#
|
|
387
|
+
# A worktree with zero status entries emits NEITHER record: there is nothing to
|
|
388
|
+
# classify and an aggregate over an empty set would assert a property of no data.
|
|
389
|
+
# A hard status read emits no record at all and returns git's non-zero exit code, so
|
|
390
|
+
# a read failure can never be mistaken for a clean worktree.
|
|
391
|
+
#
|
|
392
|
+
# The aggregate is ALL_DISPOSABLE if and only if the entry count is at least one and
|
|
393
|
+
# the UNIQUE count is zero, and HAS_UNIQUE otherwise. Its detail field carries the
|
|
394
|
+
# first non-empty per-entry detail, which is the staged-tree commit sha when the
|
|
395
|
+
# worktree's dirt is a staged tree.
|
|
396
|
+
#
|
|
397
|
+
# Args: $1 = worktree path. Returns 0 normally; git's exit code on a hard status
|
|
398
|
+
# read failure.
|
|
399
|
+
local wt="$1" out srrc=0 line xy rel res verdict detail
|
|
400
|
+
local staged="" any_staged=0 prc=0 x
|
|
401
|
+
local entry_count=0 unique_count=0 agg_detail=""
|
|
402
|
+
out=$(cleanup_wt_git --no-optional-locks -C "$wt" status --porcelain) || srrc=$?
|
|
403
|
+
if ((srrc != 0)); then # guard:status-read-hard-fail
|
|
404
|
+
return "$srrc"
|
|
405
|
+
fi
|
|
406
|
+
[[ -z $out ]] && return 0
|
|
407
|
+
# The staged-tree probe is issued only when at least one entry is actually staged, so
|
|
408
|
+
# a worktree carrying only unstaged or untracked dirt costs no rev-list walk.
|
|
409
|
+
while IFS= read -r line || [[ -n $line ]]; do
|
|
410
|
+
[[ -z $line ]] && continue
|
|
411
|
+
x="${line:0:1}"
|
|
412
|
+
if [[ $x != " " && $x != "?" && $x != "!" ]]; then
|
|
413
|
+
any_staged=1
|
|
414
|
+
break
|
|
415
|
+
fi
|
|
416
|
+
done <<<"$out"
|
|
417
|
+
if ((any_staged == 1)); then # guard:staged-probe-issue-gate
|
|
418
|
+
staged=$(dirt_staged_tree_commit "$wt") || prc=$?
|
|
419
|
+
((prc == 1)) && staged="" # guard:staged-probe-no-match
|
|
420
|
+
((prc > 1)) && staged="ERROR" # guard:staged-probe-error-map
|
|
421
|
+
fi
|
|
422
|
+
while IFS= read -r line || [[ -n $line ]]; do
|
|
423
|
+
[[ -z $line ]] && continue
|
|
424
|
+
xy="${line:0:2}"
|
|
425
|
+
rel="${line:3}"
|
|
426
|
+
# A rename or copy entry's payload is `OLD -> NEW`; the destination is the path
|
|
427
|
+
# that exists in the working tree and is therefore the one classified. Porcelain
|
|
428
|
+
# status uses that payload form ONLY for the two codes R and C, and a space does
|
|
429
|
+
# not trigger C-quoting, so an ordinary path may contain the literal ` -> `.
|
|
430
|
+
# Splitting unconditionally truncated such a path, issued every probe against a
|
|
431
|
+
# file that does not exist, and emitted a record naming the wrong file. The X
|
|
432
|
+
# column is therefore the gate. The variable read here is `xy`, not the enclosing
|
|
433
|
+
# function's `x`: `x` holds the last value the any_staged pre-scan loop assigned,
|
|
434
|
+
# not this entry's X column.
|
|
435
|
+
if [[ ${xy:0:1} == R || ${xy:0:1} == C ]]; then # guard:rename-payload-split-gate
|
|
436
|
+
rel="${rel#* -> }"
|
|
437
|
+
fi
|
|
438
|
+
res=$(classify_dirt_entry "$wt" "$xy" "$rel" "$staged")
|
|
439
|
+
verdict="${res%%|*}"
|
|
440
|
+
detail="${res#*|}"
|
|
441
|
+
entry_count=$((entry_count + 1))
|
|
442
|
+
[[ $verdict == "UNIQUE" ]] && unique_count=$((unique_count + 1)) # guard:unique-verdict-tally
|
|
443
|
+
[[ -z $agg_detail && -n $detail ]] && agg_detail="$detail"
|
|
444
|
+
printf 'DIRTFILE|%s|%s|%s|%s|%s\n' "$wt" "$verdict" "$detail" "$xy" "$rel"
|
|
445
|
+
done <<<"$out"
|
|
446
|
+
((entry_count == 0)) && return 0 # guard:aggregate-empty-entry-set
|
|
447
|
+
if ((unique_count == 0)); then # guard:aggregate-all-disposable-gate
|
|
448
|
+
printf 'DIRTSUM|%s|ALL_DISPOSABLE|%s\n' "$wt" "$agg_detail"
|
|
449
|
+
else
|
|
450
|
+
printf 'DIRTSUM|%s|HAS_UNIQUE|\n' "$wt"
|
|
451
|
+
fi
|
|
452
|
+
return 0
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
clear_disposable_dirt() {
|
|
456
|
+
# The opt-in clearing sequence: classify, require the aggregate to be exactly
|
|
457
|
+
# ALL_DISPOSABLE, reset --hard, clean -fd, then emit the result record.
|
|
458
|
+
#
|
|
459
|
+
# `git worktree remove` is never invoked from here. The caller retries the existing
|
|
460
|
+
# unforced removal through remove_worktree_safe, so this path introduces no new
|
|
461
|
+
# removal call site and is not force-removal: it clears the working tree first and
|
|
462
|
+
# then retries the same unforced removal.
|
|
463
|
+
#
|
|
464
|
+
# `git clean` is never given -x, -X, or -ff. Without -x the ignored files that -fd
|
|
465
|
+
# leaves alone stay on disk, which matches the classifier's refusal to classify them.
|
|
466
|
+
#
|
|
467
|
+
# Anything other than an exact ALL_DISPOSABLE aggregate refuses: a HAS_UNIQUE
|
|
468
|
+
# aggregate, a hard status read (which emits no aggregate at all), and a worktree with
|
|
469
|
+
# no entries all take the same fail-closed branch, because none of them establishes
|
|
470
|
+
# that every entry is disposable.
|
|
471
|
+
#
|
|
472
|
+
# Args: $1 = worktree path. Returns 0 on a completed clear, 1 on a refusal or a
|
|
473
|
+
# failure.
|
|
474
|
+
local wt="$1" cout crc=0 agg="" rrc=0 clrc=0
|
|
475
|
+
cout=$(classify_worktree_dirt "$wt") || crc=$?
|
|
476
|
+
if ((crc == 0)); then # guard:clear-classify-exit-gate
|
|
477
|
+
agg=$(printf '%s\n' "$cout" | awk -F'|' '/^DIRTSUM\|/{print $3; exit}')
|
|
478
|
+
fi
|
|
479
|
+
if [[ $agg != "ALL_DISPOSABLE" ]]; then # guard:clear-requires-all-disposable
|
|
480
|
+
printf 'ACTION|dirt-clear|%s|REFUSED-UNIQUE\n' "$wt"
|
|
481
|
+
return 1
|
|
482
|
+
fi
|
|
483
|
+
cleanup_wt_git -C "$wt" reset --hard >/dev/null || rrc=$?
|
|
484
|
+
if ((rrc != 0)); then # guard:clear-reset-hard-fail
|
|
485
|
+
printf 'ACTION|dirt-clear|%s|FAILED\n' "$wt"
|
|
486
|
+
return 1
|
|
487
|
+
fi
|
|
488
|
+
cleanup_wt_git -C "$wt" clean -fd >/dev/null || clrc=$?
|
|
489
|
+
if ((clrc != 0)); then # guard:clear-clean-hard-fail
|
|
490
|
+
printf 'ACTION|dirt-clear|%s|FAILED\n' "$wt"
|
|
491
|
+
return 1
|
|
492
|
+
fi
|
|
493
|
+
printf 'ACTION|dirt-clear|%s|OK\n' "$wt"
|
|
494
|
+
return 0
|
|
495
|
+
}
|