@@ -90,12 +90,19 @@ export namespace Snapshot {
9090
9191 const args = ( cmd : string [ ] ) => [ "--git-dir" , state . gitdir , "--work-tree" , state . worktree , ...cmd ]
9292
93+ const enc = new TextEncoder ( )
94+ const feed = ( list : string [ ] ) => Stream . make ( enc . encode ( list . join ( "\0" ) + "\0" ) )
95+
9396 const git = Effect . fnUntraced (
94- function * ( cmd : string [ ] , opts ?: { cwd ?: string ; env ?: Record < string , string > } ) {
97+ function * (
98+ cmd : string [ ] ,
99+ opts ?: { cwd ?: string ; env ?: Record < string , string > ; stdin ?: ChildProcess . CommandInput } ,
100+ ) {
95101 const proc = ChildProcess . make ( "git" , cmd , {
96102 cwd : opts ?. cwd ,
97103 env : opts ?. env ,
98104 extendEnv : true ,
105+ stdin : opts ?. stdin ,
99106 } )
100107 const handle = yield * spawner . spawn ( proc )
101108 const [ text , stderr ] = yield * Effect . all (
@@ -115,6 +122,59 @@ export namespace Snapshot {
115122 ) ,
116123 )
117124
125+ const ignore = Effect . fnUntraced ( function * ( files : string [ ] ) {
126+ if ( ! files . length ) return new Set < string > ( )
127+ const check = yield * git (
128+ [
129+ ...quote ,
130+ "--git-dir" ,
131+ path . join ( state . worktree , ".git" ) ,
132+ "--work-tree" ,
133+ state . worktree ,
134+ "check-ignore" ,
135+ "--no-index" ,
136+ "--stdin" ,
137+ "-z" ,
138+ ] ,
139+ {
140+ cwd : state . directory ,
141+ stdin : feed ( files ) ,
142+ } ,
143+ )
144+ if ( check . code !== 0 && check . code !== 1 ) return new Set < string > ( )
145+ return new Set ( check . text . split ( "\0" ) . filter ( Boolean ) )
146+ } )
147+
148+ const drop = Effect . fnUntraced ( function * ( files : string [ ] ) {
149+ if ( ! files . length ) return
150+ yield * git (
151+ [
152+ ...cfg ,
153+ ...args ( [ "rm" , "--cached" , "-f" , "--ignore-unmatch" , "--pathspec-from-file=-" , "--pathspec-file-nul" ] ) ,
154+ ] ,
155+ {
156+ cwd : state . directory ,
157+ stdin : feed ( files ) ,
158+ } ,
159+ )
160+ } )
161+
162+ const stage = Effect . fnUntraced ( function * ( files : string [ ] ) {
163+ if ( ! files . length ) return
164+ const result = yield * git (
165+ [ ...cfg , ...args ( [ "add" , "--all" , "--sparse" , "--pathspec-from-file=-" , "--pathspec-file-nul" ] ) ] ,
166+ {
167+ cwd : state . directory ,
168+ stdin : feed ( files ) ,
169+ } ,
170+ )
171+ if ( result . code === 0 ) return
172+ log . warn ( "failed to add snapshot files" , {
173+ exitCode : result . code ,
174+ stderr : result . stderr ,
175+ } )
176+ } )
177+
118178 const exists = ( file : string ) => fs . exists ( file ) . pipe ( Effect . orDie )
119179 const read = ( file : string ) => fs . readFileString ( file ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( "" ) ) )
120180 const remove = ( file : string ) => fs . remove ( file ) . pipe ( Effect . catch ( ( ) => Effect . void ) )
@@ -176,60 +236,41 @@ export namespace Snapshot {
176236 const all = Array . from ( new Set ( [ ...tracked , ...untracked ] ) )
177237 if ( ! all . length ) return
178238
179- // Filter out files that are now gitignored even if previously tracked
180- // Files may have been tracked before being gitignored, so we need to check
181- // against the source project's current gitignore rules
182- // Use --no-index to check purely against patterns (ignoring whether file is tracked)
183- const checkArgs = [
184- ...quote ,
185- "--git-dir" ,
186- path . join ( state . worktree , ".git" ) ,
187- "--work-tree" ,
188- state . worktree ,
189- "check-ignore" ,
190- "--no-index" ,
191- "--" ,
192- ...all ,
193- ]
194- const check = yield * git ( checkArgs , { cwd : state . directory } )
195- const ignored =
196- check . code === 0 ? new Set ( check . text . trim ( ) . split ( "\n" ) . filter ( Boolean ) ) : new Set < string > ( )
197- const filtered = all . filter ( ( item ) => ! ignored . has ( item ) )
239+ // Resolve source-repo ignore rules against the exact candidate set.
240+ // --no-index keeps this pattern-based even when a path is already tracked.
241+ const ignored = yield * ignore ( all )
198242
199243 // Remove newly-ignored files from snapshot index to prevent re-adding
200244 if ( ignored . size > 0 ) {
201245 const ignoredFiles = Array . from ( ignored )
202246 log . info ( "removing gitignored files from snapshot" , { count : ignoredFiles . length } )
203- yield * git ( [ ...cfg , ...args ( [ "rm" , "--cached" , "-f" , "--" , ...ignoredFiles ] ) ] , {
204- cwd : state . directory ,
205- } )
247+ yield * drop ( ignoredFiles )
206248 }
207249
208- if ( ! filtered . length ) return
209-
210- const large = ( yield * Effect . all (
211- filtered . map ( ( item ) =>
212- fs
213- . stat ( path . join ( state . directory , item ) )
214- . pipe ( Effect . catch ( ( ) => Effect . void ) )
215- . pipe (
216- Effect . map ( ( stat ) => {
217- if ( ! stat || stat . type !== "File" ) return
218- const size = typeof stat . size === "bigint" ? Number ( stat . size ) : stat . size
219- return size > limit ? item : undefined
220- } ) ,
221- ) ,
222- ) ,
223- { concurrency : 8 } ,
224- ) ) . filter ( ( item ) : item is string => Boolean ( item ) )
225- yield * sync ( large )
226- const result = yield * git ( [ ...cfg , ...args ( [ "add" , "--sparse" , "." ] ) ] , { cwd : state . directory } )
227- if ( result . code !== 0 ) {
228- log . warn ( "failed to add snapshot files" , {
229- exitCode : result . code ,
230- stderr : result . stderr ,
231- } )
232- }
250+ const allow = all . filter ( ( item ) => ! ignored . has ( item ) )
251+ if ( ! allow . length ) return
252+
253+ const large = new Set (
254+ ( yield * Effect . all (
255+ allow . map ( ( item ) =>
256+ fs
257+ . stat ( path . join ( state . directory , item ) )
258+ . pipe ( Effect . catch ( ( ) => Effect . void ) )
259+ . pipe (
260+ Effect . map ( ( stat ) => {
261+ if ( ! stat || stat . type !== "File" ) return
262+ const size = typeof stat . size === "bigint" ? Number ( stat . size ) : stat . size
263+ return size > limit ? item : undefined
264+ } ) ,
265+ ) ,
266+ ) ,
267+ { concurrency : 8 } ,
268+ ) ) . filter ( ( item ) : item is string => Boolean ( item ) ) ,
269+ )
270+ const block = new Set ( untracked . filter ( ( item ) => large . has ( item ) ) )
271+ yield * sync ( Array . from ( block ) )
272+ // Stage only the allowed candidate paths so snapshot updates stay scoped.
273+ yield * stage ( allow . filter ( ( item ) => ! block . has ( item ) ) )
233274 } )
234275
235276 const cleanup = Effect . fnUntraced ( function * ( ) {
@@ -295,33 +336,14 @@ export namespace Snapshot {
295336 . map ( ( x ) => x . trim ( ) )
296337 . filter ( Boolean )
297338
298- // Filter out files that are now gitignored
299- if ( files . length > 0 ) {
300- const checkArgs = [
301- ...quote ,
302- "--git-dir" ,
303- path . join ( state . worktree , ".git" ) ,
304- "--work-tree" ,
305- state . worktree ,
306- "check-ignore" ,
307- "--no-index" ,
308- "--" ,
309- ...files ,
310- ]
311- const check = yield * git ( checkArgs , { cwd : state . directory } )
312- if ( check . code === 0 ) {
313- const ignored = new Set ( check . text . trim ( ) . split ( "\n" ) . filter ( Boolean ) )
314- const filtered = files . filter ( ( item ) => ! ignored . has ( item ) )
315- return {
316- hash,
317- files : filtered . map ( ( x ) => path . join ( state . worktree , x ) . replaceAll ( "\\" , "/" ) ) ,
318- }
319- }
320- }
339+ // Hide ignored-file removals from the user-facing patch output.
340+ const ignored = yield * ignore ( files )
321341
322342 return {
323343 hash,
324- files : files . map ( ( x ) => path . join ( state . worktree , x ) . replaceAll ( "\\" , "/" ) ) ,
344+ files : files
345+ . filter ( ( item ) => ! ignored . has ( item ) )
346+ . map ( ( x ) => path . join ( state . worktree , x ) . replaceAll ( "\\" , "/" ) ) ,
325347 }
326348 } ) ,
327349 )
@@ -672,27 +694,12 @@ export namespace Snapshot {
672694 ]
673695 } )
674696
675- // Filter out files that are now gitignored
676- if ( rows . length > 0 ) {
677- const files = rows . map ( ( r ) => r . file )
678- const checkArgs = [
679- ...quote ,
680- "--git-dir" ,
681- path . join ( state . worktree , ".git" ) ,
682- "--work-tree" ,
683- state . worktree ,
684- "check-ignore" ,
685- "--no-index" ,
686- "--" ,
687- ...files ,
688- ]
689- const check = yield * git ( checkArgs , { cwd : state . directory } )
690- if ( check . code === 0 ) {
691- const ignored = new Set ( check . text . trim ( ) . split ( "\n" ) . filter ( Boolean ) )
692- const filtered = rows . filter ( ( r ) => ! ignored . has ( r . file ) )
693- rows . length = 0
694- rows . push ( ...filtered )
695- }
697+ // Hide ignored-file removals from the user-facing diff output.
698+ const ignored = yield * ignore ( rows . map ( ( r ) => r . file ) )
699+ if ( ignored . size > 0 ) {
700+ const filtered = rows . filter ( ( r ) => ! ignored . has ( r . file ) )
701+ rows . length = 0
702+ rows . push ( ...filtered )
696703 }
697704
698705 const step = 100
0 commit comments