diff --git a/plugins/codex/scripts/lib/git.mjs b/plugins/codex/scripts/lib/git.mjs index 1c0529a..dbf5f38 100644 --- a/plugins/codex/scripts/lib/git.mjs +++ b/plugins/codex/scripts/lib/git.mjs @@ -133,9 +133,25 @@ function formatSection(title, body) { return [`## ${title}`, "", body.trim() ? body.trim() : "(none)", ""].join("\n"); } +function listUntrackedFiles(cwd, relativePath) { + const files = gitChecked(cwd, ["ls-files", "--others", "--exclude-standard", relativePath]).stdout.trim().split("\n").filter(Boolean); + if (files.length === 1 && files[0] === relativePath) { + const absolutePath = path.join(cwd, relativePath); + return gitChecked(absolutePath, ["ls-files", "--others", "--exclude-standard"]).stdout.trim().split("\n").filter(Boolean).map((file) => path.join(relativePath, file)); + } + return files; +} + function formatUntrackedFile(cwd, relativePath) { const absolutePath = path.join(cwd, relativePath); - const stat = fs.statSync(absolutePath); + const lstat = fs.lstatSync(absolutePath); + if (lstat.isSymbolicLink() && fs.statSync(absolutePath).isDirectory()) { + return `### ${relativePath}\n(skipped: symlinked directory)`; + } + if (lstat.isDirectory()) { + return listUntrackedFiles(cwd, relativePath).map((file) => formatUntrackedFile(cwd, file)).join("\n\n"); + } + const stat = lstat.isSymbolicLink() ? fs.statSync(absolutePath) : lstat; if (stat.size > MAX_UNTRACKED_BYTES) { return `### ${relativePath}\n(skipped: ${stat.size} bytes exceeds ${MAX_UNTRACKED_BYTES} byte limit)`; } diff --git a/tests/git.test.mjs b/tests/git.test.mjs index 7ea1a04..372bcb3 100644 --- a/tests/git.test.mjs +++ b/tests/git.test.mjs @@ -55,6 +55,27 @@ test("resolveReviewTarget honors explicit base overrides", () => { assert.equal(target.baseRef, "main"); }); +test("collectReviewContext includes files inside untracked directories", () => { + const cwd = makeTempDir(); + initGitRepo(cwd); + fs.writeFileSync(path.join(cwd, "app.js"), "console.log(1);\n"); + run("git", ["add", "app.js"], { cwd }); + run("git", ["commit", "-m", "init"], { cwd }); + + fs.mkdirSync(path.join(cwd, "newdir", "sub"), { recursive: true }); + fs.writeFileSync(path.join(cwd, "newdir", "a.txt"), "hello a"); + fs.writeFileSync(path.join(cwd, "newdir", "sub", "b.txt"), "hello b"); + + const target = resolveReviewTarget(cwd, {}); + const ctx = collectReviewContext(cwd, target); + + assert.equal(ctx.mode, "working-tree"); + assert.match(ctx.content, /newdir\/a\.txt/); + assert.match(ctx.content, /newdir\/sub\/b\.txt/); + assert.match(ctx.content, /hello a/); + assert.match(ctx.content, /hello b/); +}); + test("resolveReviewTarget requires an explicit base when no default branch can be inferred", () => { const cwd = makeTempDir(); initGitRepo(cwd);