Skip to content

Patch() misreports paths containing " b/" and throws for the whole repo when any file name is C-quoted by git #120

Closed

Description

@matt-edmondson

What's wrong

GitPatchParser.ReadPathFromFileStart (GitIntegration/Parsing/GitPatchParser.cs:~191-206) finds the new-side path in a diff --git a/X b/Y header by taking whatever follows the last " b/". That fails in two ways.

1. Wrong path when the name contains b/

If you modify Plan b/notes.txt, git emits:

diff --git a/Plan b/notes.txt b/Plan b/notes.txt

The last b/ is inside the new-side path, so GitFilePatch.Path comes back as notes.txt. The code comment says searching from the end protects against the old side containing b/. It doesn't, because without a rename the new side is the same text as the old side.

The patch is silently attributed to the wrong file. A staging UI shows the wrong name, and ForPath(file.Path) or Unstage(file.Path) targets a different file, or none.

2. One quoted name makes the whole call throw

core.quotepath=false only turns off escaping of non-ASCII characters. Git still C-quotes names that contain ", \, a tab or a control character:

diff --git "a/say \"hi\".txt" "b/say \"hi\".txt"

That header has no b/, so Patch() throws GitParseException: A diff header has no recognizable new-side path. One legally named file on Linux or macOS makes the patch for every other file in the repository unreadable. Diff() copes with the same file because it uses -z.

rename from / rename to lines (ApplyHeaderLine, around line 212) are quoted the same way and aren't unquoted either.

Suggested fix

  • No rename or copy: the old and new paths are identical. Split a/P b/P at the midpoint and check that both halves match. This is the approach git's own git_header_name takes.
  • Otherwise: take the path from the +++ b/… line (stripping the trailing tab git appends when a path contains a space) or from rename to.
  • Quoted operands: when an operand starts with ", decode git's C escapes (\", \\, \t, \n, octal \NNN UTF-8 bytes) before building the RelativeFilePath. Do the same for rename from / rename to.
  • Add parser fixtures for Plan b/notes.txt and for say "hi".txt.

Activity

  1. matt-edmondson commented on Sep 26, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    • Category: Bug
    • Priority: High. Both failures come from ordinary, legal filenames:
      • A path containing b/ makes the parser silently attach the patch to the wrong file, so staging calls like ForPath and Unstage act on the wrong target.
      • One C-quoted name makes Patch() throw for the entire repository.
    • Area / suggested owner: GitPatchParser (ReadPathFromFileStart, ApplyHeaderLine)
    • Duplicates / in progress: None found. It is related to GitPatchBuilder doesn't pin context lines or rename detection, so host diff.context=0 yields patches Apply can't stage #121 (the same patch pipeline, builder side). No open PR references this issue.
    • Notes: The midpoint split that git itself uses (git_header_name) fixes the no-rename case without guessing. The quoted-operand decoder can be shared with the rename from/rename to handling.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions