wave review-e7b551ebd865: merge #3809 (3675e47274)

This commit is contained in:
garrytan committed 2000-01-01 00:00:00 +00:00
commit 738aa8f55c
6 files changed
+142 -13

No files matched your search

+14
View File
@@ -36,6 +36,20 @@ _default_base() {
BASE="${1:-$(_default_base)}"
HEAD_REF=""
if [ "${2:-}" = "--head" ]; then HEAD_REF="${3:-}"; fi
# A bare branch name reads origin's tracking branch, by its full ref, when that
# shares history with HEAD: /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). A remote-qualified name is taken as given.
_NOT_BRANCH='^[0-9a-f]{7,64}$|[~^]|@\{|^[A-Z_]*HEAD$|^refs/'
_QUALIFIED=false
for _remote in $(git remote 2>/dev/null); do
case "$BASE" in "$_remote"/*) _QUALIFIED=true ;; esac
done
if [[ ! "$BASE" =~ $_NOT_BRANCH ]] && [ "$_QUALIFIED" = false ] \
&& git rev-parse --verify -q "refs/remotes/origin/${BASE}^{commit}" >/dev/null 2>&1 \
&& git merge-base "refs/remotes/origin/${BASE}" "${HEAD_REF:-HEAD}" >/dev/null 2>&1; then
BASE="refs/remotes/origin/${BASE}"
fi
FRONTEND=false
BACKEND=false
+1 -1
View File
@@ -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's tracking branch (`refs/remotes/origin/main`) when it exists and shares history with HEAD. That's the base /ship fetches and merges against. Before, a local `main` that was behind `origin/main` set the merge base. That happens in a linked worktree, and on a Capy machine, where local `main` never moves. 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` now says `local main is not origin/main; base is origin/main` on stderr when that happens. A base you name with its remote (`upstream/main`), a commit or `HEAD` is used as given. A tag named `origin/main` or a fork's `origin/upstream/main` branch can't stand in for the base, and another remote's branch only replaces a local base that is missing or unrelated (#3650).
+21 -12
View File
@@ -85,22 +85,31 @@ 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 bare branch name that is origin's tracking branch
* (`refs/remotes/origin/main` for `main`), the base /ship merges against,
* else the related local branch, else another remote's tracking branch. A
* local `main` that is behind (a linked worktree or a Capy clone never moves
* it, #3650), missing or unrelated to 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.
* A remote-qualified name (`upstream/main`) is taken as given, and the twin is
* read by its full ref, so a fork's branch or a tag cannot stand in for it.
*/
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);
const remotes = remoteNames(cwd);
if (!isBranchName(ref) || remotes.some(remote => ref.startsWith(`${remote}/`))) return direct ? { ref, sha: direct } : null;
const twin = (remote: string) => {
const sha = resolvesToCommit(`refs/remotes/${remote}/${ref}`, cwd);
if (!sha || !related(sha)) return null;
return sha === direct ? { ref: `${remote}/${ref}`, sha } : { ref: `${remote}/${ref}`, sha, local: !direct ? 'missing' as const : related(direct) ? 'differs' as const : 'unrelated' as const };
};
const origin = remotes.includes('origin') ? twin('origin') : null;
if (origin) return origin;
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' };
for (const remote of remotes) {
const other = twin(remote);
if (other) return other;
}
return direct ? { ref, sha: direct } : null;
}
+52
View File
@@ -330,6 +330,58 @@ describe('exit-code contract (#2526)', () => {
});
});
describe('a bare base name reads origin/<base> when it exists (#3650)', () => {
function behindBase(base: string) {
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', '-B', base, 'main');
writeFileSync(join(dir, 'service.py'), 'print(1)\n');
git('add', '.');
git('commit', '-qm', 'upstream backend change');
git('update-ref', `refs/remotes/origin/${base}`, 'HEAD');
git('checkout', '-q', 'feature/test');
git('rebase', '-q', `origin/${base}`);
git('branch', '-f', base, seed);
expect(git('diff', '--name-only', `${base}...HEAD`).split('\n').sort()).toEqual(['README2.md', 'service.py']);
return { dir, git };
}
const scopeAt = (dir: string, base: string) => Object.fromEntries(spawnSync('bash', [SCRIPT, base], { cwd: dir, stdio: 'pipe', timeout: 5000 }).stdout.toString().trim().split('\n').map(l => l.split('=')));
test('a local main behind origin/main does not widen the scope to upstream commits', () => {
const { dir } = behindBase('main');
const scope = runScope(dir);
expect([scope.SCOPE_BACKEND, scope.SCOPE_DOCS]).toEqual(['false', 'true']);
});
test('a slash-named base reads its remote-tracking twin too, and HEAD or a sha stays as given', () => {
const { dir, git } = behindBase('release/x');
expect(scopeAt(dir, 'release/x').SCOPE_BACKEND).toBe('false');
git('update-ref', 'refs/remotes/origin/HEAD', git('rev-parse', 'release/x'));
expect(scopeAt(dir, 'HEAD').SCOPE_DOCS).toBe('false');
git('update-ref', `refs/remotes/origin/${git('rev-parse', 'release/x')}`, 'HEAD');
expect(scopeAt(dir, git('rev-parse', 'release/x')).SCOPE_BACKEND).toBe('true');
});
test('a colliding tag, a fork branch over an explicit remote base, or an unrelated twin never replaces the base (#3650 review)', () => {
const { dir, git } = behindBase('main');
const upstreamCommit = git('rev-parse', 'origin/main');
git('tag', 'origin/main', 'HEAD');
expect([scopeAt(dir, 'main').SCOPE_BACKEND, scopeAt(dir, 'main').SCOPE_DOCS]).toEqual(['false', 'true']);
git('remote', 'add', 'upstream', 'file:///nonexistent-upstream');
git('update-ref', 'refs/remotes/upstream/main', git('rev-parse', 'main'));
git('update-ref', 'refs/remotes/origin/upstream/main', 'HEAD');
expect(scopeAt(dir, 'upstream/main').SCOPE_BACKEND).toBe('true');
git('tag', '-d', 'origin/main');
git('checkout', '-q', '--orphan', 'island');
git('commit', '-q', '--allow-empty', '-m', 'unrelated');
git('update-ref', 'refs/remotes/origin/main', 'HEAD');
git('checkout', '-q', 'feature/test');
expect(scopeAt(dir, 'main').SCOPE_BACKEND).toBe('true');
expect(upstreamCommit).not.toBe('');
});
});
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-'));
+53
View File
@@ -153,6 +153,59 @@ 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');
});
// Value: protects=a ref a contributor controls (a fork's origin/upstream/main branch, a tag named origin/main, another
// remote's main) never replaces the base the caller named or a related local base, so it cannot hide a gate edit;
// fails_when=resolveBase rewrites remote-qualified names, resolves the twin by its short name, or lets any remote beat a
// related local base; why_new=the cases above have one remote and no colliding refs; seam=none; no_claim=nothing about
// a remote's HEAD symref.
test('a fork branch, a colliding tag or another remote never replaces the base (#3650 review)', () => {
const src = repo({ fixture: 'rh1-skip', commit: true });
const seed = gitIn(src, ['rev-parse', 'main']).trim();
const clone = (remote: string) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-gate-diff-refs-'));
repos.push(dir);
gitIn(dir, ['clone', '-q', '-o', remote, '--branch', 'feature', `file://${src}`, '.']);
return dir;
};
const named = clone('upstream');
gitIn(named, ['remote', 'add', 'origin', 'file:///nonexistent-fork']);
gitIn(named, ['update-ref', 'refs/remotes/origin/upstream/main', 'HEAD']);
expect(summary(run(named, ['upstream/main']).stdout).tagged).toBe('RH-1:1');
const tagged = clone('origin');
gitIn(tagged, ['tag', 'origin/main', 'HEAD']);
expect(summary(run(tagged, ['main']).stdout).tagged).toBe('RH-1:1');
const fork = clone('zfork');
gitIn(fork, ['branch', '-f', 'main', seed]);
gitIn(fork, ['update-ref', 'refs/remotes/zfork/main', 'HEAD']);
expect(summary(run(fork, ['main']).stdout).tagged).toBe('RH-1:1');
});
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-'));