fix(gitdiff): unquote C-quoted paths from git diff-tree --raw (#39781) (#39783)

Backport #39781 by @SudhanshuMatrix

## What

`parseGitDiffTreeLine` (`services/gitdiff/git_diff_tree.go`) read
`BasePath`/`HeadPath` straight from `git diff-tree --raw` output without
undoing git's C-quoting, unlike its sibling `parseLsTreeLine`
(`modules/git/parse.go`), which already does this with
`strconv.Unquote`.

This fix applies the same unquoting to both paths in the diff-tree
parser.

## Why

With the default `core.quotePath=true`, git C-quotes **any path
containing a non-ASCII byte** — not just tabs/newlines/backslashes. That
means any file with a CJK, Cyrillic, accented-Latin, or emoji name gets
octal-escaped, e.g.:

```
:100644 100644 b6fc4c6... 04fea06... M	"\346\227\245\346\234\254\350\252\236.txt"
```
(`日本語.txt`, confirmed against real `git diff-tree --raw` output)

`GetDiffTree` backs the file-tree sidebar on the commit, compare, and
pull request diff pages (`routers/web/repo/commit.go`, `compare.go`,
`pull.go`). Without unquoting, any such file shows its mangled, escaped
name instead of the real one, and the wrong path can throw off
jump-to-file and viewed-file-state behavior that key off the path.

Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Giteabot
2026-10-14 20:35:32 +00:00
committed by GitHub
co-authored by Shudhanshu Singh wxiaoguang
parent 74e7e813c3
commit 26f537dea6
3 changed files with 40 additions and 18 deletions
+11 -7
View File
@@ -56,13 +56,17 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) {
return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
}
if len(entryName) > 0 && entryName[0] == '"' {
entry.Name, err = strconv.Unquote(string(entryName))
if err != nil {
return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
}
} else {
entry.Name = string(entryName)
entry.Name, err = UnquoteDiffFilePath(string(entryName))
if err != nil {
return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
}
return entry, nil
}
func UnquoteDiffFilePath(path string) (string, error) {
// ref: see the document of git config's "core.quotepath"
if !strings.HasPrefix(path, `"`) {
return path, nil
}
return strconv.Unquote(path)
}
+13 -11
View File
@@ -123,24 +123,19 @@ func validateGitDiffTreeArguments(ctx context.Context, gitRepo *git.Repository,
}
func parseGitDiffTree(gitOutput io.Reader) ([]*DiffTreeRecord, error) {
/*
The output of `git diff-tree --raw -r --find-renames` is of the form:
/* The output of `git diff-tree --raw -r --find-renames` is of the form:
:<old_mode> <new_mode> <old_sha> <new_sha> <status>\t<path>
:<old_mode> <new_mode> <old_sha> <new_sha> <status>\t<path>
or for renames:
:<old_mode> <new_mode> <old_sha> <new_sha> <status>\t<old_path>\t<new_path>
or for renames:
:<old_mode> <new_mode> <old_sha> <new_sha> <status>\t<old_path>\t<new_path>
See: <https://git-scm.com/docs/git-diff-tree#_raw_output_format> for more details
*/
See: <https://git-scm.com/docs/git-diff-tree#_raw_output_format> for more details */
results := make([]*DiffTreeRecord, 0)
lines := bufio.NewScanner(gitOutput)
for lines.Scan() {
line := lines.Text()
if len(line) == 0 {
if line == "" {
continue
}
@@ -193,6 +188,13 @@ func parseGitDiffTreeLine(line string) (*DiffTreeRecord, error) {
headPath = filePaths[0]
}
if basePath, err = git.UnquoteDiffFilePath(basePath); err != nil {
return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`, invalid base path: %w", line, err)
}
if headPath, err = git.UnquoteDiffFilePath(headPath); err != nil {
return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`, invalid head path: %w", line, err)
}
return &DiffTreeRecord{
Status: status,
Score: score,
+16
View File
@@ -324,6 +324,22 @@ func TestParseGitDiffTree(t *testing.T) {
},
},
},
{
// make sure our code can unquote paths no matter core.quotePath is true or false: quoted chars are like "\t" or "\{oct}"
Name: "file modified with a C-quoted path",
GitOutput: ":100644 100644 b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0 04fea06420ca60892f73becee3614f6d023a4b7f M\t" + `"\346\227\245\346\234\254\350\252\236\ttest.txt"`,
Expected: []*DiffTreeRecord{
{
Status: "modified",
HeadPath: "日本語\ttest.txt",
BasePath: "日本語\ttest.txt",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "04fea06420ca60892f73becee3614f6d023a4b7f",
BaseBlobID: "b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0",
},
},
},
{
Name: "no changes",
GitOutput: ``,