Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions Herebyfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,65 @@ function baselineAcceptTask(localBaseline, refBaseline) {
return path.join(refBaseline, relative);
}

/**
* Recursively find top-most empty subtrees under root, excluding root itself.
* Returns the highest ancestors whose entire subtrees contain only empty
* directories, so a single rimraf can remove each whole subtree at once.
* @param {string} root
* @returns {string[]}
*/
function findEmptySubtrees(root) {
/** @type {string[]} */
const result = [];

/**
* @param {string} dir
* @returns {boolean} true if dir is entirely empty
*/
function visit(dir) {
/** @type {import("fs").Dirent[]} */
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
}
catch {
return false;
}

let empty = true;
/** @type {string[]} */
const childEmpty = [];
for (const entry of entries) {
if (entry.isDirectory()) {
const childPath = path.join(dir, entry.name);
if (visit(childPath)) {
childEmpty.push(childPath);
}
else {
empty = false;
}
}
else {
empty = false;
}
}

if (!empty) {
// This dir is not fully empty, so collect the empty children
// we found (they are top-most empty subtrees).
result.push(...childEmpty);
}
// If empty, don't collect children — the caller will either
// collect this dir or propagate emptiness further up.
return empty;
}

// If root itself is entirely empty, there's nothing to collect
// (we exclude root). Otherwise, visit already pushed the right subtrees.
visit(root);
Comment on lines +908 to +910
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findEmptySubtrees() doesn’t currently handle the case where root’s entire subtree is empty-but-nontrivial (i.e. root contains only empty directories). In that scenario visit(root) returns true and the function returns [], so no empty directories are removed even though there are empty trees under root (contradicting the doc comment / PR goal). Consider: if visit(root) returns true, collect and return root’s direct child directories (or otherwise ensure empty subtrees under an empty root are still returned), while still not deleting root itself.

Suggested change
// If root itself is entirely empty, there's nothing to collect
// (we exclude root). Otherwise, visit already pushed the right subtrees.
visit(root);
// If root itself is entirely empty, we still want to collect the
// top-most empty subtrees under it (its direct child directories),
// while excluding root itself. Otherwise, visit already pushed
// the right subtrees into result.
const rootIsEmpty = visit(root);
if (rootIsEmpty) {
/** @type {import("fs").Dirent[]} */
let entries;
try {
entries = fs.readdirSync(root, { withFileTypes: true });
}
catch {
// If we can't read root at this point, just return whatever
// was already collected.
return result;
}
for (const entry of entries) {
if (entry.isDirectory()) {
result.push(path.join(root, entry.name));
}
}
}

Copilot uses AI. Check for mistakes.
return result;
}

return async () => {
const toCopy = await glob(`${localBaseline}/**`, { nodir: true, ignore: `${localBaseline}/**/*.delete` });
for (const p of toCopy) {
Expand All @@ -865,6 +924,14 @@ function baselineAcceptTask(localBaseline, refBaseline) {
await rimraf(out);
await rimraf(p); // also delete the .delete file so that it no longer shows up in a diff tool.
}

// Remove empty directory trees left behind by deletions.
for (const dir of findEmptySubtrees(refBaseline)) {
await rimraf(dir);
}
for (const dir of findEmptySubtrees(localBaseline)) {
await rimraf(dir);
}
};
}

Expand Down