diff --git a/packages/git-changed/README.md b/packages/git-changed/README.md index 92c40c2..b377294 100644 --- a/packages/git-changed/README.md +++ b/packages/git-changed/README.md @@ -165,7 +165,7 @@ Same options; returns `result.paths`. In order, first hit wins: 1. `options.base` / `--base ` — explicit always wins. -2. `$GITHUB_BASE_REF` as `origin/`, **if that ref exists locally**. On a pull request GitHub sets this to the target branch. The existence check matters: without it you hand back a ref that every subsequent git call rejects. +2. `$GITHUB_BASE_REF` — the PR's target branch — as `origin/`, else the bare ``, **whichever exists locally**. `origin/` comes first because Actions fetches only the remote ref and a stale local branch of the same name would diff against the wrong commit; the bare branch covers running the same tool outside CI. Checking existence matters: without it you hand back a ref that every subsequent git call rejects. 3. The repository default branch — `origin/HEAD` if set, else the first of `origin/main`, `origin/master`, `main`, `master` that resolves. Then the diff is taken from `git merge-base HEAD `, not from the base tip, so work that landed on the base after you forked is not attributed to you. @@ -284,7 +284,7 @@ if (process.env.CI && source === 'worktree') { | Files from someone else's merge included | You diffed the base tip somewhere else in your pipeline; this package uses the merge base. Verify with `--json` and look at `mergeBase`. | | A new module's files are missing | You're not using this package, or not `-uall`. Untracked files inside a new directory need it. | | Tool crashes on a missing path | Something passed `existingOnly: false`. The default drops deleted paths. | -| `origin/main` not found | `$GITHUB_BASE_REF` was set but unfetched. This package falls through to the local default branch rather than returning a broken ref. | +| `origin/main` not found | `$GITHUB_BASE_REF` was set but unfetched. This package falls back to the local branch of that name, then to the default branch, rather than returning a broken ref. | ## License diff --git a/packages/git-changed/__tests__/changed.test.ts b/packages/git-changed/__tests__/changed.test.ts index 2f0bf6a..1261e4a 100644 --- a/packages/git-changed/__tests__/changed.test.ts +++ b/packages/git-changed/__tests__/changed.test.ts @@ -168,14 +168,24 @@ describe('changedFiles', () => { expect(result.files.map((f) => f.relative)).toEqual(['dirty.sql']); }); - it('ignores $GITHUB_BASE_REF when the remote ref is missing', () => { + it('accepts a bare $GITHUB_BASE_REF branch when the remote ref is missing', () => { const dir = track(makeRepo()); process.env.GITHUB_BASE_REF = 'main'; write(dir, 'dirty.sql'); // `origin/main` does not exist here. A naive implementation hands back that - // ref anyway and every later git call rejects it; this falls through to the - // local default branch instead. + // ref anyway and every later git call rejects it; this falls back to the + // local branch of the same name. + const result = changedFiles({ cwd: dir }); + expect(result.base).toBe('main'); + expect(result.files.map((f) => f.relative)).toEqual(['dirty.sql']); + }); + + it('falls through to the default branch when $GITHUB_BASE_REF names nothing', () => { + const dir = track(makeRepo()); + process.env.GITHUB_BASE_REF = 'release/9.9'; + write(dir, 'dirty.sql'); + const result = changedFiles({ cwd: dir }); expect(result.base).toBe('main'); expect(result.files.map((f) => f.relative)).toEqual(['dirty.sql']); diff --git a/packages/git-changed/src/base.ts b/packages/git-changed/src/base.ts index e0a9427..f237c0d 100644 --- a/packages/git-changed/src/base.ts +++ b/packages/git-changed/src/base.ts @@ -25,8 +25,8 @@ export function defaultBranch(cwd: string): string | undefined { * * 1. an explicit base (`--base`, or `base` in code) — always wins; * 2. `$GITHUB_BASE_REF` — the PR's target branch, set by GitHub Actions on - * `pull_request` events, as `origin/` since only the remote ref is - * fetched; + * `pull_request` events — as `origin/`, or the bare branch when the + * remote ref was never fetched; * 3. the repository's default branch. * * `undefined` means "no base is available" — a detached checkout, a fresh repo @@ -37,12 +37,17 @@ export function defaultBranch(cwd: string): string | undefined { export function resolveBase(base?: string, cwd: string = process.cwd()): string | undefined { if (base && base.trim()) return base.trim(); - const prBase = process.env.GITHUB_BASE_REF; - if (prBase && prBase.trim()) { - const ref = `origin/${prBase.trim()}`; - if (tryGit(['rev-parse', '--verify', '--quiet', ref], cwd) !== undefined) return ref; - // The remote ref is missing (a single-branch or shallow fetch). Fall through - // rather than handing back a ref that every later git call will reject. + const prBase = process.env.GITHUB_BASE_REF?.trim(); + if (prBase) { + // `origin/` first: in Actions only the remote ref is fetched, and a + // stale local branch of the same name would diff against the wrong commit. + // A bare local branch is the fallback for a developer running the same tool + // outside CI with the variable exported. + for (const ref of [`origin/${prBase}`, prBase]) { + if (tryGit(['rev-parse', '--verify', '--quiet', ref], cwd) !== undefined) return ref; + } + // Neither exists (a single-branch or shallow fetch). Fall through rather than + // handing back a ref that every later git call will reject. } return defaultBranch(cwd);