Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/git-changed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Same options; returns `result.paths`.
In order, first hit wins:

1. `options.base` / `--base <ref>` — explicit always wins.
2. `$GITHUB_BASE_REF` as `origin/<ref>`, **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/<ref>`, else the bare `<ref>`, **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 <base>`, not from the base tip, so work that landed on the base after you forked is not attributed to you.
Expand Down Expand Up @@ -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

Expand Down
16 changes: 13 additions & 3 deletions packages/git-changed/__tests__/changed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down
21 changes: 13 additions & 8 deletions packages/git-changed/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<branch>` since only the remote ref is
* fetched;
* `pull_request` eventsas `origin/<branch>`, 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
Expand All @@ -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/<branch>` 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);
Expand Down
Loading