From 62eca5926d3b59b4fa327bca703c984efc699086 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 10:28:31 +0000 Subject: [PATCH] fix(cli): fold per-file 'Removed:' logs into 'Removed directory: X' when the directory is absent from the base branch --- script/upstream/merge.ts | 10 ++- script/upstream/transforms/skip-files.ts | 109 +++++++++++++++++++---- 2 files changed, 100 insertions(+), 19 deletions(-) diff --git a/script/upstream/merge.ts b/script/upstream/merge.ts index 02933816c1f..ce347e5b722 100644 --- a/script/upstream/merge.ts +++ b/script/upstream/merge.ts @@ -426,7 +426,15 @@ async function main() { logger.step(6, 8, "Applying transformations to opencode branch (pre-merge)...") logger.info("Removing files skipped in Kilo...") - const skips = await skipFiles({ dryRun: false, verbose: options.verbose, force: true }) + // Pre-merge skip runs on the opencode branch, so HEAD points at upstream. + // Pass the base branch explicitly so the "directory missing in base" + // grouping folds removals under directories Kilo doesn't have. + const skips = await skipFiles({ + dryRun: false, + verbose: options.verbose, + force: true, + baseRef: config.baseBranch, + }) const count = skips.filter((r) => r.action === "removed").length if (count > 0) { logger.success(`Removed ${count} skipped file(s) from opencode branch`) diff --git a/script/upstream/transforms/skip-files.ts b/script/upstream/transforms/skip-files.ts index c41a07a4ee7..f2dd4d96fe2 100644 --- a/script/upstream/transforms/skip-files.ts +++ b/script/upstream/transforms/skip-files.ts @@ -26,6 +26,13 @@ export interface SkipOptions { verbose?: boolean patterns?: string[] force?: boolean + /** + * Ref to treat as "the main repo we're merging into" when deciding whether + * a removed file's parent directory is absent from the base. Defaults to + * "HEAD", which is correct post-merge (HEAD = ours), but pre-merge on the + * opencode branch the caller should pass the actual base branch. + */ + baseRef?: string } /** @@ -82,13 +89,61 @@ async function getTrackedFiles(): Promise { } /** - * Check if a file exists in a specific git ref + * Check if a path (file or directory) exists in a specific git ref. + * Works for trees too — `git cat-file -e :` returns 0 for any + * object the ref can resolve, including directory trees. */ -async function fileExistsInRef(file: string, ref: string): Promise { - const result = await $`git cat-file -e ${ref}:${file}`.quiet().nothrow() +async function pathExistsInRef(path: string, ref: string): Promise { + const result = await $`git cat-file -e ${ref}:${path}`.quiet().nothrow() return result.exitCode === 0 } +/** + * Group successfully-removed files by their highest ancestor directory that + * is missing from `baseRef`. Files whose every ancestor exists in the base + * are returned as "singles" and logged individually. + */ +async function groupByMissingDir( + files: string[], + baseRef: string, +): Promise<{ dirs: Map; singles: string[] }> { + const cache = new Map() + async function exists(dir: string): Promise { + const hit = cache.get(dir) + if (hit !== undefined) return hit + const ok = await pathExistsInRef(dir, baseRef) + cache.set(dir, ok) + return ok + } + const dirs = new Map() + const singles: string[] = [] + for (const file of files) { + const parts = file.split("/") + let top: string | null = null + for (let i = 1; i < parts.length; i++) { + const dir = parts.slice(0, i).join("/") + if (!(await exists(dir))) { + top = dir + break + } + } + if (top) dirs.set(top, (dirs.get(top) ?? 0) + 1) + else singles.push(file) + } + return { dirs, singles } +} + +function logRemovals(dirs: Map, singles: string[], dryRun: boolean): void { + const prefix = dryRun ? "[DRY-RUN] Would remove" : "Removed" + const log = dryRun ? info : success + for (const [dir, n] of dirs) { + log(`${prefix} directory: ${dir} (${n} file${n === 1 ? "" : "s"})`) + } + for (const file of singles) { + log(`${prefix}: ${file}`) + } +} + /** * Remove a file from the merge (git rm). Retries once on failure since * transient index contention (editor watchers, rerere passes) has been @@ -116,6 +171,8 @@ async function removeFile(file: string): Promise<{ ok: boolean; err?: string }> export async function skipFiles(options: SkipOptions = {}): Promise { const results: SkipResult[] = [] const patterns = options.patterns || defaultConfig.skipFiles + const baseRef = options.baseRef ?? "HEAD" + const dryRun = options.dryRun ?? false if (!patterns || patterns.length === 0) { info("No skip patterns configured") @@ -135,34 +192,50 @@ export async function skipFiles(options: SkipOptions = {}): Promise