Repository: gitpython-developers/GitPython
Pull request #945 (2019-10-18 15:38:23)

Add support for 'C'-type diffs
This addresses #932 by adding support for the 'C' (copied) change type.

I used the repro case provided in the issue as the basis for the new test that covers this addition.




Patch:
diff --git a/AUTHORS b/AUTHORS
index e91fd78b..a7a19d54 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -34,5 +34,6 @@ Contributors are:
 -Stefan Stancu <stefan.stancu _at_ gmail.com>
 -César Izurieta <cesar _at_ caih.org>
 -Arthur Milchior <arthur _at_ milchior.fr>
+-JJ Graham <thetwoj _at_ gmail.com>
 
 Portions derived from other open source works and are clearly marked.
diff --git a/git/diff.py b/git/diff.py
index 10cb9f02..0150d675 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -167,7 +167,7 @@ class DiffIndex(list):
     # R = Renamed
     # M = Modified
     # T = Changed in the type
-    change_type = ("A", "D", "R", "M", "T")
+    change_type = ("A", "C", "D", "R", "M", "T")
 
     def iter_change_type(self, change_type):
         """
@@ -193,6 +193,8 @@ class DiffIndex(list):
                 yield diff
             elif change_type == "D" and diff.deleted_file:
                 yield diff
+            elif change_type == "C" and diff.copied_file:
+                yield diff
             elif change_type == "R" and diff.renamed:
                 yield diff
             elif change_type == "M" and diff.a_blob and diff.b_blob and diff.a_blob != diff.b_blob:
@@ -243,6 +245,9 @@ class Diff(object):
                                    ^rename[ ]to[ ](?P<rename_to>.*)(?:\n|$))?
                                 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
                                 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
+                                (?:^similarity[ ]index[ ]\d+%\n
+                                   ^copy[ ]from[ ].*\n
+                                   ^copy[ ]to[ ](?P<copied_file_name>.*)(?:\n|$))?
                                 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
                                     \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
                                 (?:^---[ ](?P<a_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))?
@@ -253,11 +258,11 @@ class Diff(object):
     NULL_BIN_SHA = b"\0" * 20
 
     __slots__ = ("a_blob", "b_blob", "a_mode", "b_mode", "a_rawpath", "b_rawpath",
-                 "new_file", "deleted_file", "raw_rename_from", "raw_rename_to",
-                 "diff", "change_type", "score")
+                 "new_file", "deleted_file", "copied_file", "raw_rename_from",
+                 "raw_rename_to", "diff", "change_type", "score")
 
     def __init__(self, repo, a_rawpath, b_rawpath, a_blob_id, b_blob_id, a_mode,
-                 b_mode, new_file, deleted_file, raw_rename_from,
+                 b_mode, new_file, deleted_file, copied_file, raw_rename_from,
                  raw_rename_to, diff, change_type, score):
 
         self.a_mode = a_mode
@@ -285,6 +290,7 @@ class Diff(object):
 
         self.new_file = new_file
         self.deleted_file = deleted_file
+        self.copied_file = copied_file
 
         # be clear and use None instead of empty strings
         assert raw_rename_from is None or isinstance(raw_rename_from, binary_type)
@@ -336,6 +342,8 @@ class Diff(object):
             msg += '\nfile deleted in rhs'
         if self.new_file:
             msg += '\nfile added in rhs'
+        if self.copied_file:
+            msg += '\nfile %r copied from %r' % (self.b_path, self.a_path)
         if self.rename_from:
             msg += '\nfile renamed from %r' % self.rename_from
         if self.rename_to:
@@ -419,11 +427,12 @@ class Diff(object):
             a_path_fallback, b_path_fallback, \
                 old_mode, new_mode, \
                 rename_from, rename_to, \
-                new_file_mode, deleted_file_mode, \
+                new_file_mode, deleted_file_mode, copied_file_name, \
                 a_blob_id, b_blob_id, b_mode, \
                 a_path, b_path = header.groups()
 
-            new_file, deleted_file = bool(new_file_mode), bool(deleted_file_mode)
+            new_file, deleted_file, copied_file = \
+                bool(new_file_mode), bool(deleted_file_mode), bool(copied_file_name)
 
             a_path = cls._pick_best_path(a_path, rename_from, a_path_fallback)
             b_path = cls._pick_best_path(b_path, rename_to, b_path_fallback)
@@ -445,7 +454,7 @@ class Diff(object):
                               b_blob_id and b_blob_id.decode(defenc),
                               a_mode and a_mode.decode(defenc),
                               b_mode and b_mode.decode(defenc),
-                              new_file, deleted_file,
+                              new_file, deleted_file, copied_file,
                               rename_from,
                               rename_to,
                               None, None, None))
@@ -485,6 +494,7 @@ class Diff(object):
             b_path = path.encode(defenc)
             deleted_file = False
             new_file = False
+            copied_file = False
             rename_from = None
             rename_to = None
 
@@ -496,6 +506,11 @@ class Diff(object):
             elif change_type == 'A':
                 a_blob_id = None
                 new_file = True
+            elif change_type == 'C':
+                copied_file = True
+                a_path, b_path = path.split('\t', 1)
+                a_path = a_path.encode(defenc)
+                b_path = b_path.encode(defenc)
             elif change_type == 'R':
                 a_path, b_path = path.split('\t', 1)
                 a_path = a_path.encode(defenc)
@@ -507,8 +522,8 @@ class Diff(object):
             # END add/remove handling
 
             diff = Diff(repo, a_path, b_path, a_blob_id, b_blob_id, old_mode, new_mode,
-                        new_file, deleted_file, rename_from, rename_to, '',
-                        change_type, score)
+                        new_file, deleted_file, copied_file, rename_from, rename_to,
+                        '', change_type, score)
             index.append(diff)
 
         handle_process_output(proc, handle_diff_line, None, finalize_process, decode_streams=False)


Test patch:
diff --git a/git/test/fixtures/diff_copied_mode b/git/test/fixtures/diff_copied_mode
new file mode 100644
index 00000000..60707afc
--- /dev/null
+++ b/git/test/fixtures/diff_copied_mode
@@ -0,0 +1,4 @@
+diff --git a/test1.txt b/test2.txt
+similarity index 100%
+copy from test1.txt
+copy to test2.txt
diff --git a/git/test/fixtures/diff_copied_mode_raw b/git/test/fixtures/diff_copied_mode_raw
new file mode 100644
index 00000000..7640f3ab
--- /dev/null
+++ b/git/test/fixtures/diff_copied_mode_raw
@@ -0,0 +1,1 @@
+:100644 100644 cfe9deac6e10683917e80f877566b58644aa21df cfe9deac6e10683917e80f877566b58644aa21df C100	test1.txt	test2.txt
diff --git a/git/test/test_diff.py b/git/test/test_diff.py
index e47b9331..e1b345b5 100644
--- a/git/test/test_diff.py
+++ b/git/test/test_diff.py
@@ -112,6 +112,29 @@ class TestDiff(TestBase):
         self.assertEqual(diff.score, 100)
         self.assertEqual(len(list(diffs.iter_change_type('R'))), 1)
 
+    def test_diff_with_copied_file(self):
+        output = StringProcessAdapter(fixture('diff_copied_mode'))
+        diffs = Diff._index_from_patch_format(self.rorepo, output)
+        self._assert_diff_format(diffs)
+
+        assert_equal(1, len(diffs))
+
+        diff = diffs[0]
+        assert_true(diff.copied_file)
+        assert_true(diff.a_path, u'test1.txt')
+        assert_true(diff.b_path, u'test2.txt')
+        assert isinstance(str(diff), str)
+
+        output = StringProcessAdapter(fixture('diff_copied_mode_raw'))
+        diffs = Diff._index_from_raw_format(self.rorepo, output)
+        self.assertEqual(len(diffs), 1)
+        diff = diffs[0]
+        self.assertEqual(diff.change_type, 'C')
+        self.assertEqual(diff.score, 100)
+        self.assertEqual(diff.a_path, u'test1.txt')
+        self.assertEqual(diff.b_path, u'test2.txt')
+        self.assertEqual(len(list(diffs.iter_change_type('C'))), 1)
+
     def test_diff_with_change_in_type(self):
         output = StringProcessAdapter(fixture('diff_change_in_type'))
         diffs = Diff._index_from_patch_format(self.rorepo, output)