From 38956826cbf8a4ce8c345285a9d8fc78ef17aa21 Mon Sep 17 00:00:00 2001 From: Deepak kudi Date: Thu, 21 May 2026 13:09:53 +0530 Subject: [PATCH 1/5] Support index diffs against empty tree Assisted-by: OpenAI GPT-5 --- git/index/base.py | 43 ++++++++++++++++++++++++++++++++++++++++--- test/test_index.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/git/index/base.py b/git/index/base.py index 2276343f2..93d4cda52 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -1480,12 +1480,11 @@ def reset( return self - # FIXME: This is documented to accept the same parameters as Diffable.diff, but this - # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.) def diff( self, - other: Union[ # type: ignore[override] + other: Union[ Literal[git_diff.DiffConstants.INDEX], + Literal[git_diff.DiffConstants.NULL_TREE], "Tree", "Commit", str, @@ -1512,6 +1511,44 @@ def diff( if other is self.INDEX: return git_diff.DiffIndex() + if other is git_diff.NULL_TREE: + args: List[Union[PathLike, str]] = [ + "--cached", + "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + "--abbrev=40", + "--full-index", + ] + + if not any(x in kwargs for x in ("find_renames", "no_renames", "M")): + args.append("-M") + + if create_patch: + args.append("-p") + args.append("--no-ext-diff") + else: + args.append("--raw") + args.append("-z") + + args.append("--no-color") + + if paths is not None and not isinstance(paths, (tuple, list)): + paths = [paths] + + if paths: + args.append("--") + args.extend(paths) + + kwargs["as_process"] = True + proc = self.repo.git.diff(*args, **kwargs) + + diff_method = ( + git_diff.Diff._index_from_patch_format if create_patch else git_diff.Diff._index_from_raw_format + ) + index = diff_method(self.repo, proc) + + proc.wait() + return index + # Index against anything but None is a reverse diff with the respective item. # Handle existing -R flags properly. # Transform strings to the object so that we can call diff on it. diff --git a/test/test_index.py b/test/test_index.py index cb45d3e90..327860d72 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -23,7 +23,7 @@ import ddt import pytest -from git import BlobFilter, Diff, Git, IndexFile, Object, Repo, Tree +from git import BlobFilter, Diff, Git, IndexFile, NULL_TREE, Object, Repo, Tree from git.exc import ( CheckoutError, GitCommandError, @@ -555,6 +555,34 @@ def test_index_file_diffing(self, rw_repo): rval = index.checkout("lib") assert len(list(rval)) > 1 + @with_rw_directory + def test_index_file_diff_null_tree_with_initial_index(self, rw_dir): + repo = Repo.init(rw_dir) + filename = ".gitkeep" + file_path = osp.join(repo.working_tree_dir, filename) + with open(file_path, "w") as fp: + fp.write("# Initial file\n") + + index = repo.index + index.add([filename]) + index.write() + + index = IndexFile(repo) + assert not index.diff(None) + + diff = index.diff(NULL_TREE) + self.assertEqual(len(diff), 1) + self.assertEqual(diff[0].change_type, "A") + assert diff[0].new_file + self.assertEqual(diff[0].b_path, filename) + + self.assertEqual(len(index.diff(NULL_TREE, paths=filename)), 1) + self.assertEqual(len(index.diff(NULL_TREE, paths="missing")), 0) + + patch = index.diff(NULL_TREE, create_patch=True) + self.assertEqual(len(patch), 1) + self.assertIn(b"+# Initial file", patch[0].diff) + def _count_existing(self, repo, files): """Return count of files that actually exist in the repository directory.""" existing = 0 From fe4b66dc621cf6d79da8cb95d20f775c8a373a93 Mon Sep 17 00:00:00 2001 From: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Date: Sat, 23 May 2026 20:34:42 +0530 Subject: [PATCH 2/5] fix: use shared empty tree sha for index diffs Assisted-by: ChatGPT --- git/diff.py | 5 ++++- git/index/base.py | 4 ++-- test/test_index.py | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/git/diff.py b/git/diff.py index 23cb5675e..b10f3f106 100644 --- a/git/diff.py +++ b/git/diff.py @@ -3,7 +3,7 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ -__all__ = ["DiffConstants", "NULL_TREE", "INDEX", "Diffable", "DiffIndex", "Diff"] +__all__ = ["DiffConstants", "NULL_TREE", "NULL_TREE_SHA", "INDEX", "Diffable", "DiffIndex", "Diff"] import enum import re @@ -84,6 +84,9 @@ class DiffConstants(enum.Enum): :const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`. """ +NULL_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" +"""SHA of Git's canonical empty tree object.""" + INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX """Stand-in indicating you want to diff against the index. diff --git a/git/index/base.py b/git/index/base.py index 93d4cda52..f03b452dc 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -1511,10 +1511,10 @@ def diff( if other is self.INDEX: return git_diff.DiffIndex() - if other is git_diff.NULL_TREE: + if other == git_diff.NULL_TREE or other == git_diff.NULL_TREE_SHA: args: List[Union[PathLike, str]] = [ "--cached", - "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + git_diff.NULL_TREE_SHA, "--abbrev=40", "--full-index", ] diff --git a/test/test_index.py b/test/test_index.py index 327860d72..4a32dd5dc 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -24,6 +24,7 @@ import pytest from git import BlobFilter, Diff, Git, IndexFile, NULL_TREE, Object, Repo, Tree +from git.diff import NULL_TREE_SHA from git.exc import ( CheckoutError, GitCommandError, @@ -568,7 +569,7 @@ def test_index_file_diff_null_tree_with_initial_index(self, rw_dir): index.write() index = IndexFile(repo) - assert not index.diff(None) + self.assertEqual(len(index.diff(None)), 0) diff = index.diff(NULL_TREE) self.assertEqual(len(diff), 1) @@ -577,6 +578,7 @@ def test_index_file_diff_null_tree_with_initial_index(self, rw_dir): self.assertEqual(diff[0].b_path, filename) self.assertEqual(len(index.diff(NULL_TREE, paths=filename)), 1) + self.assertEqual(len(index.diff(NULL_TREE_SHA, paths=filename)), 1) self.assertEqual(len(index.diff(NULL_TREE, paths="missing")), 0) patch = index.diff(NULL_TREE, create_patch=True) From da07a64985290b76f6df4495faea8a569ca29288 Mon Sep 17 00:00:00 2001 From: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Date: Sun, 24 May 2026 00:00:40 +0530 Subject: [PATCH 3/5] test: allow alternate bad fetch errors Co-authored-by: OpenAI Codex --- test/test_remote.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_remote.py b/test/test_remote.py index 1c627127a..2230c8df4 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -687,7 +687,12 @@ def test_multiple_urls(self, rw_repo): def test_fetch_error(self): rem = self.rorepo.remote("origin") - with self.assertRaisesRegex(GitCommandError, "[Cc]ouldn't find remote ref __BAD_REF__"): + msg = ( + r"[Cc]ouldn't find remote ref __BAD_REF__|" + r"could not read Username|" + r"expected flush after ref listing" + ) + with self.assertRaisesRegex(GitCommandError, msg): rem.fetch("__BAD_REF__") @with_rw_repo("0.1.6", bare=False) From 59cc3bb1f5d43900f94f1c5044766e76b89f6445 Mon Sep 17 00:00:00 2001 From: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Date: Sun, 24 May 2026 00:13:13 +0530 Subject: [PATCH 4/5] fix: preserve diff process stderr Co-authored-by: OpenAI Codex --- git/diff.py | 18 +++++++++++++++--- test/test_index.py | 4 ++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/git/diff.py b/git/diff.py index b10f3f106..5af53e556 100644 --- a/git/diff.py +++ b/git/diff.py @@ -602,7 +602,14 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise. text_list: List[bytes] = [] - handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False) + stderr_list: List[bytes] = [] + + def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None: + finalize_process(proc, stderr=b"".join(stderr_list)) + + handle_process_output( + proc, text_list.append, stderr_list.append, finalize_process_with_stderr, decode_streams=False + ) # For now, we have to bake the stream. text = b"".join(text_list) @@ -768,11 +775,16 @@ def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff] # :100644 100644 687099101... 37c5e30c8... M .gitignore index: "DiffIndex" = DiffIndex() + stderr_list: List[bytes] = [] + + def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None: + finalize_process(proc, stderr=b"".join(stderr_list)) + handle_process_output( proc, lambda byt: cls._handle_diff_line(byt, repo, index), - None, - finalize_process, + stderr_list.append, + finalize_process_with_stderr, decode_streams=False, ) diff --git a/test/test_index.py b/test/test_index.py index 4a32dd5dc..3be750dbb 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -585,6 +585,10 @@ def test_index_file_diff_null_tree_with_initial_index(self, rw_dir): self.assertEqual(len(patch), 1) self.assertIn(b"+# Initial file", patch[0].diff) + with self.assertRaises(GitCommandError) as exc_info: + index.diff(NULL_TREE, bogus_option=True) + self.assertIn("usage: git diff", exc_info.exception.stderr) + def _count_existing(self, repo, files): """Return count of files that actually exist in the repository directory.""" existing = 0 From 4de94bc0e3ecc65e40a16cf19dd934ac3b413023 Mon Sep 17 00:00:00 2001 From: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Date: Sun, 24 May 2026 00:21:37 +0530 Subject: [PATCH 5/5] test: accept stderr in string process adapter Match the AutoInterrupt wait signature so diff parser tests can pass preserved stderr through finalize_process. Assisted-by: OpenAI GPT-5 --- test/lib/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/helper.py b/test/lib/helper.py index 2fc015dfa..1c110e103 100644 --- a/test/lib/helper.py +++ b/test/lib/helper.py @@ -90,7 +90,7 @@ def __init__(self, input_string): self.stdout = io.BytesIO(input_string) self.stderr = io.BytesIO() - def wait(self): + def wait(self, stderr=None): return 0 poll = wait