Repository: gitpython-developers/GitPython
Pull request #408 (2016-04-12 21:39:20)

Add support for diffing against root commit
This adds support for diff'ing against the initial commit (or any commit without parents, really). This is something that has classically been a bit hard to do since GitPython does not offer special support for this. (See #364, or http://stackoverflow.com/questions/33916648/get-the-diff-details-of-first-commit-in-gitpython)

I've opted to add this as another special case for the first argument to the `Commit.diff()` call, right next to the currently allowed `None`, the `Index` class, the commit SHA, etc. This plays nice since you'd never use this in combination with those existing options.

It enables:

    initial_commit.diff(NULL_TREE)

…which will produce a diff much like "git show <initial_commit>" would. Of course, it's possible to use a raw `repo.git.diff_tree()` call for this, but that would return a raw string, not a nice DiffIndex object. The workaround suggesting to use the magic "empty tree sha" also does not work, as the diff output is then in the "reverse direction".

Let me know what you think of this! Test cases included.

Byron: @nvie This is a nice one ! Thank so much for re-viving GitPython with your contributions.

As you can see, I have left a few notes on the code itself, and am looking forward to what you think !
Besides I very much like the `blame_incremental` feature you added, thank you !

Something you could consider is to add new features to the the `changes.rst`, that way you assure they are not forgotten when I at some point make a release. Speaking of which: If you for some reason should need one, please let me know. As the process is cumbersome, I am very much avoiding it unless there is a major bugfix to be distributed.

Last but definitely not least: Using the `six` library is totally fine, yet I hope it doesn't do anything in the background that could affect GitPython's overall performance. There is quite a vivid memory of me using the performance tests to get python 3 support without slowing things down to a crawl - at the end of this process just a few tweaks were needed and I was quite happy to not have introduced another dependency.
nvie: (I've reverted my branch and force-pushed now to be a clean change again. Sorry for mixing this one up with the other feature.)
nvie: > Something you could consider is to add new features to the the changes.rst, that way you assure they are not forgotten when I at some point make a release.

Will do!

> Speaking of which: If you for some reason should need one, please let me know. As the process is cumbersome, I am very much avoiding it unless there is a major bugfix to be distributed.

I can understand. Once everything is merged, I'll point our version to the latest master and make sure everything is stable. Then we can roll an update.

> Last but definitely not least: Using the six library is totally fine, yet I hope it doesn't do anything in the background that could affect GitPython's overall performance. There is quite a vivid memory of me using the performance tests to get python 3 support without slowing things down to a crawl - at the end of this process just a few tweaks were needed and I was quite happy to not have introduced another dependency.

I'm using six here mostly to get a convenient import target for things like `string_type`, `binary_type`, and `range`. I don't think six itself is a slowdown (it only provides a stable import target for things that have been moved around between Python versions AFAIK), but perhaps I'm missing something.

I'll address your feedback on this branch now.

Patch:
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index 23954c7f..4fdf39ca 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -4,6 +4,9 @@ Changelog
 
 1.0.3 - Fixes
 =============
+
+* `Commit.diff()` now supports diffing the root commit via `Commit.diff(NULL_TREE)`.
+* `Repo.blame()` now respects `incremental=True`, supporting incremental blames.  Incremental blames are slightly faster since they don't include the file's contents in them.
 * IMPORTANT: This release drops support for python 2.6, which is officially deprecated by the python maintainers.
 
 1.0.2 - Fixes
diff --git a/git/compat.py b/git/compat.py
index 146bfd4b..7bd8e494 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -8,7 +8,6 @@
 # flake8: noqa
 
 import sys
-import six
 
 from gitdb.utils.compat import (
     PY3,
@@ -34,6 +33,7 @@ if PY3:
         return bytes([n])
     def mviter(d):
         return d.values()
+    range = xrange
     unicode = str
 else:
     FileType = file
@@ -44,21 +44,17 @@ else:
     byte_ord = ord
     bchr = chr
     unicode = unicode
+    range = xrange
     def mviter(d):
         return d.itervalues()
 
-PRE_PY27 = sys.version_info < (2, 7)
-
 
 def safe_decode(s):
     """Safely decodes a binary string to unicode"""
-    if isinstance(s, six.text_type):
+    if isinstance(s, unicode):
         return s
-    elif isinstance(s, six.binary_type):
-        if PRE_PY27:
-            return s.decode(defenc)  # we're screwed
-        else:
-            return s.decode(defenc, errors='replace')
+    elif isinstance(s, bytes):
+        return s.decode(defenc, errors='replace')
     raise TypeError('Expected bytes or text, but got %r' % (s,))
 
 
diff --git a/git/diff.py b/git/diff.py
index 062220df..de3aa1e8 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -16,7 +16,10 @@ from git.compat import (
 )
 
 
-__all__ = ('Diffable', 'DiffIndex', 'Diff')
+__all__ = ('Diffable', 'DiffIndex', 'Diff', 'NULL_TREE')
+
+# Special object to compare against the empty tree in diffs
+NULL_TREE = object()
 
 
 class Diffable(object):
@@ -49,6 +52,7 @@ class Diffable(object):
             If None, we will be compared to the working tree.
             If Treeish, it will be compared against the respective tree
             If Index ( type ), it will be compared against the index.
+            If git.NULL_TREE, it will compare against the empty tree.
             It defaults to Index to assure the method will not by-default fail
             on bare repositories.
 
@@ -87,10 +91,17 @@ class Diffable(object):
         if paths is not None and not isinstance(paths, (tuple, list)):
             paths = [paths]
 
-        if other is not None and other is not self.Index:
-            args.insert(0, other)
+        diff_cmd = self.repo.git.diff
         if other is self.Index:
-            args.insert(0, "--cached")
+            args.insert(0, '--cached')
+        elif other is NULL_TREE:
+            args.insert(0, '-r')  # recursive diff-tree
+            args.insert(0, '--root')
+            diff_cmd = self.repo.git.diff_tree
+        elif other is not None:
+            args.insert(0, '-r')  # recursive diff-tree
+            args.insert(0, other)
+            diff_cmd = self.repo.git.diff_tree
 
         args.insert(0, self)
 
@@ -101,7 +112,7 @@ class Diffable(object):
         # END paths handling
 
         kwargs['as_process'] = True
-        proc = self.repo.git.diff(*self._process_diff_args(args), **kwargs)
+        proc = diff_cmd(*self._process_diff_args(args), **kwargs)
 
         diff_method = Diff._index_from_raw_format
         if create_patch:
diff --git a/git/repo/base.py b/git/repo/base.py
index 9d013230..f74e0b59 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -54,12 +54,12 @@ from git.compat import (
     defenc,
     PY3,
     safe_decode,
+    range,
 )
 
 import os
 import sys
 import re
-from six.moves import range
 
 DefaultDBType = GitCmdObjectDB
 if sys.version_info[:2] < (2, 5):     # python 2.4 compatiblity
diff --git a/tox.ini b/tox.ini
index da624b5e..9f03872b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,11 @@
 [tox]
-envlist = py26,py27,py33,py34,flake8
+envlist = py26,py27,py33,py34,py35,flake8
 
 [testenv]
 commands = nosetests {posargs}
 deps = -r{toxinidir}/requirements.txt
        -r{toxinidir}/test-requirements.txt
+passenv = HOME
 
 [testenv:cover]
 commands = nosetests --with-coverage {posargs}


Test patch:
diff --git a/git/test/fixtures/diff_initial b/git/test/fixtures/diff_initial
new file mode 100644
index 00000000..6037c677
--- /dev/null
+++ b/git/test/fixtures/diff_initial
@@ -0,0 +1,10 @@
+--- /dev/null
++++ b/CHANGES
+@@ -0,0 +1,7 @@
++=======
++CHANGES
++=======
++
++0.1.0
++=====
++initial release
diff --git a/git/test/test_diff.py b/git/test/test_diff.py
index b0d98248..56e395fd 100644
--- a/git/test/test_diff.py
+++ b/git/test/test_diff.py
@@ -21,7 +21,8 @@ from git import (
     Repo,
     GitCommandError,
     Diff,
-    DiffIndex
+    DiffIndex,
+    NULL_TREE,
 )
 
 
@@ -128,6 +129,21 @@ class TestDiff(TestBase):
         assert res[0].deleted_file
         assert res[0].b_path == ''
 
+    def test_diff_initial_commit(self):
+        initial_commit = self.rorepo.commit('33ebe7acec14b25c5f84f35a664803fcab2f7781')
+
+        # Without creating a patch...
+        diff_index = initial_commit.diff(NULL_TREE)
+        assert diff_index[0].b_path == 'CHANGES'
+        assert diff_index[0].new_file
+        assert diff_index[0].diff == ''
+
+        # ...and with creating a patch
+        diff_index = initial_commit.diff(NULL_TREE, create_patch=True)
+        assert diff_index[0].b_path == 'CHANGES'
+        assert diff_index[0].new_file
+        assert diff_index[0].diff == fixture('diff_initial')
+
     def test_diff_patch_format(self):
         # test all of the 'old' format diffs for completness - it should at least
         # be able to deal with it
@@ -149,7 +165,7 @@ class TestDiff(TestBase):
                 diff_item = commit.tree
             # END use tree every second item
 
-            for other in (None, commit.Index, commit.parents[0]):
+            for other in (None, NULL_TREE, commit.Index, commit.parents[0]):
                 for paths in (None, "CHANGES", ("CHANGES", "lib")):
                     for create_patch in range(2):
                         diff_index = diff_item.diff(other=other, paths=paths, create_patch=create_patch)