mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
fix(gate-diff): read a bare base as origin/<base> in gate-diff and diff-scope (#3650)
gstack-gate-diff and gstack-diff-scope resolved a bare base such as main to the local branch whenever it shared history with HEAD. A local main that is behind origin/main (a linked worktree, or a Capy clone, where local main never moves) then set the merge base, so /ship's gate scan and specialist scope also read commits already upstream. Both now prefer origin/<base> when it resolves, the base /ship fetches and merges against. Fixes #3650 Co-authored-by: exGeni <135786749+exGeni@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
6954a8c42b
commit
4aa052350b
6 files changed
+68
-14
No files matched your search
@@ -34,6 +34,13 @@ _default_base() {
|
||||
echo "main"
|
||||
}
|
||||
BASE="${1:-$(_default_base)}"
|
||||
# A bare branch name reads its remote-tracking twin when there is one: /ship
|
||||
# merges against origin/<base>, and a linked worktree or a Capy clone never
|
||||
# moves the local branch, so a stale one widens the scope (#3650).
|
||||
case "$BASE" in
|
||||
*/*|*[~^:@]*) ;;
|
||||
*) git rev-parse --verify -q "refs/remotes/origin/${BASE}^{commit}" >/dev/null 2>&1 && BASE="origin/${BASE}" ;;
|
||||
esac
|
||||
HEAD_REF=""
|
||||
if [ "${2:-}" = "--head" ]; then HEAD_REF="${3:-}"; fi
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ function main(argv: string[]): number {
|
||||
process.stdout.write(`GATE_ERROR=no_base ref=${baseRef} fix=${noBaseFix(baseRef, root)}\n`);
|
||||
return 2;
|
||||
}
|
||||
if (base.local) console.error(`gstack-gate-diff: ${base.local === 'unrelated' ? `local ${baseRef} shares no history with HEAD` : `no local branch ${baseRef}`}; base is ${base.ref}`);
|
||||
if (base.local) console.error(`gstack-gate-diff: ${base.local === 'unrelated' ? `local ${baseRef} shares no history with HEAD` : base.local === 'differs' ? `local ${baseRef} is not ${base.ref}` : `no local branch ${baseRef}`}; base is ${base.ref}`);
|
||||
const mb = mergeBase(base.sha, head, root);
|
||||
if (!mb) {
|
||||
process.stdout.write(`GATE_ERROR=no_base ref=${baseRef} fix=${noBaseFix(baseRef, root)}\n`);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- `gstack-gate-diff` and `gstack-diff-scope` now read a bare base name such as `main` as `origin/main` when that ref exists, the base /ship fetches and merges against. Before, a local `main` that was behind `origin/main` (a linked worktree, or a Capy clone, where local `main` never moves) set the merge base, so the gate scan and the specialist scope also read commits already on `origin/main`. One /ship run in #3650 listed a `.github/workflows/ci.yml` hunk from upstream and dispatched an extra performance reviewer. `gstack-gate-diff` says so on stderr (`local main is not origin/main; base is origin/main`). An explicit `origin/main` or commit is unchanged (#3650).
|
||||
+14
-13
@@ -85,22 +85,23 @@ export function isShallow(cwd: string): boolean {
|
||||
|
||||
/**
|
||||
* The base as a ref that resolves here and, when `head` is given, shares
|
||||
* history with it: the ref itself, else its remote-tracking branch
|
||||
* (`origin/main` for `main`). A clone that checked out only the feature
|
||||
* branch has no local `main`, and the merge-base against `origin/main` is the
|
||||
* same commit; a stale local `main` that shares no history with HEAD (a
|
||||
* fetched-over clone) yields to the twin too, so the fix line's fetch into the
|
||||
* tracking ref is a fix and not a loop.
|
||||
* history with it: for a branch name its remote-tracking branch first
|
||||
* (`origin/main` for `main`), the base /ship merges against, else the ref
|
||||
* itself. A local `main` that is missing, behind (a linked worktree or a Capy
|
||||
* clone never moves it, #3650) or shares no history with HEAD (a fetched-over
|
||||
* clone) yields to the twin, so the fix line's fetch into the tracking ref is
|
||||
* a fix and not a loop.
|
||||
*/
|
||||
export function resolveBase(ref: string, cwd: string, head?: string): { ref: string; sha: string; local?: 'missing' | 'unrelated' } | null {
|
||||
export function resolveBase(ref: string, cwd: string, head?: string): { ref: string; sha: string; local?: 'missing' | 'unrelated' | 'differs' } | null {
|
||||
const related = (sha: string) => !head || mergeBase(sha, head, cwd) !== null;
|
||||
const direct = resolvesToCommit(ref, cwd);
|
||||
if (direct && related(direct)) return { ref, sha: direct };
|
||||
if (!isBranchName(ref)) return direct ? { ref, sha: direct } : null;
|
||||
for (const remote of remoteNames(cwd)) {
|
||||
const tracking = `${remote}/${ref}`;
|
||||
const sha = resolvesToCommit(tracking, cwd);
|
||||
if (sha && related(sha)) return { ref: tracking, sha, local: direct ? 'unrelated' : 'missing' };
|
||||
if (isBranchName(ref)) {
|
||||
for (const remote of remoteNames(cwd)) {
|
||||
const tracking = `${remote}/${ref}`;
|
||||
const sha = resolvesToCommit(tracking, cwd);
|
||||
if (!sha || !related(sha)) continue;
|
||||
return sha === direct ? { ref: tracking, sha } : { ref: tracking, sha, local: !direct ? 'missing' : related(direct) ? 'differs' : 'unrelated' };
|
||||
}
|
||||
}
|
||||
return direct ? { ref, sha: direct } : null;
|
||||
}
|
||||
|
||||
@@ -330,6 +330,25 @@ describe('exit-code contract (#2526)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('a bare base name reads origin/<base> when it exists (#3650)', () => {
|
||||
test('a local main behind origin/main does not widen the scope to upstream commits', () => {
|
||||
const dir = createRepo(['README2.md']);
|
||||
const git = (...args: string[]) => spawnSync('git', args, { cwd: dir, stdio: 'pipe', timeout: 5000 }).stdout.toString().trim();
|
||||
const seed = git('rev-parse', 'main');
|
||||
git('checkout', '-q', 'main');
|
||||
writeFileSync(join(dir, 'service.py'), 'print(1)\n');
|
||||
git('add', '.');
|
||||
git('commit', '-qm', 'upstream backend change');
|
||||
git('update-ref', 'refs/remotes/origin/main', 'HEAD');
|
||||
git('checkout', '-q', 'feature/test');
|
||||
git('rebase', '-q', 'origin/main');
|
||||
git('branch', '-f', 'main', seed);
|
||||
expect(git('diff', '--name-only', 'main...HEAD').split('\n').sort()).toEqual(['README2.md', 'service.py']);
|
||||
const scope = runScope(dir);
|
||||
expect([scope.SCOPE_BACKEND, scope.SCOPE_DOCS]).toEqual(['false', 'true']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uncommitted work is visible (#2299)', () => {
|
||||
test('uncommitted change on a branch with no commits sets the scope', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'diff-scope-dirty-'));
|
||||
|
||||
@@ -153,6 +153,32 @@ describe('gstack-gate-diff arguments and exit contract', () => {
|
||||
// fetches from it; fails_when=remoteNames or resolveBase hard-code origin, or noBaseFix falls back to the literal 'origin'
|
||||
// while a real remote exists; why_new=every existing fixture has either an origin remote or none; seam=none;
|
||||
// no_claim=nothing about the order when origin and another remote both carry the branch.
|
||||
// Value: protects=a bare base name scans from the same merge base /ship merges against (origin/<base>) when the local
|
||||
// branch is related but behind (#3650); fails_when=resolveBase prefers any related local branch, so a stale local main
|
||||
// widens the scan to commits already upstream; why_new=the stale-main test above covers only unrelated history;
|
||||
// seam=none; no_claim=nothing about a local base that is ahead of its remote-tracking twin.
|
||||
test('a related local main that is behind origin/main yields to origin/main (#3650)', () => {
|
||||
const src = repo({ fixture: 'rh1-skip', commit: true });
|
||||
const seed = gitIn(src, ['rev-parse', 'main']).trim();
|
||||
gitIn(src, ['checkout', '-q', 'main']);
|
||||
fs.mkdirSync(path.join(src, '.github', 'workflows'), { recursive: true });
|
||||
fs.writeFileSync(path.join(src, '.github', 'workflows', 'ci.yml'), 'on: push\njobs: {}\n');
|
||||
gitIn(src, ['add', '-A']);
|
||||
gitIn(src, ['commit', '-q', '-m', 'upstream ci change']);
|
||||
gitIn(src, ['checkout', '-q', 'feature']);
|
||||
gitIn(src, ['rebase', '-q', 'main']);
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-gate-diff-behind-'));
|
||||
repos.push(dir);
|
||||
gitIn(dir, ['clone', '-q', '--branch', 'feature', `file://${src}`, '.']);
|
||||
gitIn(dir, ['branch', '-f', 'main', seed]);
|
||||
const bare = run(dir, ['main']);
|
||||
const remote = run(dir, ['origin/main']);
|
||||
expect([bare.status, remote.status]).toEqual([0, 0]);
|
||||
expect(summary(bare.stdout).candidate).toBe(summary(remote.stdout).candidate);
|
||||
expect(bare.stdout).toBe(remote.stdout);
|
||||
expect(bare.stderr).toContain('gstack-gate-diff: local main is not origin/main; base is origin/main');
|
||||
});
|
||||
|
||||
test('a clone whose only remote is upstream resolves the base to upstream/main, and the fix line fetches from upstream', () => {
|
||||
const src = repo({ fixture: 'rh1-skip', commit: true });
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-gate-diff-upstream-'));
|
||||
|
||||
Reference in new issue
Block a user