[meld] Handle metadata-only changes in Git (closes bgo#655315)



commit 03abb9c391e5a445b812af88e010cd54bd755098
Author: Kai Willadsen <kai willadsen gmail com>
Date:   Sat Aug 6 07:49:39 2011 +1000

    Handle metadata-only changes in Git (closes bgo#655315)
    
    In situations where a file has a metadata-only change (e.g., mode
    changes or empty file creation), Git's diff consists of an empty
    patch preceeded by several lines of metadata. In the end, this ends
    up as an invalid patch that won't apply, leading to the dreaded
    Patch Failed Box of Doom.
    
    This commit adds pruning of metadata from git-generated diffs, makes
    sure that an empty-of-content git diff results in an actually empty
    patch, and changes our git plumbing queries to parse out permission
    change information so that we can show it in the UI.
    
    With this change, we're abusing the "Options" column in the VC UI to
    show essentially random extra information. However, reworking that UI
    so that it makes sense across VCs is a huge project, and Git doesn't
    otherwise use the "Options" column.

 meld/vc/git.py |   60 ++++++++++++++++++++++++++++++++++++++++++++++++-------
 meld/vcview.py |    6 +++++
 2 files changed, 58 insertions(+), 8 deletions(-)
---
diff --git a/meld/vc/git.py b/meld/vc/git.py
index bc13493..684aaa5 100644
--- a/meld/vc/git.py
+++ b/meld/vc/git.py
@@ -5,6 +5,7 @@
 ### Copyright (C) 2002-2005 Stephen Kennedy <stevek gnome org>
 ### Copyright (C) 2005 Aaron Bentley <aaron bentley utoronto ca>
 ### Copyright (C) 2007 Josà Fonseca <j_r_fonseca yahoo co uk>
+### Copyright (C) 2010-2012 Kai Willadsen <kai willadsen gmail com>
 
 ### Redistribution and use in source and binary forms, with or without
 ### modification, are permitted provided that the following conditions
@@ -28,6 +29,7 @@
 ### THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 import os
+import re
 import errno
 import _vc
 
@@ -38,6 +40,8 @@ class Vc(_vc.CachedVc):
     VC_DIR = ".git"
     PATCH_STRIP_NUM = 1
     PATCH_INDEX_RE = "^diff --git [ac]/(.*) [bw]/.*$"
+    GIT_DIFF_FILES_RE = ":(\d+) (\d+) [a-z0-9]+ [a-z0-9]+ ([ADMU])\t(.*)"
+
     state_map = {
         "X": _vc.STATE_NONE,     # Unknown
         "A": _vc.STATE_NEW,      # New
@@ -45,10 +49,13 @@ class Vc(_vc.CachedVc):
         "M": _vc.STATE_MODIFIED, # Modified
         "T": _vc.STATE_MODIFIED, # Type-changed
         "U": _vc.STATE_CONFLICT, # Unmerged
-        "I": _vc.STATE_IGNORED,  # Ignored (made-up status letter)
-        "?": _vc.STATE_NONE,     # Unversioned
     }
 
+    def __init__(self, location):
+        super(Vc, self).__init__(location)
+        self.diff_re = re.compile(self.GIT_DIFF_FILES_RE)
+        self._tree_meta_cache = {}
+
     def check_repo_root(self, location):
         # Check exists instead of isdir, since .git might be a git-file
         if not os.path.exists(os.path.join(location, self.VC_DIR)):
@@ -91,25 +98,25 @@ class Vc(_vc.CachedVc):
 
                 # Get the status of files that are different in the "index" vs
                 # the HEAD of the git repository
-                proc = _vc.popen([self.CMD, "diff-index", "--name-status", \
+                proc = _vc.popen([self.CMD, "diff-index", \
                     "--cached", "HEAD", path], cwd=self.location)
                 entries = proc.read().split("\n")[:-1]
 
                 # Get the status of files that are different in the "index" vs
                 # the files on disk
-                proc = _vc.popen([self.CMD, "diff-files", "--name-status", \
+                proc = _vc.popen([self.CMD, "diff-files", \
                     "-0", path], cwd=self.location)
                 entries += (proc.read().split("\n")[:-1])
 
                 # Identify ignored files
                 proc = _vc.popen([self.CMD, "ls-files", "--others", \
                     "--ignored", "--exclude-standard", path], cwd=self.location)
-                entries += ("I\t%s" % f for f in proc.read().split("\n")[:-1])
+                ignored_entries = proc.read().split("\n")[:-1]
 
                 # Identify unversioned files
                 proc = _vc.popen([self.CMD, "ls-files", "--others", \
                     "--exclude-standard", path], cwd=self.location)
-                entries += ("?\t%s" % f for f in proc.read().split("\n")[:-1])
+                unversioned_entries = proc.read().split("\n")[:-1]
 
                 # An unmerged file or a file that has been modified, added to
                 # git's index, then modified again would result in the file
@@ -133,13 +140,26 @@ class Vc(_vc.CachedVc):
         else:
             # There are 1 or more modified files, parse their state
             for entry in entries:
-                statekey, name = entry.split("\t", 2)
+                columns = self.diff_re.search(entry).groups()
+                old_mode, new_mode, statekey, name = columns
                 if os.name == 'nt':
                     # Git returns unix-style paths on Windows
                     name = os.path.normpath(name.strip())
                 path = os.path.join(self.root, name.strip())
                 state = self.state_map.get(statekey.strip(), _vc.STATE_NONE)
                 tree_state[path] = state
+                if old_mode != new_mode:
+                    msg = _("Mode changed from %s to %s" % \
+                            (old_mode, new_mode))
+                    self._tree_meta_cache[path] = msg
+
+            for entry in ignored_entries:
+                path = os.path.join(self.root, entry.strip())
+                tree_state[path] = _vc.STATE_IGNORED
+
+            for entry in unversioned_entries:
+                path = os.path.join(self.root, entry.strip())
+                tree_state[path] = _vc.STATE_NONE
 
     def _lookup_tree_cache(self, rootdir):
         # Get a list of all files in rootdir, as well as their status
@@ -160,7 +180,8 @@ class Vc(_vc.CachedVc):
         retdirs = []
         for name,path in files:
             state = tree.get(path, _vc.STATE_NORMAL)
-            retfiles.append( _vc.File(path, name, state) )
+            meta = self._tree_meta_cache.get(path, "")
+            retfiles.append(_vc.File(path, name, state, options=meta))
         for name,path in dirs:
             # git does not operate on dirs, just files
             retdirs.append( _vc.Dir(path, name, _vc.STATE_NORMAL))
@@ -171,3 +192,26 @@ class Vc(_vc.CachedVc):
                 if folder == directory:
                     retfiles.append( _vc.File(path, name, state) )
         return retdirs, retfiles
+
+    def clean_patch(self, patch):
+        """Remove extended header lines from the provided patch
+        
+        This removes any of Git's extended header information, being lines
+        giving 'index', 'mode', 'new file' or 'deleted file' metadata. If
+        there is no patch content other than this header information (e.g., if
+        a file has had a mode change and nothing else) then an empty patch is
+        returned, to avoid a non-applyable patch. Anything that doesn't look
+        like a Git format patch is returned unchanged.
+        """
+        if not re.match(self.PATCH_INDEX_RE, patch, re.M):
+            return patch
+
+        patch_lines = patch.splitlines(True)
+        for i, line in enumerate(patch_lines):
+            # A bit loose, but this marks the start of a standard format patch
+            if line.startswith("--- "):
+                break
+        else:
+            return ""
+        return "".join([patch_lines[0]] + patch_lines[i:])
+
diff --git a/meld/vcview.py b/meld/vcview.py
index 6c837a8..06269f3 100644
--- a/meld/vcview.py
+++ b/meld/vcview.py
@@ -412,6 +412,12 @@ class VcView(melddoc.MeldDoc, gnomeglade.Component):
                 diff = difffunc()
                 yield 1
             prefix, patch = diff[0], diff[1]
+
+            try:
+                patch = self.vc.clean_patch(patch)
+            except AttributeError:
+                pass
+
             yield _("[%s] Applying patch") % self.label_text
             if patch:
                 applied = self.show_patch(prefix, patch, silent=silent_error)



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]