@@ -31,10 +31,12 @@ async function cleanupEmptyExerciseDirectories(cwd: string) {
3131
3232 let deletedCount = 0
3333
34+ // Determine which directories contain any files (tracked or untracked), and
35+ // which are "fileless" (contain only subdirectories or are empty).
36+ const hasFilesMap = new Map < string , boolean > ( )
3437 for ( const dir of directories ) {
3538 if ( dir === 'exercises' ) continue // Skip the root exercises directory
3639
37- // Check if directory has any files (excluding gitignored files)
3840 const [ trackedFiles , untrackedFiles ] = await Promise . all ( [
3941 execa ( 'git' , [ 'ls-files' , dir ] , { cwd, reject : false } ) . catch ( ( ) => ( {
4042 stdout : '' ,
@@ -45,26 +47,43 @@ async function cleanupEmptyExerciseDirectories(cwd: string) {
4547 } ) . catch ( ( ) => ( { stdout : '' } ) ) ,
4648 ] )
4749
48- // Fix: Use proper boolean logic to check if both outputs are empty
4950 const hasTrackedFiles = trackedFiles . stdout . trim ( ) . length > 0
5051 const hasUntrackedFiles = untrackedFiles . stdout . trim ( ) . length > 0
51- const hasFiles = hasTrackedFiles || hasUntrackedFiles
52-
53- if ( ! hasFiles ) {
54- console . log ( ` Deleting empty directory: ${ dir } ` )
55- try {
56- // Use fs.rmdir instead of shell command to avoid shell injection
57- await fs . rmdir ( path . join ( cwd , dir ) )
58- deletedCount ++
59- } catch {
60- // Directory might not be empty or might not exist, which is fine
61- // We'll just continue with the next directory
62- }
52+ hasFilesMap . set ( dir , hasTrackedFiles || hasUntrackedFiles )
53+ }
54+
55+ // Build a set of directories that have no files anywhere in their subtree
56+ const emptyDirs = directories . filter (
57+ ( dir ) => dir !== 'exercises' && hasFilesMap . get ( dir ) === false ,
58+ )
59+ const emptySet = new Set ( emptyDirs )
60+
61+ // From the empty directories, pick only the top-most ones (those that do not
62+ // have an ancestor also in the empty set). Deleting these recursively is
63+ // faster and removes entire empty trees in one go.
64+ const topLevelEmptyDirs = emptyDirs . filter ( ( dir ) => {
65+ let parent = path . posix . dirname ( dir )
66+ while ( parent && parent !== '.' && parent !== 'exercises' ) {
67+ if ( emptySet . has ( parent ) ) return false
68+ parent = path . posix . dirname ( parent )
69+ }
70+ return true
71+ } )
72+
73+ for ( const dir of topLevelEmptyDirs ) {
74+ console . log ( ` Deleting empty directory tree: ${ dir } ` )
75+ try {
76+ // Recursively remove the directory tree. This is safe because we've
77+ // confirmed there are no files (tracked or untracked) anywhere within.
78+ await fs . rm ( path . join ( cwd , dir ) , { recursive : true , force : true } )
79+ deletedCount ++
80+ } catch {
81+ // Directory might not exist due to race conditions; continue.
6382 }
6483 }
6584
6685 if ( deletedCount > 0 ) {
67- console . log ( ` Deleted ${ deletedCount } empty directories .` )
86+ console . log ( ` Deleted ${ deletedCount } empty directory tree(s) .` )
6887 } else {
6988 console . log ( ' No empty directories found.' )
7089 }
0 commit comments