@@ -228,16 +228,36 @@ static void fl_add(file_list_t *fl, const char *abs_path, const char *rel_path,
228228
229229/* ── Recursive walk ──────────────────────────────────────────────── */
230230
231+ /* Compute path relative to a nested .gitignore's directory.
232+ * "webapp/src/foo.js" with prefix "webapp" → "src/foo.js". */
233+ static const char * local_rel_path (const char * rel_path , const char * local_prefix ) {
234+ if (!local_prefix || local_prefix [0 ] == '\0' ) {
235+ return rel_path ;
236+ }
237+ size_t prefix_len = strlen (local_prefix );
238+ if (strncmp (rel_path , local_prefix , prefix_len ) == 0 && rel_path [prefix_len ] == '/' ) {
239+ return rel_path + prefix_len + SKIP_ONE ;
240+ }
241+ return rel_path ;
242+ }
243+
231244/* Check if a directory entry should be skipped (hardcoded dirs + gitignore). */
232245static bool should_skip_directory (const char * entry_name , const char * rel_path ,
233246 const cbm_discover_opts_t * opts , const cbm_gitignore_t * gitignore ,
234- const cbm_gitignore_t * cbmignore ) {
247+ const cbm_gitignore_t * cbmignore , const cbm_gitignore_t * local_gi ,
248+ const char * local_gi_prefix ) {
235249 if (cbm_should_skip_dir (entry_name , opts ? opts -> mode : CBM_MODE_FULL )) {
236250 return true;
237251 }
238252 if (gitignore && cbm_gitignore_matches (gitignore , rel_path , true)) {
239253 return true;
240254 }
255+ if (local_gi ) {
256+ const char * lrel = local_rel_path (rel_path , local_gi_prefix );
257+ if (cbm_gitignore_matches (local_gi , lrel , true)) {
258+ return true;
259+ }
260+ }
241261 if (cbmignore && cbm_gitignore_matches (cbmignore , rel_path , true)) {
242262 return true;
243263 }
@@ -247,7 +267,8 @@ static bool should_skip_directory(const char *entry_name, const char *rel_path,
247267/* Check if a regular file should be skipped (filters + gitignore + size). */
248268static bool should_skip_file (const char * entry_name , const char * rel_path ,
249269 const cbm_discover_opts_t * opts , const cbm_gitignore_t * gitignore ,
250- const cbm_gitignore_t * cbmignore , off_t file_size ) {
270+ const cbm_gitignore_t * cbmignore , const cbm_gitignore_t * local_gi ,
271+ const char * local_gi_prefix , off_t file_size ) {
251272 cbm_index_mode_t mode = opts ? opts -> mode : CBM_MODE_FULL ;
252273 if (cbm_has_ignored_suffix (entry_name , mode )) {
253274 return true;
@@ -261,6 +282,12 @@ static bool should_skip_file(const char *entry_name, const char *rel_path,
261282 if (gitignore && cbm_gitignore_matches (gitignore , rel_path , false)) {
262283 return true;
263284 }
285+ if (local_gi ) {
286+ const char * lrel = local_rel_path (rel_path , local_gi_prefix );
287+ if (cbm_gitignore_matches (local_gi , lrel , false)) {
288+ return true;
289+ }
290+ }
264291 if (cbmignore && cbm_gitignore_matches (cbmignore , rel_path , false)) {
265292 return true;
266293 }
@@ -308,8 +335,10 @@ static int safe_stat(const char *abs_path, struct stat *st) {
308335/* Process a single regular file entry during directory walk. */
309336static void walk_dir_process_file (const char * abs_path , const char * rel_path , const char * name ,
310337 const cbm_discover_opts_t * opts , const cbm_gitignore_t * gitignore ,
311- const cbm_gitignore_t * cbmignore , off_t size , file_list_t * out ) {
312- if (should_skip_file (name , rel_path , opts , gitignore , cbmignore , size )) {
338+ const cbm_gitignore_t * cbmignore , const cbm_gitignore_t * local_gi ,
339+ const char * local_gi_prefix , off_t size , file_list_t * out ) {
340+ if (should_skip_file (name , rel_path , opts , gitignore , cbmignore , local_gi , local_gi_prefix ,
341+ size )) {
313342 return ;
314343 }
315344 CBMLanguage lang = detect_file_language (name , abs_path );
@@ -322,9 +351,38 @@ static void walk_dir_process_file(const char *abs_path, const char *rel_path, co
322351typedef struct {
323352 char dir [CBM_SZ_4K ];
324353 char prefix [CBM_SZ_4K ];
354+ cbm_gitignore_t * local_gi ; /* nested .gitignore for this subtree */
355+ char local_gi_prefix [CBM_SZ_4K ]; /* rel_prefix when local_gi was loaded */
325356} walk_frame_t ;
326357#define WALK_STACK_CAP 512
327358/* Build abs/rel paths and process one directory entry. */
359+ /* Try to load a nested .gitignore from this directory. Returns owned pointer or NULL. */
360+ static cbm_gitignore_t * try_load_nested_gitignore (const walk_frame_t * frame ) {
361+ if (frame -> local_gi || frame -> prefix [0 ] == '\0' ) {
362+ return NULL ;
363+ }
364+ char gi_path [CBM_SZ_4K ];
365+ snprintf (gi_path , sizeof (gi_path ), "%s/.gitignore" , frame -> dir );
366+ struct stat gi_st ;
367+ if (stat (gi_path , & gi_st ) == 0 && S_ISREG (gi_st .st_mode )) {
368+ return cbm_gitignore_load (gi_path );
369+ }
370+ return NULL ;
371+ }
372+
373+ /* Push a subdirectory onto the walk stack, inheriting local gitignore context. */
374+ static void walk_push_subdir (walk_frame_t * stack , int * top , const char * abs_path ,
375+ const char * rel_path , const walk_frame_t * parent ) {
376+ if (* top >= WALK_STACK_CAP ) {
377+ return ;
378+ }
379+ snprintf (stack [* top ].dir , CBM_SZ_4K , "%s" , abs_path );
380+ snprintf (stack [* top ].prefix , CBM_SZ_4K , "%s" , rel_path );
381+ stack [* top ].local_gi = parent -> local_gi ;
382+ snprintf (stack [* top ].local_gi_prefix , CBM_SZ_4K , "%s" , parent -> local_gi_prefix );
383+ (* top )++ ;
384+ }
385+
328386static void walk_dir_process_entry (cbm_dirent_t * entry , const walk_frame_t * frame ,
329387 const cbm_discover_opts_t * opts ,
330388 const cbm_gitignore_t * gitignore ,
@@ -345,33 +403,47 @@ static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *fram
345403 }
346404
347405 if (S_ISDIR (st .st_mode )) {
348- if (!should_skip_directory (entry -> name , rel_path , opts , gitignore , cbmignore )) {
349- if (* top < WALK_STACK_CAP ) {
350- snprintf (stack [* top ].dir , CBM_SZ_4K , "%s" , abs_path );
351- snprintf (stack [* top ].prefix , CBM_SZ_4K , "%s" , rel_path );
352- (* top )++ ;
353- }
406+ if (!should_skip_directory (entry -> name , rel_path , opts , gitignore , cbmignore ,
407+ frame -> local_gi , frame -> local_gi_prefix )) {
408+ walk_push_subdir (stack , top , abs_path , rel_path , frame );
354409 }
355410 } else if (S_ISREG (st .st_mode )) {
356411 walk_dir_process_file (abs_path , rel_path , entry -> name , opts , gitignore , cbmignore ,
357- st .st_size , out );
412+ frame -> local_gi , frame -> local_gi_prefix , st .st_size , out );
358413 }
359414}
360415
416+ enum { GI_OWNED_CAP = 64 };
417+
361418static void walk_dir (const char * dir_path , const char * rel_prefix , const cbm_discover_opts_t * opts ,
362419 const cbm_gitignore_t * gitignore , const cbm_gitignore_t * cbmignore ,
363420 file_list_t * out ) {
364421 walk_frame_t * stack = calloc (WALK_STACK_CAP , sizeof (walk_frame_t ));
365422 if (!stack ) {
366423 return ;
367424 }
425+ /* Collect all owned gitignores — freed at the end because child frames
426+ * on the stack hold borrowed pointers to them. */
427+ cbm_gitignore_t * owned_gis [GI_OWNED_CAP ];
428+ int owned_count = 0 ;
429+
368430 int top = 0 ;
369431 snprintf (stack [top ].dir , CBM_SZ_4K , "%s" , dir_path );
370432 snprintf (stack [top ].prefix , CBM_SZ_4K , "%s" , rel_prefix );
371433 top ++ ;
372434
373435 while (top > 0 ) {
374436 walk_frame_t frame = stack [-- top ];
437+
438+ cbm_gitignore_t * loaded = try_load_nested_gitignore (& frame );
439+ if (loaded ) {
440+ frame .local_gi = loaded ;
441+ snprintf (frame .local_gi_prefix , sizeof (frame .local_gi_prefix ), "%s" , frame .prefix );
442+ if (owned_count < GI_OWNED_CAP ) {
443+ owned_gis [owned_count ++ ] = loaded ;
444+ }
445+ }
446+
375447 cbm_dir_t * d = cbm_opendir (frame .dir );
376448 if (!d ) {
377449 continue ;
@@ -383,6 +455,9 @@ static void walk_dir(const char *dir_path, const char *rel_prefix, const cbm_dis
383455 }
384456 cbm_closedir (d );
385457 }
458+ for (int i = 0 ; i < owned_count ; i ++ ) {
459+ cbm_gitignore_free (owned_gis [i ]);
460+ }
386461 free (stack );
387462}
388463
0 commit comments