mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
v1.92.25.0 fix(ship): Step 3 stops by name on an unfinished merge, cherry-pick or revert (GST-732) (#3785)
* test(ship): an unfinished merge or cherry-pick stops Step 3 by name (GST-732, failing first) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship): an unfinished merge, cherry-pick or revert stops Step 3 by name before any git merge (GST-732) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ship): Step 3 names the MERGE: up to date outcome (GST-732) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(ship): regenerate ship goldens for the Step 3 MERGE: up to date wording (GST-732) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (GST-732: cover the unfinished revert stop) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (GST-732: only a real operation state file stops Step 3; unresolved conflicts with none in progress stop by name) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): scope GST-732 to repositories that merge the base in /ship Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (GST-732: a failed --git-path probe is not an operation in progress) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ship): shorten Step 3's MERGE: up to date note so the ship skeleton fits its cap on current main (GST-732) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a2ce5bd888
commit
f739756389
13 files changed
+69
-23
No files matched your search
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## [1.92.25.0] - 2026-11-01
|
||||
|
||||
- In a repository that merges the base during `/ship` (any `queue_mode` but `stamp-at-merge`), Step 3 now stops by name when an earlier merge, cherry-pick or revert is still unfinished (`STOP: an unfinished cherry-pick is in progress (CHERRY_PICK_HEAD): …`), with any conflicted files listed, and runs no `git merge`. Conflicted files left with no operation in progress, as after a conflicted `git stash pop`, stop with those files named instead of a merge message. Before, both fell through to `git merge`, so on a machine whose shared-tree guard refuses `git merge`, the guard's refusal was the only message (GST-732). A branch or tag named `MERGE_HEAD`, `CHERRY_PICK_HEAD` or `REVERT_HEAD` is not mistaken for one. The ship skill's Step 3 now names the `MERGE: up to date` outcome.
|
||||
|
||||
## [1.92.24.0] - 2026-11-01
|
||||
|
||||
- `/ship` can finish after the owner accepts a Step 14.5 documentation risk. `gstack-docs-candidate snapshot --exception .gstack/tmp/docs-audit.json` writes the post-audit snapshot from the blocked record, naming who accepted it, the date, how and the base SHA, and `gstack-ship-release finish` prints that exception on its `DOCS:` line. Finish still stops on a blocked audit with no accepted exception, and on an exception snapshot whose docs-audit record no longer carries it (GST-713).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.92.24.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.92.25.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+16
-3
@@ -67,9 +67,22 @@ const forkPoint = (cwd: string, base: string) => git(cwd, ['merge-base', `origin
|
||||
|
||||
function merge(cwd: string, base: string, r: Report): boolean {
|
||||
const policy = shipPolicy(cwd, base);
|
||||
if (policy?.queue_mode === 'stamp-at-merge') r.add('MERGE: skipped (queue_mode stamp-at-merge: the slot stamp merges the base; docs/ship-policy.md#ship-before-ready)');
|
||||
else if (!git(cwd, ['ls-files', '-u']).stdout.trim() && ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD'].every(ref => git(cwd, ['rev-parse', '-q', '--verify', ref]).status !== 0)
|
||||
&& git(cwd, ['merge-base', '--is-ancestor', `origin/${base}`, 'HEAD']).status === 0) r.add('MERGE: up to date');
|
||||
const stampAtMerge = policy?.queue_mode === 'stamp-at-merge';
|
||||
const pending = stampAtMerge ? undefined : ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD'].find(ref => {
|
||||
const p = git(cwd, ['rev-parse', '--git-path', ref]);
|
||||
return p.status === 0 && !!p.stdout.trim() && fs.existsSync(path.resolve(cwd, p.stdout.trim()));
|
||||
});
|
||||
const unmerged = stampAtMerge ? [] : git(cwd, ['diff', '--name-only', '--diff-filter=U']).stdout.split('\n').filter(Boolean);
|
||||
if (pending) {
|
||||
r.stop(`an unfinished ${pending.replace(/_HEAD$/, '').toLowerCase().replace('_', '-')} is in progress (${pending}${unmerged.length ? `, conflicts in ${short(unmerged, 8)}` : ''}): resolve and commit it, or abort it, then rerun (Step 3)`);
|
||||
return false;
|
||||
}
|
||||
if (unmerged.length) {
|
||||
r.stop(`unresolved conflicts in ${short(unmerged, 8)} with no merge, cherry-pick or revert in progress: resolve them (git add) or restore the files, then rerun (Step 3)`);
|
||||
return false;
|
||||
}
|
||||
if (stampAtMerge) r.add('MERGE: skipped (queue_mode stamp-at-merge: the slot stamp merges the base; docs/ship-policy.md#ship-before-ready)');
|
||||
else if (git(cwd, ['merge-base', '--is-ancestor', `origin/${base}`, 'HEAD']).status === 0) r.add('MERGE: up to date');
|
||||
else {
|
||||
const before = git(cwd, ['rev-parse', 'HEAD']).stdout.trim();
|
||||
const m = git(cwd, ['merge', `origin/${base}`, '--no-edit'], 300_000);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.92.24",
|
||||
"version": "1.92.25",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
+1
-1
@@ -455,7 +455,7 @@ not web services already deployed.
|
||||
|
||||
## Step 3: Merge the base branch (BEFORE tests)
|
||||
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
|
||||
|
||||
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
|
||||
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ not web services already deployed.
|
||||
|
||||
## Step 3: Merge the base branch (BEFORE tests)
|
||||
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
|
||||
|
||||
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
|
||||
|
||||
|
||||
+1
-1
@@ -455,7 +455,7 @@ not web services already deployed.
|
||||
|
||||
## Step 3: Merge the base branch (BEFORE tests)
|
||||
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
|
||||
|
||||
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
|
||||
|
||||
|
||||
+1
-1
@@ -455,7 +455,7 @@ not web services already deployed.
|
||||
|
||||
## Step 3: Merge the base branch (BEFORE tests)
|
||||
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
|
||||
|
||||
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
|
||||
|
||||
|
||||
+1
-1
@@ -454,7 +454,7 @@ not web services already deployed.
|
||||
|
||||
## Step 3: Merge the base branch (BEFORE tests)
|
||||
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
|
||||
|
||||
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
|
||||
|
||||
|
||||
+1
-1
@@ -456,7 +456,7 @@ not web services already deployed.
|
||||
|
||||
## Step 3: Merge the base branch (BEFORE tests)
|
||||
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `skipped` under stamp-at-merge).
|
||||
The preflight merges the fetched base (`git merge`) so tests and reviews cover the integrated code (`MERGE:`; `up to date` if nothing new; `skipped` under stamp-at-merge).
|
||||
|
||||
**Merge conflicts:** `MERGE:` names them. Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing; then commit and rerun the preflight.
|
||||
|
||||
|
||||
@@ -44,20 +44,45 @@ describe('preflight merge when the base is already in HEAD (GST-691)', () => {
|
||||
expect(r.stdout).toContain('STOP: git merge origin/main failed: shared-tree guard: git merge is not allowed in this checkout');
|
||||
});
|
||||
|
||||
// Value: protects=an unfinished merge of another branch still stops at Step 3 with its conflicted files named, though the base is in HEAD;
|
||||
// fails_when=the ancestor shortcut reports up to date over an index with unmerged paths and the run tests conflict markers;
|
||||
// why_new=GST-691 /ship adversarial finding 1; seam=git ls-files -u; no_claim=how the agent resolves it
|
||||
test('an unfinished merge with the base already in HEAD still stops, conflicted or resolved and staged', () => {
|
||||
const { f } = guarded();
|
||||
// Value: protects=an unfinished merge, cherry-pick or revert stops at Step 3 with its name and conflicted files, though the base is in HEAD, and no git merge runs;
|
||||
// fails_when=the ancestor shortcut reports up to date over one, or the stop is a guard's refusal that names no cause;
|
||||
// why_new=GST-691 /ship adversarial findings; seam=MERGE_HEAD/CHERRY_PICK_HEAD/REVERT_HEAD; no_claim=how the agent resolves it
|
||||
test('an unfinished merge, cherry-pick or revert with the base already in HEAD stops and names it, conflicted or resolved', () => {
|
||||
const { f, merges } = guarded();
|
||||
f.git('checkout', '-q', '-b', 'other', 'main'); f.write({ 'src/a.ts': 'export const a = 5;\n' }); f.commit('other edits a');
|
||||
f.git('checkout', '-q', 'feat'); f.write({ 'src/a.ts': 'export const a = 6;\n' }); f.commit('feat edits a');
|
||||
expect(f.git('merge', '--no-edit', 'other').status).not.toBe(0);
|
||||
const r = f.run(PREFLIGHT, ['--base', 'main']);
|
||||
expect([r.status, lineOf(r.stdout, 'MERGE')]).toEqual([3, '']);
|
||||
expect(r.stdout).toMatch(/STOP: merge conflicts in src\/a\.ts/);
|
||||
const conflicted = f.run(PREFLIGHT, ['--base', 'main']);
|
||||
expect([conflicted.status, lineOf(conflicted.stdout, 'MERGE')]).toEqual([3, '']);
|
||||
expect(conflicted.stdout).toContain('STOP: an unfinished merge is in progress (MERGE_HEAD, conflicts in src/a.ts)');
|
||||
f.write({ 'src/a.ts': 'export const a = 7;\n' }); f.git('add', 'src/a.ts');
|
||||
const staged = f.run(PREFLIGHT, ['--base', 'main']);
|
||||
expect([staged.status, lineOf(staged.stdout, 'MERGE')]).toEqual([3, '']);
|
||||
expect(staged.stdout).toContain('STOP: git merge origin/main failed:');
|
||||
expect(f.run(PREFLIGHT, ['--base', 'main']).stdout).toContain('STOP: an unfinished merge is in progress (MERGE_HEAD):');
|
||||
f.git('merge', '--abort');
|
||||
expect(f.git('cherry-pick', 'other').status).not.toBe(0);
|
||||
f.write({ 'src/a.ts': 'export const a = 8;\n' }); f.git('add', 'src/a.ts');
|
||||
expect(f.run(PREFLIGHT, ['--base', 'main']).stdout).toContain('STOP: an unfinished cherry-pick is in progress (CHERRY_PICK_HEAD):');
|
||||
f.git('cherry-pick', '--abort');
|
||||
f.write({ 'src/a.ts': 'export const a = 9;\n' }); f.commit('feat edits a again');
|
||||
expect(f.git('revert', '--no-edit', 'HEAD~1').status).not.toBe(0);
|
||||
f.write({ 'src/a.ts': 'export const a = 10;\n' }); f.git('add', 'src/a.ts');
|
||||
expect(f.run(PREFLIGHT, ['--base', 'main']).stdout).toContain('STOP: an unfinished revert is in progress (REVERT_HEAD):');
|
||||
expect(merges()).toEqual([]);
|
||||
});
|
||||
|
||||
// Value: protects=only a real in-progress operation stops Step 3, and unresolved index entries with none in progress stop with their files named before any git merge;
|
||||
// fails_when=a branch or tag named like MERGE_HEAD/CHERRY_PICK_HEAD/REVERT_HEAD gives a stop no abort can clear, or a conflicted stash pop reaches git merge and gets a merge-specific stop;
|
||||
// why_new=GST-732 /ship adversarial findings 2 and 4; seam=git rev-parse --git-path, git diff --diff-filter=U; no_claim=git am or sequencer state
|
||||
test('a ref named REVERT_HEAD is not an unfinished revert, and a conflicted stash pop stops with its files named', () => {
|
||||
const { f, merges } = guarded();
|
||||
f.git('branch', 'REVERT_HEAD'); f.git('tag', 'CHERRY_PICK_HEAD');
|
||||
const named = f.run(PREFLIGHT, ['--base', 'main']);
|
||||
expect([named.status, lineOf(named.stdout, 'MERGE')]).toEqual([0, 'MERGE: up to date']);
|
||||
f.write({ 'src/a.ts': 'export const a = 6;\n' }); f.git('stash');
|
||||
f.write({ 'src/a.ts': 'export const a = 7;\n' }); f.commit('feat edits a');
|
||||
expect(f.git('stash', 'pop').status).not.toBe(0);
|
||||
const r = f.run(PREFLIGHT, ['--base', 'main']);
|
||||
expect(r.status).toBe(3);
|
||||
expect(r.stdout).toContain('STOP: unresolved conflicts in src/a.ts with no merge, cherry-pick or revert in progress');
|
||||
expect(merges()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"bytes": 29,
|
||||
"why": "GST-732: Step 3 names the MERGE: up to date outcome"
|
||||
}
|
||||
Reference in new issue
Block a user