@@ -19,6 +19,12 @@ interface IgnoreRules {
1919 readonly relativePaths : ReadonlySet < string > ;
2020}
2121
22+ interface ScanState {
23+ readonly root : string ;
24+ readonly ignoreRules : IgnoreRules ;
25+ readonly useGitCheckIgnore : boolean ;
26+ }
27+
2228const internalDirectoryNames = new Set ( [ ".git" , ".jj" ] ) ;
2329
2430const emptyDiff = ( ) : VcsDiff => ( {
@@ -109,6 +115,21 @@ const shouldSkipDir = (name: string, relativePath: string, ignoreRules: IgnoreRu
109115 ignoreRules . directoryNames . has ( name ) ||
110116 ignoreRules . relativePaths . has ( toPortablePath ( relativePath ) ) ;
111117
118+ const ignoredPathsByFallback = (
119+ relativePaths : ReadonlyArray < string > ,
120+ ignoreRules : IgnoreRules ,
121+ ) : Set < string > => {
122+ const ignored = new Set < string > ( ) ;
123+ for ( const relativePath of relativePaths ) {
124+ const normalized = toPortablePath ( relativePath ) ;
125+ const name = normalized . split ( "/" ) . at ( - 1 ) ?? normalized ;
126+ if ( shouldSkipDir ( name , normalized , ignoreRules ) ) {
127+ ignored . add ( normalized ) ;
128+ }
129+ }
130+ return ignored ;
131+ } ;
132+
112133const makeRunProcess =
113134 ( spawner : ChildProcessSpawner [ "Service" ] ) =>
114135 ( options : RunProcessOptions ) : Effect . Effect < ProcessResult , ProcessExecutionError > =>
@@ -219,22 +240,63 @@ export const GitClientLive = Layer.effect(
219240 } satisfies IgnoreRules ;
220241 } ) ;
221242
222- const listTopLevelDirs = Effect . fn ( function * ( root : string ) {
243+ const ignoredPaths = Effect . fn ( function * (
244+ state : ScanState ,
245+ relativePaths : ReadonlyArray < string > ,
246+ ) {
247+ if ( relativePaths . length === 0 ) {
248+ return new Set < string > ( ) ;
249+ }
250+ const fallbackIgnored = ignoredPathsByFallback ( relativePaths , state . ignoreRules ) ;
251+ if ( ! state . useGitCheckIgnore ) {
252+ return fallbackIgnored ;
253+ }
254+
255+ const input = relativePaths . map ( toPortablePath ) . join ( "\n" ) ;
256+ const result = yield * run ( {
257+ command : "git" ,
258+ args : [ "check-ignore" , "--stdin" ] ,
259+ cwd : state . root ,
260+ stdin : input . length > 0 ? `${ input } \n` : "" ,
261+ allowFailure : true ,
262+ } ) ;
263+
264+ if ( result . exitCode === 0 || result . exitCode === 1 ) {
265+ return new Set ( [ ...fallbackIgnored , ...normalizeLines ( result . stdout ) . map ( toPortablePath ) ] ) ;
266+ }
267+
268+ return fallbackIgnored ;
269+ } ) ;
270+
271+ const buildScanState = Effect . fn ( function * ( root : string ) {
223272 const ignoreRules = yield * loadIgnoreRules ( root ) ;
224- const entries = yield * fs
225- . readDirectory ( root )
226- . pipe ( Effect . mapError ( ( cause ) => processError ( "readDirectory" , cause ) ) ) ;
227- const dirs = yield * Effect . filter ( entries , ( entry ) => {
228- if ( entry . startsWith ( "." ) || shouldSkipDir ( entry , entry , ignoreRules ) ) {
229- return Effect . succeed ( false ) ;
230- }
231- return fs . stat ( path . join ( root , entry ) ) . pipe (
232- Effect . map ( ( info ) => info . type === "Directory" ) ,
233- Effect . catch ( ( ) => Effect . succeed ( false ) ) ,
273+ return {
274+ root,
275+ ignoreRules,
276+ useGitCheckIgnore : true ,
277+ } satisfies ScanState ;
278+ } ) ;
279+
280+ const listTopLevelDirs : ( root : string ) => Effect . Effect < Array < string > , ProcessExecutionError > =
281+ Effect . fn ( function * ( root : string ) {
282+ const state = yield * buildScanState ( root ) ;
283+ const entries = yield * fs
284+ . readDirectory ( root )
285+ . pipe ( Effect . mapError ( ( cause ) => processError ( "readDirectory" , cause ) ) ) ;
286+ const maybeDirectoryEntries : Array < string | undefined > = yield * Effect . forEach (
287+ entries ,
288+ ( entry ) =>
289+ fs . stat ( path . join ( root , entry ) ) . pipe (
290+ Effect . map ( ( info ) => ( info . type === "Directory" ? entry : undefined ) ) ,
291+ Effect . catch ( ( ) => Effect . succeed ( undefined ) ) ,
292+ ) ,
234293 ) ;
294+ const directoryEntries = maybeDirectoryEntries . filter (
295+ ( entry ) : entry is string => entry != null ,
296+ ) ;
297+ const ignored = yield * ignoredPaths ( state , directoryEntries ) ;
298+ return directoryEntries . filter ( ( entry ) => ! ignored . has ( toPortablePath ( entry ) ) ) . sort ( ) ;
235299 } ) ;
236- return dirs . sort ( ) ;
237- } ) ;
238300
239301 const readGitLog = Effect . fn ( function * ( cwd : string , max : number ) {
240302 const result = yield * run ( {
@@ -500,64 +562,108 @@ export const JjClientLive = Layer.effect(
500562 } satisfies IgnoreRules ;
501563 } ) ;
502564
565+ const ignoredPaths = Effect . fn ( function * (
566+ state : ScanState ,
567+ relativePaths : ReadonlyArray < string > ,
568+ ) {
569+ if ( relativePaths . length === 0 ) {
570+ return new Set < string > ( ) ;
571+ }
572+ const fallbackIgnored = ignoredPathsByFallback ( relativePaths , state . ignoreRules ) ;
573+ if ( ! state . useGitCheckIgnore ) {
574+ return fallbackIgnored ;
575+ }
576+
577+ const input = relativePaths . map ( toPortablePath ) . join ( "\n" ) ;
578+ const result = yield * run ( {
579+ command : "git" ,
580+ args : [ "check-ignore" , "--stdin" ] ,
581+ cwd : state . root ,
582+ stdin : input . length > 0 ? `${ input } \n` : "" ,
583+ allowFailure : true ,
584+ } ) ;
585+
586+ if ( result . exitCode === 0 || result . exitCode === 1 ) {
587+ return new Set ( [ ...fallbackIgnored , ...normalizeLines ( result . stdout ) . map ( toPortablePath ) ] ) ;
588+ }
589+
590+ return fallbackIgnored ;
591+ } ) ;
592+
593+ const buildScanState = Effect . fn ( function * ( root : string ) {
594+ const ignoreRules = yield * loadIgnoreRules ( root ) ;
595+ const useGitCheckIgnore = yield * fs
596+ . exists ( path . join ( root , ".git" ) )
597+ . pipe ( Effect . mapError ( ( cause ) => processError ( "readIgnoreRules" , cause ) ) ) ;
598+ return {
599+ root,
600+ ignoreRules,
601+ useGitCheckIgnore,
602+ } satisfies ScanState ;
603+ } ) ;
604+
503605 const walkFiles : (
504- root : string ,
505- ignoreRules : IgnoreRules ,
606+ state : ScanState ,
506607 cwd ?: string ,
507608 ) => Effect . Effect < Array < string > , ProcessExecutionError > = Effect . fn ( function * (
508- root : string ,
509- ignoreRules : IgnoreRules ,
510- cwd = root ,
609+ state : ScanState ,
610+ cwd = state . root ,
511611 ) {
512612 const entries = yield * fs
513613 . readDirectory ( cwd )
514614 . pipe ( Effect . mapError ( ( cause ) => processError ( "walkFiles" , cause ) ) ) ;
515- const nested : Array < Array < string > > = yield * Effect . forEach ( entries , ( entry ) =>
615+ const scanned = yield * Effect . forEach ( entries , ( entry ) =>
516616 Effect . gen ( function * ( ) {
517- if ( entry . startsWith ( "." ) && ! [ ".gitignore" , ".env.example" , ".envrc" ] . includes ( entry ) ) {
518- const hiddenPath = path . join ( cwd , entry ) ;
519- const hiddenInfo = yield * fs
520- . stat ( hiddenPath )
521- . pipe ( Effect . mapError ( ( cause ) => processError ( "walkFiles" , cause ) ) ) ;
522- if ( hiddenInfo . type === "Directory" ) {
523- return [ ] as Array < string > ;
524- }
525- }
526-
527617 const fullPath = path . join ( cwd , entry ) ;
528618 const info = yield * fs
529619 . stat ( fullPath )
530620 . pipe ( Effect . mapError ( ( cause ) => processError ( "walkFiles" , cause ) ) ) ;
531-
532- if ( info . type === "Directory" ) {
533- if ( shouldSkipDir ( entry , path . relative ( root , fullPath ) , ignoreRules ) ) {
534- return [ ] as Array < string > ;
535- }
536- return yield * walkFiles ( root , ignoreRules , fullPath ) ;
621+ return {
622+ entry,
623+ fullPath,
624+ relativePath : toPortablePath ( path . relative ( state . root , fullPath ) ) ,
625+ info,
626+ } ;
627+ } ) ,
628+ ) ;
629+ const ignored = yield * ignoredPaths (
630+ state ,
631+ scanned . map ( ( item ) => item . relativePath ) ,
632+ ) ;
633+ const nested : Array < Array < string > > = yield * Effect . forEach ( scanned , ( item ) =>
634+ Effect . gen ( function * ( ) {
635+ if ( ignored . has ( item . relativePath ) ) {
636+ return [ ] as Array < string > ;
537637 }
538-
539- return [ path . relative ( root , fullPath ) ] ;
638+ if ( item . info . type === "Directory" ) {
639+ return yield * walkFiles ( state , item . fullPath ) ;
640+ }
641+ return [ item . relativePath ] ;
540642 } ) ,
541643 ) ;
542644 return nested . flat ( ) . sort ( ) ;
543645 } ) ;
544646
545- const listTopLevelDirs = Effect . fn ( function * ( root : string ) {
546- const ignoreRules = yield * loadIgnoreRules ( root ) ;
547- const entries = yield * fs
548- . readDirectory ( root )
549- . pipe ( Effect . mapError ( ( cause ) => processError ( "readDirectory" , cause ) ) ) ;
550- const dirs = yield * Effect . filter ( entries , ( entry ) => {
551- if ( entry . startsWith ( "." ) || shouldSkipDir ( entry , entry , ignoreRules ) ) {
552- return Effect . succeed ( false ) ;
553- }
554- return fs . stat ( path . join ( root , entry ) ) . pipe (
555- Effect . map ( ( info ) => info . type === "Directory" ) ,
556- Effect . catch ( ( ) => Effect . succeed ( false ) ) ,
647+ const listTopLevelDirs : ( root : string ) => Effect . Effect < Array < string > , ProcessExecutionError > =
648+ Effect . fn ( function * ( root : string ) {
649+ const state = yield * buildScanState ( root ) ;
650+ const entries = yield * fs
651+ . readDirectory ( root )
652+ . pipe ( Effect . mapError ( ( cause ) => processError ( "readDirectory" , cause ) ) ) ;
653+ const maybeDirectoryEntries : Array < string | undefined > = yield * Effect . forEach (
654+ entries ,
655+ ( entry ) =>
656+ fs . stat ( path . join ( root , entry ) ) . pipe (
657+ Effect . map ( ( info ) => ( info . type === "Directory" ? entry : undefined ) ) ,
658+ Effect . catch ( ( ) => Effect . succeed ( undefined ) ) ,
659+ ) ,
660+ ) ;
661+ const directoryEntries = maybeDirectoryEntries . filter (
662+ ( entry ) : entry is string => entry != null ,
557663 ) ;
664+ const ignored = yield * ignoredPaths ( state , directoryEntries ) ;
665+ return directoryEntries . filter ( ( entry ) => ! ignored . has ( toPortablePath ( entry ) ) ) . sort ( ) ;
558666 } ) ;
559- return dirs . sort ( ) ;
560- } ) ;
561667
562668 const readGitLog = Effect . fn ( function * ( cwd : string , max : number ) {
563669 const result = yield * run ( {
@@ -732,8 +838,8 @@ export const JjClientLive = Layer.effect(
732838 topLevelDirs : ( cwd ) => Effect . flatMap ( repoRoot ( cwd ) , listTopLevelDirs ) ,
733839 projectFiles : Effect . fn ( function * ( cwd : string ) {
734840 const root = yield * repoRoot ( cwd ) ;
735- const ignoreRules = yield * loadIgnoreRules ( root ) ;
736- const files = yield * walkFiles ( root , ignoreRules ) ;
841+ const state = yield * buildScanState ( root ) ;
842+ const files = yield * walkFiles ( state ) ;
737843 return files . slice ( 0 , 300 ) ;
738844 } ) ,
739845 } satisfies VcsClient ;
0 commit comments