@danmoisan/drm-copilot-mcp 1.1.24 → 1.1.26
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/parallel-orchestrator.md +7 -3
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +6 -2
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +41 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +69 -3
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +2 -2
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.cleanup.ps1 +246 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +1 -0
- package/resources/claude-customizations/.claude/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 +626 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmission.psm1 +508 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1 +141 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionPrecedence.psm1 +508 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +17 -1
- package/resources/claude-customizations/.claude/settings.json +1 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +93 -14
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +23 -10
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +54 -46
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh +234 -0
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +15 -5
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +71 -7
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +26 -2
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +82 -12
- package/resources/claude-customizations/pack-manifests/core.json +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +15 -5
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +4 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +15 -5
- package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +3 -1
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -10
|
@@ -162,7 +162,9 @@ The script emits pipe-delimited, `LC_ALL=C`-ordered records, one per line:
|
|
|
162
162
|
2. **Editorial triage of the cherry-pick candidates.** Review each
|
|
163
163
|
`COMMIT|...|UNIQUE|...` record — this is the LLM-judgment boundary. Confirm
|
|
164
164
|
editorially that the unique commits are genuinely documentation/agent-memory
|
|
165
|
-
content
|
|
165
|
+
content: paths under the repository-relative prefixes `docs/` and `.claude/agent-memory/`,
|
|
166
|
+
compared case-sensitively. A Markdown file outside those two prefixes (for example
|
|
167
|
+
`CLAUDE.md` or `.claude/rules/*.md`) is not consolidation content.
|
|
166
168
|
The script only reports the deterministic facts (SHA, paths, author, date); deciding
|
|
167
169
|
what counts as documentation is this skill's responsibility.
|
|
168
170
|
|
|
@@ -174,21 +176,42 @@ The script emits pipe-delimited, `LC_ALL=C`-ordered records, one per line:
|
|
|
174
176
|
reuse it silently. Conflicts are aborted and surfaced as `CONFLICT` for editorial
|
|
175
177
|
resolution, never auto-resolved.
|
|
176
178
|
|
|
179
|
+
**Cleanup context.** A cleanup run writes no orchestration checkpoint, so the enforcement
|
|
180
|
+
hooks recognize its operations by the cleanup context instead: the sanctioned-removal
|
|
181
|
+
manifest at the session root (`artifacts/orchestration/cleanup-worktrees-manifest.json` in
|
|
182
|
+
the main checkout where this skill runs, written whole with Write; see Sanctioned Removal
|
|
183
|
+
Manifest) and the consolidation worktree, identified by its branch
|
|
184
|
+
`documentationandmemories`. Any staging in that worktree beyond what the script performs
|
|
185
|
+
uses `git -C <consolidation root> add -- <path>` with paths under `docs/` or
|
|
186
|
+
`.claude/agent-memory/` only, and the commit uses
|
|
187
|
+
`git -C <consolidation root> commit -F <message file>` with no pathspec. The preimplementation
|
|
188
|
+
gate admits that commit only when every staged path is under those two prefixes.
|
|
189
|
+
|
|
177
190
|
4. **Push and hand off PR creation to `Agent(pr-author)`.** Push the consolidation
|
|
178
|
-
branch (`git
|
|
179
|
-
`
|
|
180
|
-
`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
branch (`git -C <consolidation root> push -u origin documentationandmemories`).
|
|
192
|
+
The push precedes `gh pr create`: the PR-author gate requires
|
|
193
|
+
`refs/remotes/origin/documentationandmemories` to resolve to the same commit as the
|
|
194
|
+
consolidation worktree `HEAD`, so push again after any later commit in that worktree. Refresh the
|
|
195
|
+
PR-context bundle with `mcp__drm-copilot__collect_pr_context` using base branch `main` and
|
|
196
|
+
`workspace_root` set to the consolidation worktree, so that
|
|
197
|
+
`artifacts/pr_context.summary.txt` and `artifacts/pr_context.appendix.txt` exist beneath the
|
|
198
|
+
consolidation worktree. A consolidation pull request has no orchestrator-state checkpoint and
|
|
199
|
+
is not validated against one; the hooks perform the cleanup readiness check instead.
|
|
200
|
+
`enforce-model-routing-receipt.ps1` admits the `Agent(pr-author)` delegation, and
|
|
201
|
+
`enforce-pr-author-skill.ps1` admits `gh pr create`, only while a fresh, valid manifest exists
|
|
202
|
+
at the session root; the PR-author gate also requires an explicit
|
|
203
|
+
`--head documentationandmemories --base main` and a branch diff limited to `docs/` and
|
|
204
|
+
`.claude/agent-memory/`, and its receipt checks still apply. Then delegate PR creation to
|
|
205
|
+
`Agent(pr-author)` per `.claude/skills/pr-author/SKILL.md`, which owns the body-file and
|
|
206
|
+
receipt contract; defer to that skill for it rather than restating any value from it here.
|
|
207
|
+
The delegation is non-isolated and passes the absolute body path beneath the consolidation
|
|
208
|
+
worktree, as that skill's coordinator-session topology requires. For `<N>`,
|
|
187
209
|
use the GitHub issue number the run executes under when the run has one. When the run
|
|
188
210
|
has none, `<N>` is an arbitrary run-scoped identifier chosen by the pr-author agent.
|
|
189
|
-
The delegation prompt MUST carry
|
|
190
|
-
and
|
|
191
|
-
|
|
211
|
+
The delegation prompt MUST carry exactly one `branch: documentationandmemories` label, on
|
|
212
|
+
its own line, and no `--head` or `--branch` text, plus the canonical issue number line when
|
|
213
|
+
the run has an issue number.
|
|
214
|
+
`enforce-model-routing-receipt.ps1` identifies the item from those lines and denies a
|
|
192
215
|
gated delegation it cannot identify. A cleanup run with no issue number is therefore
|
|
193
216
|
identified by the branch label alone, which is why the label is required and the issue line
|
|
194
217
|
is not.
|
|
@@ -397,6 +420,15 @@ specific files or commit SHAs, before step 9 acts on any finding.
|
|
|
397
420
|
`git worktree remove <path>` command only when a record in it covers that exact path. A removal
|
|
398
421
|
issued with no covering record is denied with each gate's existing, unchanged reason code.
|
|
399
422
|
|
|
423
|
+
The manifest lives at the session root: the main checkout where this skill runs. Both removal
|
|
424
|
+
gates, the PR-author gate, and the model-routing gate read the relative path
|
|
425
|
+
`artifacts/orchestration/cleanup-worktrees-manifest.json` against the hook process location,
|
|
426
|
+
which is the session root, so a manifest written in any other worktree is never read. Write the
|
|
427
|
+
whole document with Write; the preimplementation gate admits that Write without a checkpoint
|
|
428
|
+
only at the session-root spelling and only when the content carries the expected `tool` and
|
|
429
|
+
`schema_version`. An Edit of the manifest is not supported without a checkpoint, and neither is a
|
|
430
|
+
Codex `apply_patch` Update, Delete, or Move of it; rewrite the document with Write instead.
|
|
431
|
+
|
|
400
432
|
The document is UTF-8 JSON with `snake_case` keys. It carries six top-level fields. Its `removals`
|
|
401
433
|
and `preserved_files` entries are sibling arrays and are never nested: removal records are keyed by
|
|
402
434
|
worktree path, preserve records by the pair of worktree path and file path, and the gates must
|
|
@@ -445,7 +477,8 @@ These records serve the consolidation consumer and are never read by either gate
|
|
|
445
477
|
consumer reads that worktree's `MEMORY.md`.
|
|
446
478
|
- `source_path` — string, required, repo-relative within that worktree. The file to preserve.
|
|
447
479
|
- `change_class` — string, required, either `untracked` or `modified`, recording whether staging
|
|
448
|
-
adds a new file or carries a working-tree modification.
|
|
480
|
+
adds a new file or carries a working-tree modification. How each class is staged is described
|
|
481
|
+
under `### Preserve staging outcomes`.
|
|
449
482
|
- `disposition` — string, required, exactly `PRESERVE`.
|
|
450
483
|
- `verdict` — string, required, a member of the step-5 verdict vocabulary listed above.
|
|
451
484
|
- `target_path` — string, required, repo-relative on the consolidation branch. Required because the
|
|
@@ -499,6 +532,52 @@ These records serve the consolidation consumer and are never read by either gate
|
|
|
499
532
|
}
|
|
500
533
|
```
|
|
501
534
|
|
|
535
|
+
### Preserve staging outcomes
|
|
536
|
+
|
|
537
|
+
`cleanup-worktrees.sh --preserve` reports each record's outcome as
|
|
538
|
+
`ACTION|preserve-stage|<target_path>|<result>`. The result vocabulary is:
|
|
539
|
+
|
|
540
|
+
- `OK` — the record was written and staged.
|
|
541
|
+
- `FAILED` — a write or staging operation failed; the remaining records continue.
|
|
542
|
+
- `SKIPPED-INVALID` — a record field failed validation.
|
|
543
|
+
- `MISSING-SOURCE` — the source file does not exist in the named worktree.
|
|
544
|
+
- `HOST-TOKEN-BLOCKED` — the incoming bytes carry a host-identifying token; the whole pass stops
|
|
545
|
+
with exit 3 and nothing is staged.
|
|
546
|
+
- `IGNORED-TARGET` — the target path is ignored on the consolidation branch and is not forced.
|
|
547
|
+
- `MISSING-WORKTREE` — the consolidation worktree does not exist.
|
|
548
|
+
- `REFUSED-WOULD-REGRESS` — staging the record would lose a line that the consolidation branch
|
|
549
|
+
already carries, or the record repeats an earlier record's `target_path`.
|
|
550
|
+
|
|
551
|
+
Staging depends on whether `<consolidation-worktree>/<target_path>` is an existing regular file:
|
|
552
|
+
|
|
553
|
+
- Absent target, either `change_class`: the source is copied byte for byte.
|
|
554
|
+
- Existing target, `untracked` record: the source is copied only when every line of the target is
|
|
555
|
+
present in the source in the same order; otherwise the record is refused.
|
|
556
|
+
- Existing target, `modified` record with a merge base (the source path equals the target path, and
|
|
557
|
+
the file exists at the merge base of the worktree `HEAD` and the consolidation `HEAD`): the
|
|
558
|
+
worktree change must only add lines to the base (G1); a `git merge-file --union --object-id`
|
|
559
|
+
merge of the target, the base, and the source then runs; the merged result must keep every target
|
|
560
|
+
line (G2) and must add no line beyond the worktree's own additions (G3). The merged blob is
|
|
561
|
+
staged.
|
|
562
|
+
- Existing target, `modified` record without a merge base: treated as an existing `untracked`
|
|
563
|
+
target.
|
|
564
|
+
- A record whose `target_path` repeats an earlier record's in the same pass is refused; the first
|
|
565
|
+
record keeps the target.
|
|
566
|
+
|
|
567
|
+
A refused record still emits its `PRESERVE|` record, writes a `cleanup-worktrees: refused` line to
|
|
568
|
+
stderr naming the failed guard, appends no `MEMORY.md` index line, and makes the pass exit 1.
|
|
569
|
+
|
|
570
|
+
Residuals:
|
|
571
|
+
|
|
572
|
+
- A resurrected line textually identical to a line the worktree added cannot be distinguished by G3.
|
|
573
|
+
- The merge path requires Git 2.43 or later for `merge-file --object-id`; an older Git fails the
|
|
574
|
+
merge and the record is refused.
|
|
575
|
+
- The order in which `--union` places two insertions at the same position is not specified by Git;
|
|
576
|
+
G2 and G3 bound the result whatever the order.
|
|
577
|
+
- The planning phase writes unreferenced blob objects (`hash-object -w`, `merge-file --object-id`)
|
|
578
|
+
to the object store; it writes no index, ref, or working-tree file.
|
|
579
|
+
- A refused record is not consolidated; the operator consolidates it manually.
|
|
580
|
+
|
|
502
581
|
### Accepted residual
|
|
503
582
|
|
|
504
583
|
Both gates read the command text of the `Bash` tool call they are given. A removal routed
|
|
@@ -30,19 +30,24 @@ source "$SCRIPT_DIR/cleanup_worktrees_actions_lib.sh"
|
|
|
30
30
|
# shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh
|
|
31
31
|
# shellcheck disable=SC1091
|
|
32
32
|
source "$SCRIPT_DIR/cleanup_worktrees_detached_lib.sh"
|
|
33
|
-
#
|
|
34
|
-
#
|
|
33
|
+
# None of the four libraries below requires being sourced last, and none claims to be.
|
|
34
|
+
# Each library's source-time work is limited to function definitions plus constant and
|
|
35
35
|
# environment-default variable assignments; none of them calls a function while being
|
|
36
36
|
# sourced, and no function name is defined by more than one library, so no later source can
|
|
37
37
|
# shadow an earlier definition. Bash resolves a function name when the call runs, not when
|
|
38
38
|
# the caller is defined, so the cross-library calls resolve whatever the order: the
|
|
39
39
|
# disposable-dirt classifier is called BY run_report and BY delete_candidate in the
|
|
40
40
|
# libraries above, and the preserve library calls cleanup_wt_git from the enumeration
|
|
41
|
-
# library
|
|
42
|
-
# therefore grouping for readability only.
|
|
41
|
+
# library, consolidation_worktree_path from the actions library, and the staging decision
|
|
42
|
+
# from the merge library. The order here is therefore grouping for readability only.
|
|
43
43
|
# shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh
|
|
44
44
|
# shellcheck disable=SC1091
|
|
45
45
|
source "$SCRIPT_DIR/cleanup_worktrees_dirt_lib.sh"
|
|
46
|
+
# The merge library holds the preserve staging decision. It is sourced before the
|
|
47
|
+
# line-ending library so that library stays immediately before the preserve library.
|
|
48
|
+
# shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh
|
|
49
|
+
# shellcheck disable=SC1091
|
|
50
|
+
source "$SCRIPT_DIR/cleanup_worktrees_preserve_merge_lib.sh"
|
|
46
51
|
# The line-ending and index group is placed immediately before the preserve library, whose
|
|
47
52
|
# read-only planning phase calls the functions it defines.
|
|
48
53
|
# shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh
|
|
@@ -69,12 +74,20 @@ Commands:
|
|
|
69
74
|
preserve | --preserve
|
|
70
75
|
Stage the manifest's preserved_files[] findings onto the
|
|
71
76
|
consolidation branch. Takes no operand; the manifest path comes
|
|
72
|
-
from CLEANUP_WT_MANIFEST_PATH.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
from CLEANUP_WT_MANIFEST_PATH. Staging depends on the target and
|
|
78
|
+
change_class: a target absent from the consolidation branch is
|
|
79
|
+
copied byte for byte; an existing target is copied only when
|
|
80
|
+
none of its lines would be lost (untracked, or modified with no
|
|
81
|
+
merge base), or receives a three-way union merge of the
|
|
82
|
+
worktree's added lines (modified with a merge base). A record
|
|
83
|
+
that would lose a line, fails a merge guard, or repeats an
|
|
84
|
+
earlier target_path is refused with
|
|
85
|
+
ACTION|preserve-stage|<target>|REFUSED-WOULD-REGRESS. The
|
|
86
|
+
MEMORY.md index line is carried across using the destination
|
|
87
|
+
file's own re-derived line-ending convention, and the whole pass
|
|
88
|
+
is refused when the incoming bytes carry a host-identifying
|
|
89
|
+
token. Exit 0 clean, 1 something was skipped or refused or an
|
|
90
|
+
index was created, 3 host-token hard stop, 127 a required tool
|
|
78
91
|
could not be resolved.
|
|
79
92
|
--help | -h | help Print this help and exit 0.
|
|
80
93
|
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
# cleanup_worktrees_actions_lib.sh, because it calls cleanup_wt_git (defined in the
|
|
13
13
|
# enumeration library) and consolidation_worktree_path (defined in the actions library).
|
|
14
14
|
#
|
|
15
|
-
# Two-phase design. preserve_plan
|
|
16
|
-
# and every record, verifies each source
|
|
17
|
-
#
|
|
18
|
-
# plan stream.
|
|
19
|
-
#
|
|
20
|
-
#
|
|
15
|
+
# Two-phase design. preserve_plan writes no index, ref, or working-tree file: it validates
|
|
16
|
+
# the manifest and every record, verifies each source exists, runs the host-token
|
|
17
|
+
# pre-pass, takes each staging and index decision, and emits the PRESERVE| and ACTION|
|
|
18
|
+
# records plus an internal plan stream. Its one write is the exception stated in
|
|
19
|
+
# cleanup_worktrees_preserve_merge_lib.sh: unreferenced blob objects created by
|
|
20
|
+
# `hash-object -w` and `merge-file --object-id`. preserve_commit_plan is the only writer of
|
|
21
|
+
# files and is entered only when the pre-pass reported no match.
|
|
21
22
|
#
|
|
22
23
|
# Seams. All git commands go through cleanup_wt_git (CLEANUP_WT_GIT_BIN); jq resolves
|
|
23
24
|
# through preserve_resolve_jq (CLEANUP_WT_JQ_BIN); the manifest path comes from
|
|
@@ -174,9 +175,8 @@ preserve_validate_record() {
|
|
|
174
175
|
# ACTION|preserve-stage|<target-path>|SKIPPED-INVALID; the first violation in the chain
|
|
175
176
|
# below wins, because one bad field skips the record whatever the others say.
|
|
176
177
|
#
|
|
177
|
-
# change_class is validated for vocabulary
|
|
178
|
-
#
|
|
179
|
-
# here so no later change invents a divergent path for one of them.
|
|
178
|
+
# change_class is validated here for vocabulary only; its effect on staging belongs to
|
|
179
|
+
# preserve_merge_decide (absent target copied, existing target guarded or merged).
|
|
180
180
|
#
|
|
181
181
|
# host_token_scan is refused when it is absent, is not a JSON object, is missing either
|
|
182
182
|
# member, or carries a result outside clean/tokens_present: a record whose scan metadata
|
|
@@ -236,16 +236,18 @@ preserve_validate_record() {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
preserve_plan_one() {
|
|
239
|
-
# Plan one manifest record.
|
|
239
|
+
# Plan one manifest record. Writes no index, ref, or working-tree file; the staging
|
|
240
|
+
# decision may write unreferenced blob objects (`hash-object -w`, `merge-file
|
|
241
|
+
# --object-id`), the exception cleanup_worktrees_preserve_merge_lib.sh states.
|
|
240
242
|
#
|
|
241
|
-
# The PRESERVE|
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
#
|
|
243
|
+
# The PRESERVE| record is emitted for every record that survives validation, before any
|
|
244
|
+
# per-record outcome is known, as DIRTY|<worktree-path>|<status-line> is; the outcome
|
|
245
|
+
# travels separately on the ACTION| result channel. The source file is read by plain
|
|
246
|
+
# file I/O at <worktree_path>/<source_path>, never through git (see the header).
|
|
245
247
|
#
|
|
246
|
-
# The
|
|
247
|
-
#
|
|
248
|
-
#
|
|
248
|
+
# The plan entry carries ten columns: target_path, source file, destination file, index
|
|
249
|
+
# action, index path, index line, terminator token, leading-terminator flag, staging
|
|
250
|
+
# mode (`copy` or `merge`), and the merged blob id (empty for `copy`).
|
|
249
251
|
#
|
|
250
252
|
# Args: $1 = the tab-separated record. Returns 0 when the record was planned and 1 when
|
|
251
253
|
# it was skipped, refused, or reported.
|
|
@@ -294,6 +296,11 @@ preserve_plan_one() {
|
|
|
294
296
|
printf 'cleanup-worktrees: destination is ignored, refusing to force: %s\n' "$tgt" >&2
|
|
295
297
|
return 1
|
|
296
298
|
fi
|
|
299
|
+
# The staging decision (merge library): a repeated target_path is refused, and a target
|
|
300
|
+
# that already exists is guarded or merged rather than overwritten. A refusal returns
|
|
301
|
+
# before the index decision, so a refused record produces no index record either.
|
|
302
|
+
preserve_merge_check_duplicate "$tgt" || return 1
|
|
303
|
+
preserve_merge_decide "${PRESERVE_FIELDS[2]}" "$wt" "$src" "$tgt" "$PRESERVE_CWT" || return 1
|
|
297
304
|
# The index decision, including the line-ending re-derivation it rests on, lives in
|
|
298
305
|
# the line-ending library. A memory_index_line of JSON null means the file is not a
|
|
299
306
|
# memory entry: it is copied and staged, and no index is read, created, or appended to.
|
|
@@ -307,16 +314,19 @@ preserve_plan_one() {
|
|
|
307
314
|
ipre=$PRESERVE_INDEX_PRE
|
|
308
315
|
fi
|
|
309
316
|
local entry
|
|
310
|
-
printf -v entry '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s' \
|
|
311
|
-
"$tgt" "$srcfile" "$PRESERVE_CWT/$tgt" "$iaction" "$ipath" "$iline" "$iterm" "$ipre"
|
|
317
|
+
printf -v entry '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s' \
|
|
318
|
+
"$tgt" "$srcfile" "$PRESERVE_CWT/$tgt" "$iaction" "$ipath" "$iline" "$iterm" "$ipre" \
|
|
319
|
+
"$PRESERVE_STAGE_MODE" "$PRESERVE_STAGE_OID"
|
|
312
320
|
PRESERVE_PLAN_STREAM+=("$entry")
|
|
313
321
|
return 0
|
|
314
322
|
}
|
|
315
323
|
|
|
316
324
|
preserve_plan() {
|
|
317
|
-
# Phase 1 of the two-phase driver: read, validate, decide, and report. It
|
|
318
|
-
#
|
|
319
|
-
# byte-unchanged and there is nothing to undo.
|
|
325
|
+
# Phase 1 of the two-phase driver: read, validate, decide, and report. It writes no
|
|
326
|
+
# index, ref, or working-tree file, so a failure anywhere in it leaves the consolidation
|
|
327
|
+
# worktree byte-unchanged and there is nothing to undo. The one exception is the staging
|
|
328
|
+
# decision's unreferenced blob objects (`hash-object -w`, `merge-file --object-id`),
|
|
329
|
+
# which nothing references unless phase 2 stages the merged result.
|
|
320
330
|
#
|
|
321
331
|
# Records are processed in LC_ALL=C order of the pair (worktree_path, source_path)
|
|
322
332
|
# rather than in manifest array order, because the report stream's ordering is
|
|
@@ -324,10 +334,8 @@ preserve_plan() {
|
|
|
324
334
|
# a contract. The sort is field-aware rather than whole-line, so a worktree_path that
|
|
325
335
|
# is a prefix of another cannot reorder the pair.
|
|
326
336
|
#
|
|
327
|
-
#
|
|
328
|
-
#
|
|
329
|
-
# per record that reached the writing phase, and PRESERVE_EXIT accumulates the
|
|
330
|
-
# per-record skip signal that becomes the pass's exit code.
|
|
337
|
+
# Outcomes survive the loop on globals: PRESERVE_PLAN_STREAM (one entry per planned
|
|
338
|
+
# record), PRESERVE_EXIT (the per-record skip signal), and PRESERVE_SEEN_TARGETS.
|
|
331
339
|
#
|
|
332
340
|
# Returns 0 when the pass may proceed to the writing phase, 1 when the manifest itself
|
|
333
341
|
# was rejected, 3 on the host-token hard stop, and 127 when a required tool could not
|
|
@@ -337,6 +345,9 @@ preserve_plan() {
|
|
|
337
345
|
PRESERVE_EXIT=0
|
|
338
346
|
PRESERVE_BLOCKED=0
|
|
339
347
|
PRESERVE_CWT=""
|
|
348
|
+
# Read by preserve_merge_check_duplicate in the merge library (SC2034 cannot see it).
|
|
349
|
+
# shellcheck disable=SC2034
|
|
350
|
+
PRESERVE_SEEN_TARGETS=""
|
|
340
351
|
local manifest records sorted line rc=0
|
|
341
352
|
manifest=${CLEANUP_WT_MANIFEST_PATH:-artifacts/orchestration/cleanup-worktrees-manifest.json}
|
|
342
353
|
PRESERVE_CWT=$(consolidation_worktree_path) || return $?
|
|
@@ -353,31 +364,26 @@ preserve_plan() {
|
|
|
353
364
|
[[ -n $line ]] || continue
|
|
354
365
|
preserve_plan_one "$line" || PRESERVE_EXIT=1
|
|
355
366
|
done <<<"$sorted"
|
|
356
|
-
# The hard stop is evaluated
|
|
357
|
-
#
|
|
358
|
-
# for any record. Nothing has been written at this point, so there is nothing to undo.
|
|
367
|
+
# The hard stop is evaluated after every record is examined, so every match is reported
|
|
368
|
+
# and the writing phase is entered for no record; no file has been written to undo.
|
|
359
369
|
((PRESERVE_BLOCKED == 0)) || return 3
|
|
360
370
|
return 0
|
|
361
371
|
}
|
|
362
372
|
|
|
363
373
|
preserve_commit_plan() {
|
|
364
|
-
# Phase 2: the only function in this library that writes
|
|
374
|
+
# Phase 2: the only function in this library that writes files. It executes each plan
|
|
375
|
+
# entry's decisions with exactly four kinds of operation: mkdir -p of the destination
|
|
376
|
+
# directory, the content write, the index append or creation, and the staging call. It
|
|
377
|
+
# takes no staging decision of its own, which keeps it thin enough to stay measured.
|
|
365
378
|
#
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
#
|
|
370
|
-
# leave in the coverage denominator rather than exclude from measurement.
|
|
379
|
+
# The content write follows column 9. `merge` writes the merged blob phase 1 recorded
|
|
380
|
+
# (`cat-file blob <oid>`); `copy` or empty copies the source byte for byte. Neither
|
|
381
|
+
# applies a line-ending transformation to the preserved content; the line-ending
|
|
382
|
+
# obligation is scoped to the index append, and staging re-normalizes tracked text.
|
|
371
383
|
#
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
|
|
375
|
-
# line-ending obligation is scoped to the index append, where the defect occurred.
|
|
376
|
-
#
|
|
377
|
-
# A non-zero result from any of the three operations emits
|
|
378
|
-
# ACTION|preserve-stage|<target-path>|FAILED and contributes to exit 1; it never aborts
|
|
379
|
-
# the remaining records, because each record's write is independent of the others.
|
|
380
|
-
local entry tgt srcfile dstfile iaction ipath iline iterm ipre fail
|
|
384
|
+
# A failed operation emits ACTION|preserve-stage|<target-path>|FAILED and contributes to
|
|
385
|
+
# exit 1; it never aborts the remaining records, whose writes are independent.
|
|
386
|
+
local entry tgt srcfile dstfile iaction ipath iline iterm ipre mode oid fail
|
|
381
387
|
((${#PRESERVE_PLAN_STREAM[@]} > 0)) || return 0
|
|
382
388
|
for entry in "${PRESERVE_PLAN_STREAM[@]}"; do
|
|
383
389
|
preserve_split_tsv "$entry"
|
|
@@ -389,10 +395,12 @@ preserve_commit_plan() {
|
|
|
389
395
|
iline=${PRESERVE_FIELDS[5]}
|
|
390
396
|
iterm=${PRESERVE_FIELDS[6]}
|
|
391
397
|
ipre=${PRESERVE_FIELDS[7]}
|
|
392
|
-
fail=""
|
|
398
|
+
mode=${PRESERVE_FIELDS[8]-} oid=${PRESERVE_FIELDS[9]-} fail=""
|
|
393
399
|
if ! mkdir -p -- "${dstfile%/*}" 2>/dev/null; then
|
|
394
400
|
fail="cannot create the destination directory"
|
|
395
|
-
elif ! cat
|
|
401
|
+
elif [[ $mode == "merge" ]] && ! cleanup_wt_git -C "$PRESERVE_CWT" cat-file blob "$oid" >"$dstfile"; then
|
|
402
|
+
fail="reading the merged blob failed"
|
|
403
|
+
elif [[ $mode != "merge" ]] && ! cat -- "$srcfile" >"$dstfile" 2>/dev/null; then
|
|
396
404
|
fail="the verbatim byte copy failed"
|
|
397
405
|
elif [[ $iaction != "none" ]] &&
|
|
398
406
|
! preserve_render_index_append "$ipath" "$iline" "$iterm" "$ipre"; then
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cleanup_worktrees_preserve_merge_lib.sh: sourceable staging-decision function group for
|
|
3
|
+
# the preserve pass of the cleanup-worktrees tool. For each preserve record whose target
|
|
4
|
+
# already exists as a regular file on the consolidation branch, it decides whether the
|
|
5
|
+
# source may be copied, whether a three-way union merge may be staged instead, or whether
|
|
6
|
+
# the record must be refused with ACTION|preserve-stage|<target-path>|REFUSED-WOULD-REGRESS.
|
|
7
|
+
#
|
|
8
|
+
# Sourcing contract: this library defines functions only and runs no work at source time.
|
|
9
|
+
# It MUST be sourced before cleanup_worktrees_preserve_lib.sh, whose preserve_plan_one calls
|
|
10
|
+
# preserve_merge_check_duplicate and preserve_merge_decide. Its own git calls go through
|
|
11
|
+
# cleanup_wt_git, defined in cleanup_worktrees_enumerate_lib.sh.
|
|
12
|
+
#
|
|
13
|
+
# Phase-1 write exception. The preserve planning phase writes no index, ref, or working-tree
|
|
14
|
+
# file. The one write it performs is the creation of unreferenced blob objects in the object
|
|
15
|
+
# store: `git hash-object -w` hashes the two merge inputs and `git merge-file --object-id`
|
|
16
|
+
# records the merged result. Nothing refers to those blobs unless the writing phase stages
|
|
17
|
+
# the merged result, and an unreferenced blob is removed by the normal object pruning.
|
|
18
|
+
#
|
|
19
|
+
# Line-comparison rule. Every guard compares lines. Content is split with the
|
|
20
|
+
# `while IFS= read -r line || [[ -n $line ]]` idiom, so an unterminated final line counts as
|
|
21
|
+
# a line, and one trailing carriage return is stripped from each line before comparison, so
|
|
22
|
+
# a CRLF working-tree file and its LF-normalized blob compare equal.
|
|
23
|
+
#
|
|
24
|
+
# No git call in this library passes -f or --force, and no process substitution is passed
|
|
25
|
+
# to git as a file argument: native git on Windows cannot open an MSYS /dev/fd path.
|
|
26
|
+
|
|
27
|
+
preserve_merge_split_lines() {
|
|
28
|
+
# Split one content string into the global array PRESERVE_MERGE_LINES under the
|
|
29
|
+
# line-comparison rule stated in the header. An empty string yields no line at all.
|
|
30
|
+
#
|
|
31
|
+
# Args: $1 = the content.
|
|
32
|
+
PRESERVE_MERGE_LINES=()
|
|
33
|
+
# The carriage return is held in a variable: ANSI-C quoting nested inside a
|
|
34
|
+
# double-quoted expansion is not interpreted consistently across bash versions.
|
|
35
|
+
local line cr=$'\r'
|
|
36
|
+
while IFS= read -r line || [[ -n $line ]]; do
|
|
37
|
+
PRESERVE_MERGE_LINES+=("${line%"$cr"}")
|
|
38
|
+
done < <(printf '%s' "${1-}")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
preserve_merge_is_subsequence() {
|
|
42
|
+
# Return 0 when every line of the needle appears in the haystack in the same order, not
|
|
43
|
+
# necessarily adjacent, and 1 otherwise. An empty needle is a subsequence of anything.
|
|
44
|
+
#
|
|
45
|
+
# Args: $1 = needle content, $2 = haystack content.
|
|
46
|
+
local -a needle=() hay=()
|
|
47
|
+
preserve_merge_split_lines "${1-}"
|
|
48
|
+
needle=("${PRESERVE_MERGE_LINES[@]}")
|
|
49
|
+
preserve_merge_split_lines "${2-}"
|
|
50
|
+
hay=("${PRESERVE_MERGE_LINES[@]}")
|
|
51
|
+
local n=${#needle[@]} i=0 line
|
|
52
|
+
if ((n == 0)); then
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
for line in "${hay[@]}"; do
|
|
56
|
+
if [[ $line == "${needle[$i]}" ]]; then
|
|
57
|
+
i=$((i + 1))
|
|
58
|
+
if ((i == n)); then
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
done
|
|
63
|
+
return 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
preserve_merge_extra_within() {
|
|
67
|
+
# Guard G3. Return 0 when multiset(merged) minus multiset(main) is contained in
|
|
68
|
+
# multiset(worktree) minus multiset(base), and 1 otherwise. A merged line that main does
|
|
69
|
+
# not carry is admitted only as one of the worktree's own additions, so a base line that
|
|
70
|
+
# main deleted and the merge re-emitted is refused. Counts are kept per line, so a line
|
|
71
|
+
# the worktree added once may appear at most once beyond main's own copies.
|
|
72
|
+
#
|
|
73
|
+
# Keys carry a fixed prefix because bash refuses an empty associative-array subscript,
|
|
74
|
+
# and a blank line is an ordinary line.
|
|
75
|
+
#
|
|
76
|
+
# Args: $1 = merged content, $2 = main content, $3 = worktree content, $4 = base content.
|
|
77
|
+
local -A extra=() added=()
|
|
78
|
+
local line key
|
|
79
|
+
preserve_merge_split_lines "${1-}"
|
|
80
|
+
for line in "${PRESERVE_MERGE_LINES[@]}"; do
|
|
81
|
+
key="k$line"
|
|
82
|
+
extra["$key"]=$((${extra["$key"]:-0} + 1))
|
|
83
|
+
done
|
|
84
|
+
preserve_merge_split_lines "${2-}"
|
|
85
|
+
for line in "${PRESERVE_MERGE_LINES[@]}"; do
|
|
86
|
+
key="k$line"
|
|
87
|
+
extra["$key"]=$((${extra["$key"]:-0} - 1))
|
|
88
|
+
done
|
|
89
|
+
preserve_merge_split_lines "${3-}"
|
|
90
|
+
for line in "${PRESERVE_MERGE_LINES[@]}"; do
|
|
91
|
+
key="k$line"
|
|
92
|
+
added["$key"]=$((${added["$key"]:-0} + 1))
|
|
93
|
+
done
|
|
94
|
+
preserve_merge_split_lines "${4-}"
|
|
95
|
+
for line in "${PRESERVE_MERGE_LINES[@]}"; do
|
|
96
|
+
key="k$line"
|
|
97
|
+
added["$key"]=$((${added["$key"]:-0} - 1))
|
|
98
|
+
done
|
|
99
|
+
for key in "${!extra[@]}"; do
|
|
100
|
+
if ((${extra["$key"]} > ${added["$key"]:-0} && ${extra["$key"]} > 0)); then
|
|
101
|
+
return 1
|
|
102
|
+
fi
|
|
103
|
+
done
|
|
104
|
+
return 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
preserve_merge_refuse() {
|
|
108
|
+
# Refuse one record: emit ACTION|preserve-stage|<target-path>|REFUSED-WOULD-REGRESS on
|
|
109
|
+
# stdout, a `cleanup-worktrees: refused` diagnostic naming the reason on stderr, set
|
|
110
|
+
# PRESERVE_EXIT to 1, and return 1. A refusal is not a hard stop: the remaining records
|
|
111
|
+
# are still planned and the pass exits 1.
|
|
112
|
+
#
|
|
113
|
+
# Args: $1 = target_path, $2 = the reason, which begins with the failed guard's token.
|
|
114
|
+
printf 'ACTION|preserve-stage|%s|REFUSED-WOULD-REGRESS\n' "${1-}"
|
|
115
|
+
printf 'cleanup-worktrees: refused %s: %s\n' "${1-}" "${2-}" >&2
|
|
116
|
+
# Read by preserve_plan and run_preserve across a source boundary shellcheck does not
|
|
117
|
+
# follow, so SC2034 reports it unused here; the suppression is line-scoped.
|
|
118
|
+
# shellcheck disable=SC2034
|
|
119
|
+
PRESERVE_EXIT=1
|
|
120
|
+
return 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
preserve_merge_check_duplicate() {
|
|
124
|
+
# Refuse a record whose target_path an earlier record of the same pass already named. A
|
|
125
|
+
# target counts as seen as soon as a record reaches this check, whatever that record's
|
|
126
|
+
# later outcome, so the first record keeps the target and every later one is refused.
|
|
127
|
+
# The seen set is the newline-delimited global PRESERVE_SEEN_TARGETS, which preserve_plan
|
|
128
|
+
# resets at the start of every pass.
|
|
129
|
+
#
|
|
130
|
+
# Args: $1 = target_path. Returns 0 when the target is new, 1 when it was refused.
|
|
131
|
+
local tgt=${1-}
|
|
132
|
+
local seen=${PRESERVE_SEEN_TARGETS:-$'\n'}
|
|
133
|
+
if [[ $seen == *$'\n'"$tgt"$'\n'* ]]; then
|
|
134
|
+
preserve_merge_refuse "$tgt" "duplicate-target: an earlier record in this pass names the same target_path"
|
|
135
|
+
return 1
|
|
136
|
+
fi
|
|
137
|
+
PRESERVE_SEEN_TARGETS="$seen$tgt"$'\n'
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
preserve_merge_resolve_base() {
|
|
142
|
+
# Echo the blob id of the source file at the merge base of the worktree HEAD and the
|
|
143
|
+
# consolidation HEAD, or return 1 when no base is available. The base is unavailable
|
|
144
|
+
# when source_path differs from target_path (no git call is made in that case), when
|
|
145
|
+
# the worktree HEAD does not resolve, when the two HEADs have no merge base, or when the
|
|
146
|
+
# file is absent at the merge base. An empty answer from git counts as unavailable.
|
|
147
|
+
#
|
|
148
|
+
# Args: $1 = worktree path, $2 = source_path, $3 = target_path, $4 = consolidation path.
|
|
149
|
+
local wt=${1-} src=${2-} tgt=${3-} cwt=${4-} head="" mb="" oid=""
|
|
150
|
+
if [[ $src != "$tgt" ]]; then
|
|
151
|
+
return 1
|
|
152
|
+
fi
|
|
153
|
+
head=$(cleanup_wt_git -C "$wt" rev-parse --verify --quiet HEAD) || return 1
|
|
154
|
+
[[ -n $head ]] || return 1
|
|
155
|
+
mb=$(cleanup_wt_git -C "$cwt" merge-base "$head" HEAD) || return 1
|
|
156
|
+
[[ -n $mb ]] || return 1
|
|
157
|
+
oid=$(cleanup_wt_git -C "$cwt" rev-parse --verify --quiet "$mb:$src") || return 1
|
|
158
|
+
[[ -n $oid ]] || return 1
|
|
159
|
+
printf '%s\n' "$oid"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
preserve_merge_guard_b() {
|
|
163
|
+
# Guard (b), the fallback when no merge base is available: admit a whole-file copy only
|
|
164
|
+
# when main's version is a line-subsequence of the source, so no main line is lost.
|
|
165
|
+
#
|
|
166
|
+
# Args: $1 = target_path, $2 = main content, $3 = source content.
|
|
167
|
+
if preserve_merge_is_subsequence "${2-}" "${3-}"; then
|
|
168
|
+
PRESERVE_STAGE_MODE="copy"
|
|
169
|
+
return 0
|
|
170
|
+
fi
|
|
171
|
+
preserve_merge_refuse "${1-}" "guard-b: the target holds lines the source lacks"
|
|
172
|
+
return 1
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# PRESERVE_STAGE_MODE and PRESERVE_STAGE_OID are read by preserve_plan_one across a source
|
|
176
|
+
# boundary shellcheck does not follow, so SC2034 reports them unused; the suppression is
|
|
177
|
+
# scoped to this one function.
|
|
178
|
+
# shellcheck disable=SC2034
|
|
179
|
+
preserve_merge_decide() {
|
|
180
|
+
# Decide how one record is staged. Sets PRESERVE_STAGE_MODE to `copy` or `merge` and
|
|
181
|
+
# PRESERVE_STAGE_OID to the merged blob id (empty for `copy`), or refuses the record.
|
|
182
|
+
#
|
|
183
|
+
# - target not a regular file: `copy`, with no git call at all;
|
|
184
|
+
# - `untracked` record, or `modified` record with no merge base: guard (b);
|
|
185
|
+
# - `modified` record with a merge base: G1 (the worktree change is insertion-only),
|
|
186
|
+
# then a `git merge-file --union --object-id` merge, then G2 (every main line
|
|
187
|
+
# survives) and G3 (no line beyond the worktree's own additions); `merge` on success.
|
|
188
|
+
#
|
|
189
|
+
# The source and target are read with plain file I/O. The base and merged contents are
|
|
190
|
+
# read from the object store with `cat-file blob`.
|
|
191
|
+
#
|
|
192
|
+
# Args: $1 = change_class, $2 = worktree path, $3 = source_path, $4 = target_path,
|
|
193
|
+
# $5 = consolidation path. Returns 0 when the record may be staged, 1 when it was refused.
|
|
194
|
+
local cls=${1-} wt=${2-} src=${3-} tgt=${4-} cwt=${5-}
|
|
195
|
+
PRESERVE_STAGE_MODE="copy"
|
|
196
|
+
PRESERVE_STAGE_OID=""
|
|
197
|
+
if [[ ! -f "$cwt/$tgt" ]]; then
|
|
198
|
+
return 0
|
|
199
|
+
fi
|
|
200
|
+
local main_c wt_c base_oid base_c main_oid wt_oid merged_oid merged_c mrc=0
|
|
201
|
+
main_c=$(<"$cwt/$tgt")
|
|
202
|
+
wt_c=$(<"$wt/$src")
|
|
203
|
+
if [[ $cls == "untracked" ]] || ! base_oid=$(preserve_merge_resolve_base "$wt" "$src" "$tgt" "$cwt"); then
|
|
204
|
+
preserve_merge_guard_b "$tgt" "$main_c" "$wt_c"
|
|
205
|
+
return
|
|
206
|
+
fi
|
|
207
|
+
base_c=$(cleanup_wt_git -C "$cwt" cat-file blob "$base_oid") ||
|
|
208
|
+
{ preserve_merge_refuse "$tgt" "G1: the base blob could not be read" || return 1; }
|
|
209
|
+
if ! preserve_merge_is_subsequence "$base_c" "$wt_c"; then
|
|
210
|
+
preserve_merge_refuse "$tgt" "G1: the worktree removed or changed a base line"
|
|
211
|
+
return 1
|
|
212
|
+
fi
|
|
213
|
+
main_oid=$(cleanup_wt_git -C "$cwt" hash-object -w --path="$tgt" -- "$tgt") || main_oid=""
|
|
214
|
+
wt_oid=$(cleanup_wt_git -C "$wt" hash-object -w --path="$tgt" -- "$src") || wt_oid=""
|
|
215
|
+
merged_oid=$(cleanup_wt_git -C "$cwt" merge-file --union --object-id "$main_oid" "$base_oid" "$wt_oid") || mrc=$?
|
|
216
|
+
if ((mrc != 0)) || [[ -z $main_oid || -z $wt_oid || -z $merged_oid ]]; then
|
|
217
|
+
preserve_merge_refuse "$tgt" "merge-file rc=$mrc: no merged blob was produced"
|
|
218
|
+
return 1
|
|
219
|
+
fi
|
|
220
|
+
merged_c=$(cleanup_wt_git -C "$cwt" cat-file blob "$merged_oid") ||
|
|
221
|
+
{ preserve_merge_refuse "$tgt" "G2: the merged blob could not be read" || return 1; }
|
|
222
|
+
if ! preserve_merge_is_subsequence "$main_c" "$merged_c"; then
|
|
223
|
+
preserve_merge_refuse "$tgt" "G2: the merged result lost a main line"
|
|
224
|
+
return 1
|
|
225
|
+
fi
|
|
226
|
+
if ! preserve_merge_extra_within "$merged_c" "$main_c" "$wt_c" "$base_c"; then
|
|
227
|
+
preserve_merge_refuse "$tgt" "G3: the merged result carries a line the worktree did not add"
|
|
228
|
+
return 1
|
|
229
|
+
fi
|
|
230
|
+
PRESERVE_STAGE_MODE="merge"
|
|
231
|
+
PRESERVE_STAGE_OID=$merged_oid
|
|
232
|
+
printf 'cleanup-worktrees: merged %s\n' "$tgt" >&2
|
|
233
|
+
return 0
|
|
234
|
+
}
|