Skip to content

fix: guard checkDiffFile against mode-only diffs with empty OrigName/NewName#221

Open
ktersius wants to merge 2 commits into
launchdarkly:mainfrom
ktersius:fix/mode-only-diff-panic
Open

fix: guard checkDiffFile against mode-only diffs with empty OrigName/NewName#221
ktersius wants to merge 2 commits into
launchdarkly:mainfrom
ktersius:fix/mode-only-diff-panic

Conversation

@ktersius

@ktersius ktersius commented Jun 18, 2026

Copy link
Copy Markdown

Problem

The action panics with index out of range [1] with length 1 at diff/diff.go:44 when a pull request contains a file whose only change is a permission/mode bit (no content diff). The bug is present in both v2.0.0 and v2.1.0.

Root cause

checkDiffFile splits OrigName and NewName on / and unconditionally accesses [1]:

parsedFileA := strings.SplitN(parsedDiff.OrigName, "/", 2)
parsedFileB := strings.SplitN(parsedDiff.NewName, "/", 2)
fullPathToA := workspace + "/" + parsedFileA[1]   // line 44 — PANICS
fullPathToB := workspace + "/" + parsedFileB[1]

For a standard git diff, file names always carry an a/ or b/ prefix (or are /dev/null), so SplitN returns two elements. Mode-only diffs have no --- / +++ headers at all. The sourcegraph/go-diff parser therefore returns OrigName = "" and NewName = "" for those entries:

strings.SplitN("", "/", 2)  // [""] — length 1, not 2

Accessing [1] on a single-element slice is a runtime panic.

How to reproduce

Open a PR containing a file whose only change is the execute bit:

git checkout -b repro/mode-change
chmod -x some-file.yaml        # was 100755, becomes 100644
git add some-file.yaml
git commit -m "chore: remove execute bit"
git push origin repro/mode-change
# open PR — the action panics during "Preprocessing diffs..."

The diff block that triggers it (no --- / +++ lines):

diff --git a/some-file.yaml b/some-file.yaml
old mode 100755
new mode 100644

The resulting panic:

panic: runtime error: index out of range [1] with length 1

goroutine 1 [running]:
github.com/launchdarkly/find-code-references-in-pull-request/diff.checkDiffFile(...)
        /app/diff/diff.go:44 +0x46a
github.com/launchdarkly/find-code-references-in-pull-request/diff.PreprocessDiffs(...)
        /app/diff/diff.go:21 +0x9d
main.main()
        /app/main.go:60 +0x4c5

Note: the stat: no such file or directory line printed just before the panic is from a separate, preceding file processed normally — it is not the file that causes the crash.

Fix

Add a length guard before indexing. Mode-only and header-less diffs have no content to scan, so returning ignore = true is the correct behaviour:

if len(parsedFileA) < 2 || len(parsedFileB) < 2 {
    return "", true
}

🤖 This PR was created with the assistance of Claude Code

…NewName

Mode-only diffs (e.g. chmod changes) and some binary diffs produce no
--- / +++ file headers in the unified diff output. The sourcegraph/go-diff
parser therefore returns empty OrigName and NewName strings for those
FileDiff entries. checkDiffFile assumed at least one '/' was always present
(from the 'a/' or 'b/' prefix) and unconditionally accessed index [1] after
SplitN, causing a runtime panic:

  panic: runtime error: index out of range [1] with length 1
      diff/diff.go:44

Guard the slice length before indexing and skip entries with no file headers
— there is no content to scan in mode-only or header-less diffs.
@ktersius ktersius requested a review from a team as a code owner June 18, 2026 21:43
Add a table entry to Test_checkDiffFile covering the case where OrigName
and NewName are both empty strings — the shape go-diff produces for
mode-only diffs (chmod changes with no content diff).

Before the fix, calling checkDiffFile with empty names panicked:
  panic: runtime error: index out of range [1] with length 1
      diff/diff.go:44

After the fix the function returns ("", true), skipping the entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant