22 * Changed-file detection for `--changed`, so a CI gate (or a human before a
33 * commit) pays only for the SQL that a branch actually touched.
44 *
5- * Modelled on pgpm's bundle-drift check (`pgpm/core/src/packaging/check.ts` in
6- * constructive): resolve a base ref (explicit → `origin/$GITHUB_BASE_REF` in a
7- * PR → the repository 's default branch), diff `HEAD` against the **merge base**
8- * so unrelated commits on the base branch don't widen the set, and union that
9- * with uncommitted/untracked working-tree changes. Deleted paths are dropped —
10- * there is nothing left on disk to lint .
5+ * The git plumbing — base resolution, merge-base diff, working-tree union,
6+ * rename targets, dropping paths that no longer exist — lives in `git-changed`,
7+ * which is shared with pgpm 's bundle-drift check. This module is the `.sql`
8+ * filter over it, and the place where "no base" stays non-fatal: a lint gate
9+ * that refuses to run on a shallow clone lints nothing, which is worse than
10+ * linting the working tree .
1111 */
1212
13- import { execFileSync } from 'child_process' ;
14- import { existsSync , statSync } from 'fs' ;
15- import * as path from 'path' ;
16-
17- function git ( args : string [ ] , cwd : string ) : string {
18- return execFileSync ( 'git' , args , {
19- cwd,
20- encoding : 'utf-8' ,
21- stdio : [ 'ignore' , 'pipe' , 'ignore' ] ,
22- maxBuffer : 64 * 1024 * 1024
23- } ) ;
24- }
25-
26- function tryGit ( args : string [ ] , cwd : string ) : string | null {
27- try {
28- return git ( args , cwd ) ;
29- } catch {
30- return null ;
31- }
32- }
33-
34- /** The repository's default branch as a remote-tracking ref, when discoverable. */
35- function defaultBranch ( cwd : string ) : string | undefined {
36- const head = tryGit ( [ 'symbolic-ref' , '--short' , 'refs/remotes/origin/HEAD' ] , cwd ) ;
37- if ( head && head . trim ( ) ) return head . trim ( ) ;
38- for ( const candidate of [ 'origin/main' , 'origin/master' , 'main' , 'master' ] ) {
39- if ( tryGit ( [ 'rev-parse' , '--verify' , '--quiet' , candidate ] , cwd ) ) return candidate ;
40- }
41- return undefined ;
42- }
13+ import { changedFiles as gitChangedFiles , isRepo , resolveBase } from 'git-changed' ;
4314
4415/**
4516 * Resolve the ref to diff against. An explicit `base` wins; otherwise the PR
@@ -48,14 +19,7 @@ function defaultBranch(cwd: string): string | undefined {
4819 * the caller falls back to working-tree changes only.
4920 */
5021export function resolveChangedBase ( base ?: string , cwd : string = process . cwd ( ) ) : string | undefined {
51- if ( base && base . trim ( ) ) return base . trim ( ) ;
52- const prBase = process . env . GITHUB_BASE_REF ;
53- if ( prBase && prBase . trim ( ) ) {
54- const ref = `origin/${ prBase . trim ( ) } ` ;
55- if ( tryGit ( [ 'rev-parse' , '--verify' , '--quiet' , ref ] , cwd ) ) return ref ;
56- return prBase . trim ( ) ;
57- }
58- return defaultBranch ( cwd ) ;
22+ return resolveBase ( base , cwd ) ;
5923}
6024
6125export interface ChangedFilesResult {
@@ -67,65 +31,23 @@ export interface ChangedFilesResult {
6731 mergeBase ?: string ;
6832}
6933
70- /** Parse `git status --porcelain` into paths (rename target wins). */
71- function workingTreePaths ( cwd : string ) : string [ ] {
72- const out : string [ ] = [ ] ;
73- // `-uall` lists untracked *files*; the default collapses a new directory to
74- // the directory name, which would hide every file a new module adds.
75- const status = tryGit ( [ 'status' , '--porcelain' , '-uall' ] , cwd ) ?? '' ;
76- for ( const rawLine of status . split ( '\n' ) ) {
77- const line = rawLine . trimEnd ( ) ;
78- if ( ! line ) continue ;
79- let p = line . slice ( 3 ) ;
80- const arrow = p . indexOf ( ' -> ' ) ;
81- if ( arrow !== - 1 ) p = p . slice ( arrow + 4 ) ;
82- p = p . replace ( / ^ " | " $ / g, '' ) ;
83- if ( p ) out . push ( p ) ;
34+ function collect ( cwd : string , base : string | undefined , ext ?: string ) : ChangedFilesResult {
35+ if ( ! isRepo ( cwd ) ) {
36+ throw new Error ( `--changed needs a git repository; ${ cwd } is not inside one` ) ;
8437 }
85- return out ;
38+ const result = gitChangedFiles ( { cwd, base, ext } ) ;
39+ return { files : result . paths , base : result . base , mergeBase : result . mergeBase } ;
8640}
8741
8842/**
8943 * Collect the files that differ from `base` (via `git merge-base`) plus any
90- * uncommitted/untracked working-tree changes. Falls back to `git diff HEAD`
91- * when no base is resolvable or no merge base exists — a shallow clone or a
92- * detached CI checkout — rather than failing the run.
44+ * uncommitted/untracked working-tree changes.
9345 */
9446export function changedFiles ( options : { cwd ?: string ; base ?: string } = { } ) : ChangedFilesResult {
95- const cwd = options . cwd ?? process . cwd ( ) ;
96- if ( ! tryGit ( [ 'rev-parse' , '--git-dir' ] , cwd ) ) {
97- throw new Error ( `--changed needs a git repository; ${ cwd } is not inside one` ) ;
98- }
99-
100- const files = new Set < string > ( workingTreePaths ( cwd ) ) ;
101- const base = resolveChangedBase ( options . base , cwd ) ;
102- let mergeBase : string | undefined ;
103-
104- if ( base ) {
105- const found = tryGit ( [ 'merge-base' , 'HEAD' , base ] , cwd ) ;
106- mergeBase = found ?. trim ( ) || undefined ;
107- }
108- // No base, or no common ancestor (shallow clone / detached checkout): the
109- // uncommitted diff against HEAD is all the history we can see.
110- const diffArgs = mergeBase
111- ? [ 'diff' , '--name-only' , '--diff-filter=ACMR' , mergeBase , 'HEAD' ]
112- : [ 'diff' , '--name-only' , '--diff-filter=ACMR' , 'HEAD' ] ;
113- for ( const rawLine of ( tryGit ( diffArgs , cwd ) ?? '' ) . split ( '\n' ) ) {
114- const p = rawLine . trim ( ) ;
115- if ( p ) files . add ( p ) ;
116- }
117-
118- const abs : string [ ] = [ ] ;
119- for ( const rel of files ) {
120- const full = path . resolve ( cwd , rel ) ;
121- // Deleted or renamed-away paths have nothing left to lint.
122- if ( existsSync ( full ) && statSync ( full ) . isFile ( ) ) abs . push ( full ) ;
123- }
124- return { files : abs . sort ( ) , base, mergeBase } ;
47+ return collect ( options . cwd ?? process . cwd ( ) , options . base ) ;
12548}
12649
12750/** {@link changedFiles }, narrowed to `.sql`. */
12851export function changedSqlFiles ( options : { cwd ?: string ; base ?: string } = { } ) : ChangedFilesResult {
129- const result = changedFiles ( options ) ;
130- return { ...result , files : result . files . filter ( ( f ) => f . toLowerCase ( ) . endsWith ( '.sql' ) ) } ;
52+ return collect ( options . cwd ?? process . cwd ( ) , options . base , '.sql' ) ;
13153}
0 commit comments