Skip to content

Commit f6ac084

Browse files
authored
Merge pull request #2292 from Keerthana-64/diff-raw-record-fields
fix(diff): read raw diff records field by field
2 parents 28ab8fb + 53df596 commit f6ac084

2 files changed

Lines changed: 35 additions & 17 deletions

File tree

‎git/diff.py‎

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -733,12 +733,17 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]
733733
# Discard everything before the first colon, and the colon itself.
734734
_, _, lines = lines.partition(":")
735735

736-
for line in lines.split("\x00:"):
737-
if not line:
736+
# Each record is a metadata field starting with a colon, followed by one path,
737+
# or two for copies and renames, each terminated by NUL. A path may itself
738+
# start with a colon or contain a line feed, so read the fields in order
739+
# instead of splitting at colons.
740+
fields = iter(lines.rstrip("\n").split("\x00"))
741+
for meta in fields:
742+
if not meta:
738743
# The line data is empty, skip.
739744
continue
740-
meta, _, path = line.partition("\x00")
741-
path = path.rstrip("\x00")
745+
if meta.startswith(":"):
746+
meta = meta[1:]
742747
a_blob_id: Optional[str]
743748
b_blob_id: Optional[str]
744749
old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4)
@@ -748,7 +753,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]
748753
change_type: Lit_change_type = cast(Lit_change_type, _change_type[0])
749754
score_str = "".join(_change_type[1:])
750755
score = int(score_str) if score_str.isdigit() else None
751-
path = path.strip("\n")
756+
path = next(fields, "")
752757
a_path = path.encode(defenc)
753758
b_path = path.encode(defenc)
754759
deleted_file = False
@@ -767,13 +772,9 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]
767772
new_file = True
768773
elif change_type == "C":
769774
copied_file = True
770-
a_path_str, b_path_str = path.split("\x00", 1)
771-
a_path = a_path_str.encode(defenc)
772-
b_path = b_path_str.encode(defenc)
775+
b_path = next(fields, "").encode(defenc)
773776
elif change_type == "R":
774-
a_path_str, b_path_str = path.split("\x00", 1)
775-
a_path = a_path_str.encode(defenc)
776-
b_path = b_path_str.encode(defenc)
777+
b_path = next(fields, "").encode(defenc)
777778
rename_from, rename_to = a_path, b_path
778779
elif change_type == "T":
779780
# Nothing to do.
@@ -817,17 +818,17 @@ def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff]
817818
# :100644 100644 687099101... 37c5e30c8... M .gitignore
818819

819820
index: "DiffIndex" = DiffIndex()
821+
text_list: List[bytes] = []
820822
stderr_list: List[bytes] = []
821823

822824
def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
823825
finalize_process(proc, stderr=b"".join(stderr_list))
824826

827+
# Output is read line by line, but paths may contain line feeds, so parse
828+
# the records only once all of it has been read.
825829
handle_process_output(
826-
proc,
827-
lambda byt: cls._handle_diff_line(byt, repo, index),
828-
stderr_list.append,
829-
finalize_process_with_stderr,
830-
decode_streams=False,
830+
proc, text_list.append, stderr_list.append, finalize_process_with_stderr, decode_streams=False
831831
)
832+
cls._handle_diff_line(b"".join(text_list), repo, index)
832833

833834
return index

‎test/test_diff.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
55

66
import gc
7+
from io import BytesIO
78
import os.path as osp
89
import shutil
910
import sys
@@ -12,10 +13,13 @@
1213
import ddt
1314
import pytest
1415

15-
from git import NULL_TREE, Diff, DiffIndex, Diffable, GitCommandError, Repo, Submodule
16+
from gitdb.base import IStream
17+
18+
from git import NULL_TREE, Blob, Diff, DiffIndex, Diffable, GitCommandError, Repo, Submodule
1619
from git.cmd import Git
1720
from git.diff import decode_path
1821
from git.exc import UnsafeOptionError
22+
from git.index.typ import BaseIndexEntry
1923

2024
from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory
2125

@@ -253,6 +257,19 @@ def test_diff_file_with_colon(self):
253257
Diff._handle_diff_line(output, None, res)
254258
self.assertEqual(len(res), 3)
255259

260+
@with_rw_directory
261+
def test_diff_raw_paths_with_colon_or_line_feed(self, rw_dir):
262+
repo = Repo.init(rw_dir)
263+
binsha = repo.odb.store(IStream(Blob.type, 1, BytesIO(b"x"))).binsha
264+
fake_meta = ":100644 100644 %s %s M" % ("1" * 40, "2" * 40)
265+
paths = [":planted", "docs/a\n" + fake_meta, "docs/b", "setup.py"]
266+
repo.index.add([BaseIndexEntry((0o100644, binsha, 0, path)) for path in paths])
267+
commit = repo.index.commit("paths")
268+
269+
diffs = commit.diff(NULL_TREE)
270+
self.assertEqual([d.b_path for d in diffs], sorted(paths))
271+
self.assertEqual({d.change_type for d in diffs}, {"A"})
272+
256273
def test_empty_diff(self):
257274
res = []
258275
Diff._handle_diff_line(b"", None, res)

0 commit comments

Comments
 (0)