Skip to content

Commit a6dcd1e

Browse files
authored
Merge pull request #2155 from puneetdixit200/fix-initial-index-diff
Support index diffs against the empty tree
2 parents da05ac6 + 4de94bc commit a6dcd1e

5 files changed

Lines changed: 101 additions & 10 deletions

File tree

git/diff.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# This module is part of GitPython and is released under the
44
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
55

6-
__all__ = ["DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff"]
6+
__all__ = ["DiffConstants", "NULL_TREE", "NULL_TREE_SHA", "INDEX", "Diffable", "DiffIndex", "Diff"]
77

88
import enum
99
import re
@@ -84,6 +84,9 @@ class DiffConstants(enum.Enum):
8484
:const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`.
8585
"""
8686

87+
NULL_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
88+
"""SHA of Git's canonical empty tree object."""
89+
8790
INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX
8891
"""Stand-in indicating you want to diff against the index.
8992
@@ -599,7 +602,14 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn
599602

600603
# FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
601604
text_list: List[bytes] = []
602-
handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False)
605+
stderr_list: List[bytes] = []
606+
607+
def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
608+
finalize_process(proc, stderr=b"".join(stderr_list))
609+
610+
handle_process_output(
611+
proc, text_list.append, stderr_list.append, finalize_process_with_stderr, decode_streams=False
612+
)
603613

604614
# For now, we have to bake the stream.
605615
text = b"".join(text_list)
@@ -765,11 +775,16 @@ def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff]
765775
# :100644 100644 687099101... 37c5e30c8... M .gitignore
766776

767777
index: "DiffIndex" = DiffIndex()
778+
stderr_list: List[bytes] = []
779+
780+
def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
781+
finalize_process(proc, stderr=b"".join(stderr_list))
782+
768783
handle_process_output(
769784
proc,
770785
lambda byt: cls._handle_diff_line(byt, repo, index),
771-
None,
772-
finalize_process,
786+
stderr_list.append,
787+
finalize_process_with_stderr,
773788
decode_streams=False,
774789
)
775790

git/index/base.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,12 +1480,11 @@ def reset(
14801480

14811481
return self
14821482

1483-
# FIXME: This is documented to accept the same parameters as Diffable.diff, but this
1484-
# does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
14851483
def diff(
14861484
self,
1487-
other: Union[ # type: ignore[override]
1485+
other: Union[
14881486
Literal[git_diff.DiffConstants.INDEX],
1487+
Literal[git_diff.DiffConstants.NULL_TREE],
14891488
"Tree",
14901489
"Commit",
14911490
str,
@@ -1512,6 +1511,44 @@ def diff(
15121511
if other is self.INDEX:
15131512
return git_diff.DiffIndex()
15141513

1514+
if other == git_diff.NULL_TREE or other == git_diff.NULL_TREE_SHA:
1515+
args: List[Union[PathLike, str]] = [
1516+
"--cached",
1517+
git_diff.NULL_TREE_SHA,
1518+
"--abbrev=40",
1519+
"--full-index",
1520+
]
1521+
1522+
if not any(x in kwargs for x in ("find_renames", "no_renames", "M")):
1523+
args.append("-M")
1524+
1525+
if create_patch:
1526+
args.append("-p")
1527+
args.append("--no-ext-diff")
1528+
else:
1529+
args.append("--raw")
1530+
args.append("-z")
1531+
1532+
args.append("--no-color")
1533+
1534+
if paths is not None and not isinstance(paths, (tuple, list)):
1535+
paths = [paths]
1536+
1537+
if paths:
1538+
args.append("--")
1539+
args.extend(paths)
1540+
1541+
kwargs["as_process"] = True
1542+
proc = self.repo.git.diff(*args, **kwargs)
1543+
1544+
diff_method = (
1545+
git_diff.Diff._index_from_patch_format if create_patch else git_diff.Diff._index_from_raw_format
1546+
)
1547+
index = diff_method(self.repo, proc)
1548+
1549+
proc.wait()
1550+
return index
1551+
15151552
# Index against anything but None is a reverse diff with the respective item.
15161553
# Handle existing -R flags properly.
15171554
# Transform strings to the object so that we can call diff on it.

test/lib/helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def __init__(self, input_string):
9090
self.stdout = io.BytesIO(input_string)
9191
self.stderr = io.BytesIO()
9292

93-
def wait(self):
93+
def wait(self, stderr=None):
9494
return 0
9595

9696
poll = wait

test/test_index.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
import ddt
2424
import pytest
2525

26-
from git import BlobFilter, Diff, Git, IndexFile, Object, Repo, Tree
26+
from git import BlobFilter, Diff, Git, IndexFile, NULL_TREE, Object, Repo, Tree
27+
from git.diff import NULL_TREE_SHA
2728
from git.exc import (
2829
CheckoutError,
2930
GitCommandError,
@@ -555,6 +556,39 @@ def test_index_file_diffing(self, rw_repo):
555556
rval = index.checkout("lib")
556557
assert len(list(rval)) > 1
557558

559+
@with_rw_directory
560+
def test_index_file_diff_null_tree_with_initial_index(self, rw_dir):
561+
repo = Repo.init(rw_dir)
562+
filename = ".gitkeep"
563+
file_path = osp.join(repo.working_tree_dir, filename)
564+
with open(file_path, "w") as fp:
565+
fp.write("# Initial file\n")
566+
567+
index = repo.index
568+
index.add([filename])
569+
index.write()
570+
571+
index = IndexFile(repo)
572+
self.assertEqual(len(index.diff(None)), 0)
573+
574+
diff = index.diff(NULL_TREE)
575+
self.assertEqual(len(diff), 1)
576+
self.assertEqual(diff[0].change_type, "A")
577+
assert diff[0].new_file
578+
self.assertEqual(diff[0].b_path, filename)
579+
580+
self.assertEqual(len(index.diff(NULL_TREE, paths=filename)), 1)
581+
self.assertEqual(len(index.diff(NULL_TREE_SHA, paths=filename)), 1)
582+
self.assertEqual(len(index.diff(NULL_TREE, paths="missing")), 0)
583+
584+
patch = index.diff(NULL_TREE, create_patch=True)
585+
self.assertEqual(len(patch), 1)
586+
self.assertIn(b"+# Initial file", patch[0].diff)
587+
588+
with self.assertRaises(GitCommandError) as exc_info:
589+
index.diff(NULL_TREE, bogus_option=True)
590+
self.assertIn("usage: git diff", exc_info.exception.stderr)
591+
558592
def _count_existing(self, repo, files):
559593
"""Return count of files that actually exist in the repository directory."""
560594
existing = 0

test/test_remote.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,12 @@ def test_multiple_urls(self, rw_repo):
687687

688688
def test_fetch_error(self):
689689
rem = self.rorepo.remote("origin")
690-
with self.assertRaisesRegex(GitCommandError, "[Cc]ouldn't find remote ref __BAD_REF__"):
690+
msg = (
691+
r"[Cc]ouldn't find remote ref __BAD_REF__|"
692+
r"could not read Username|"
693+
r"expected flush after ref listing"
694+
)
695+
with self.assertRaisesRegex(GitCommandError, msg):
691696
rem.fetch("__BAD_REF__")
692697

693698
@with_rw_repo("0.1.6", bare=False)

0 commit comments

Comments
 (0)