From b27e98ed51d1df0a97a6fe7a954b6ce6522e946d Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:36:34 -0400 Subject: [PATCH] fix(pr-review-cli): show every changed file's text in the full diff The full-diff step ran git diff in the head checkout, so a pull request that adds a .gitattributes marking a path -diff or binary, or puts a NUL byte in a text file, turned that file into "Binary files differ" and hid its text from the reviewer. Both model jobs now write the diff with --text --no-textconv (plus the existing --no-ext-diff). A test runs the step against a repository whose head hides three files those three ways and requires each file's text in the written diff. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/pr-review-cli.test.cjs | 70 ++++++++++++++++++++++++++ .github/workflows/pr-review-cli.yml | 8 ++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/.github/scripts/pr-review-cli.test.cjs b/.github/scripts/pr-review-cli.test.cjs index cd8c56f8..ce077c6e 100644 --- a/.github/scripts/pr-review-cli.test.cjs +++ b/.github/scripts/pr-review-cli.test.cjs @@ -1105,3 +1105,73 @@ test("the review leaves each leg as an artifact named for the run, head and leg, /needs\.[\w-]+\.outputs\.(review|last-summary)(?![\w-])/u, ); }); + +test("the full diff shows text the head marks -diff or binary, or hides behind a NUL byte, in both lanes", () => { + const { spawnSync } = require("node:child_process"); + const git = (cwd, ...args) => { + const result = spawnSync("git", args, { cwd, encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); + }; + for (const job of [key, subscription]) { + const dir = makeTemporaryDirectory("pr-review-cli-full-diff-"); + const origin = path.join(dir, "origin"); + fs.mkdirSync(origin); + git(origin, "init", "--quiet", "--initial-branch=main"); + git(origin, "config", "user.email", "t@example.com"); + git(origin, "config", "user.name", "t"); + fs.writeFileSync(path.join(origin, "README"), "base\n"); + git(origin, "add", "."); + git(origin, "commit", "--quiet", "-m", "base"); + const base = git(origin, "rev-parse", "HEAD"); + fs.writeFileSync( + path.join(origin, ".gitattributes"), + "hidden.sh -diff\nopaque.sh binary\n", + ); + fs.writeFileSync(path.join(origin, "hidden.sh"), "echo NO-DIFF-PAYLOAD\n"); + fs.writeFileSync(path.join(origin, "opaque.sh"), "echo BINARY-PAYLOAD\n"); + fs.writeFileSync(path.join(origin, "nul.sh"), "\0\necho NUL-PAYLOAD\n"); + git(origin, "add", "."); + git(origin, "commit", "--quiet", "-m", "head"); + const head = git(origin, "rev-parse", "HEAD"); + const work = path.join(dir, "work"); + git(dir, "clone", "--quiet", origin, work); + const fullDiff = step(job, "full-diff"); + const result = spawnSync( + "bash", + ["-e", "-o", "pipefail", "-c", fullDiff.run], + { + cwd: work, + encoding: "utf8", + env: { + PATH: process.env.PATH, + HOME: dir, + GH_TOKEN: "test-token", + GITHUB_SERVER_URL: "https://github.invalid", + GITHUB_OUTPUT: path.join(dir, "output"), + REPO: "o/r", + BASE_SHA: base, + HEAD_SHA: head, + MERGE_BASE: base, + DIFF_FILE: fullDiff.env.DIFF_FILE, + }, + }, + ); + assert.equal(result.status, 0, result.stderr); + const diff = fs.readFileSync( + path.join(work, fullDiff.env.DIFF_FILE), + "utf8", + ); + for (const payload of [ + "NO-DIFF-PAYLOAD", + "BINARY-PAYLOAD", + "NUL-PAYLOAD", + ]) { + assert.match(diff, new RegExp(`^\\+echo ${payload}$`, "mu"), payload); + } + assert.match( + fs.readFileSync(path.join(dir, "output"), "utf8"), + /^written=true$/mu, + ); + } +}); diff --git a/.github/workflows/pr-review-cli.yml b/.github/workflows/pr-review-cli.yml index 43adacb9..ddb12023 100644 --- a/.github/workflows/pr-review-cli.yml +++ b/.github/workflows/pr-review-cli.yml @@ -802,7 +802,9 @@ jobs: export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="http.$GITHUB_SERVER_URL/.extraheader" GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $basic" git fetch --quiet --no-tags --depth=1 --filter=blob:none origin "$base" mkdir -p "$(dirname "$DIFF_FILE")" - git diff --no-color --no-ext-diff "$base" "$HEAD_SHA" > "$DIFF_FILE" + # --text: a head .gitattributes (-diff, binary) or a NUL byte must not + # turn a changed file into "Binary files differ" the model cannot read. + git diff --no-color --no-ext-diff --no-textconv --text "$base" "$HEAD_SHA" > "$DIFF_FILE" files="$(git diff --name-only "$base" "$HEAD_SHA" | wc -l | tr -d ' ')" { echo "written=true" @@ -1887,7 +1889,9 @@ jobs: export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="http.$GITHUB_SERVER_URL/.extraheader" GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $basic" git fetch --quiet --no-tags --depth=1 --filter=blob:none origin "$base" mkdir -p "$(dirname "$DIFF_FILE")" - git diff --no-color --no-ext-diff "$base" "$HEAD_SHA" > "$DIFF_FILE" + # --text: a head .gitattributes (-diff, binary) or a NUL byte must not + # turn a changed file into "Binary files differ" the model cannot read. + git diff --no-color --no-ext-diff --no-textconv --text "$base" "$HEAD_SHA" > "$DIFF_FILE" files="$(git diff --name-only "$base" "$HEAD_SHA" | wc -l | tr -d ' ')" { echo "written=true"