From 4624eaaa0e56a014ea44b107b5f3f61dd9620ba1 Mon Sep 17 00:00:00 2001
From: Louis des Landes <louis@obsidian.com.au>
Date: Tue, 18 Dec 2012 17:28:29 +1100
Subject: [PATCH] Added 3 way merge for bzr / git / svn

---
 meld/dirdiff.py    |  4 ++--
 meld/melddoc.py    |  3 ++-
 meld/meldwindow.py | 15 +++++++++-----
 meld/tree.py       |  4 +++-
 meld/vc/_vc.py     | 10 +++++++++
 meld/vc/bzr.py     | 38 +++++++++++++++++++++++++++++++++++
 meld/vc/git.py     | 20 ++++++++++++++++++
 meld/vc/svn.py     | 59 ++++++++++++++++++++++++++++++++----------------------
 meld/vcview.py     | 36 +++++++++++++++++++++++++++------
 9 files changed, 150 insertions(+), 39 deletions(-)

diff --git a/meld/dirdiff.py b/meld/dirdiff.py
index 3db55a5..bfc67d3 100644
--- a/meld/dirdiff.py
+++ b/meld/dirdiff.py
@@ -906,7 +906,7 @@ class DirDiff(melddoc.MeldDoc, gnomeglade.Component):
         if not rows[pane]:
             return
         if os.path.isfile(rows[pane]):
-            self.emit("create-diff", [r for r in rows if os.path.isfile(r)])
+            self.emit("create-diff", [r for r in rows if os.path.isfile(r)], {})
         elif os.path.isdir(rows[pane]):
             if view.row_expanded(path):
                 view.collapse_row(path)
@@ -948,7 +948,7 @@ class DirDiff(melddoc.MeldDoc, gnomeglade.Component):
         for row in selected:
             row_paths = self.model.value_paths(self.model.get_iter(row))
             paths = [p for p in row_paths if os.path.exists(p)]
-            self.emit("create-diff", paths)
+            self.emit("create-diff", paths, {})
 
     def on_button_copy_left_clicked(self, button):
         self.copy_selected(-1)
diff --git a/meld/melddoc.py b/meld/melddoc.py
index 7414e6a..76391a3 100644
--- a/meld/melddoc.py
+++ b/meld/melddoc.py
@@ -40,7 +40,8 @@ class MeldDoc(gobject.GObject):
         'file-changed':         (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
                                  (gobject.TYPE_STRING,)),
         'create-diff':          (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
-                                 (gobject.TYPE_PYOBJECT,)),
+                                 (gobject.TYPE_PYOBJECT, 
+                                  gobject.TYPE_PYOBJECT,), ),
         'status-changed':       (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
                                  (gobject.TYPE_PYOBJECT,)),
         'current-diff-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
diff --git a/meld/meldwindow.py b/meld/meldwindow.py
index 9c27207..5d25877 100644
--- a/meld/meldwindow.py
+++ b/meld/meldwindow.py
@@ -628,7 +628,8 @@ class MeldWindow(gnomeglade.Component):
         self.scheduler.add_scheduler(page.scheduler)
         page.connect("label-changed", self.on_notebook_label_changed)
         page.connect("file-changed", self.on_file_changed)
-        page.connect("create-diff", lambda obj,arg: self.append_diff(arg) )
+        page.connect("create-diff", lambda obj, arg, kwargs: 
+                     self.append_diff(arg, **kwargs))
 
         # Allow reordering of tabs
         self.notebook.set_tab_reorderable(page.widget, True);
@@ -650,14 +651,17 @@ class MeldWindow(gnomeglade.Component):
         doc.set_files(files)
         return doc
 
-    def append_filemerge(self, files):
+    def append_filemerge(self, files, merge_output=None):
         assert len(files) == 3
         doc = filemerge.FileMerge(app.prefs, len(files))
         self._append_page(doc, "text-x-generic")
         doc.set_files(files)
+        if merge_output is not None:
+            doc.set_merge_output_file(merge_output)
         return doc
 
-    def append_diff(self, paths, auto_compare=False, auto_merge=False):
+    def append_diff(self, paths, auto_compare=False, auto_merge=False, 
+                    merge_output=None):
         dirslist = [p for p in paths if os.path.isdir(p)]
         fileslist = [p for p in paths if os.path.isfile(p)]
         if dirslist and fileslist:
@@ -682,7 +686,7 @@ class MeldWindow(gnomeglade.Component):
         elif dirslist:
             return self.append_dirdiff(paths, auto_compare)
         elif auto_merge:
-            return self.append_filemerge(paths)
+            return self.append_filemerge(paths, merge_output=merge_output)
         else:
             return self.append_filediff(paths)
 
@@ -717,7 +721,8 @@ class MeldWindow(gnomeglade.Component):
         self.scheduler.add_scheduler(doc.scheduler)
         path = os.path.abspath(path)
         doc.set_location(path)
-        doc.connect("create-diff", lambda obj,arg: self.append_diff(arg))
+        doc.connect("create-diff", lambda obj, arg, kwargs: 
+                    self.append_diff(arg,**kwargs))
         doc.run_diff([path])
 
     def open_paths(self, paths, auto_compare=False, auto_merge=False):
diff --git a/meld/tree.py b/meld/tree.py
index 349e0d9..dc8c0b9 100644
--- a/meld/tree.py
+++ b/meld/tree.py
@@ -31,7 +31,9 @@ from meld.vc._vc import \
     STATE_IGNORED, STATE_NONE, STATE_NORMAL, STATE_NOCHANGE, \
     STATE_ERROR, STATE_EMPTY, STATE_NEW, \
     STATE_MODIFIED, STATE_CONFLICT, STATE_REMOVED, \
-    STATE_MISSING, STATE_NONEXIST, STATE_MAX
+    STATE_MISSING, STATE_NONEXIST, STATE_MAX, \
+    CONFLICT_BASE, CONFLICT_LOCAL, CONFLICT_REMOTE, \
+    CONFLICT_MERGED, CONFLICT_OTHER, CONFLICT_THIS
 
 
 class DiffTreeStore(gtk.TreeStore):
diff --git a/meld/vc/_vc.py b/meld/vc/_vc.py
index eddb8f3..e04643f 100644
--- a/meld/vc/_vc.py
+++ b/meld/vc/_vc.py
@@ -35,6 +35,16 @@ STATE_ERROR, STATE_EMPTY, STATE_NEW, \
 STATE_MODIFIED, STATE_CONFLICT, STATE_REMOVED, \
 STATE_MISSING, STATE_NONEXIST, STATE_MAX = list(range(13))
 
+# Order is important here, matches up with git show numbers
+CONFLICT_MERGED, CONFLICT_BASE, CONFLICT_LOCAL, \
+CONFLICT_REMOTE, CONFLICT_MAX = list(range(5))
+# These names are used by BZR, and are logically identical.
+CONFLICT_OTHER = CONFLICT_REMOTE
+CONFLICT_THIS = CONFLICT_LOCAL
+
+conflicts = _("Merged:Base:Local:Remote").split(':')
+assert len(conflicts)== CONFLICT_MAX
+
 class Entry(object):
     # These are the possible states of files. Be sure to get the colons correct.
     states = _("Ignored:Unversioned:::Error::Newly added:Modified:Conflict:Removed:Missing:Not present").split(":")
diff --git a/meld/vc/bzr.py b/meld/vc/bzr.py
index e64153c..dcc6b5c 100644
--- a/meld/vc/bzr.py
+++ b/meld/vc/bzr.py
@@ -27,6 +27,9 @@ import os
 import re
 
 from . import _vc
+import subprocess
+import tempfile
+import shutil
 
 
 class Vc(_vc.CachedVc):
@@ -38,6 +41,13 @@ class Vc(_vc.CachedVc):
     PATCH_INDEX_RE = "^=== modified file '(.*)'.*$"
     CONFLICT_RE = "conflict in (.*)$"
 
+    conflict_map = {
+        _vc.CONFLICT_BASE: '.BASE',
+        _vc.CONFLICT_OTHER: '.OTHER',
+        _vc.CONFLICT_THIS: '.THIS',
+        _vc.CONFLICT_MERGED: '',
+    }
+
     # We use None here to indicate flags that we don't deal with or care about
     state_1_map = {
         " ": None,               # First status column empty
@@ -144,3 +154,31 @@ class Vc(_vc.CachedVc):
                 state = _vc.STATE_NORMAL
                 retdirs.append( _vc.Dir(path, d, state) )
         return retdirs, retfiles
+
+    def get_path_for_repo_file(self, path, commit=None):
+        if not path.startswith(self.root + os.path.sep):
+            raise _vc.InvalidVCPath(self, path, "Path not in repository")
+                
+        path = path[len(self.root) + 1:]
+        
+        args = [self.CMD, "cat", path]
+        if commit:
+            args.append("-r%s" % commit)
+        process = subprocess.Popen(args,
+                                   cwd=self.location, stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        vc_file = process.stdout
+
+        # Error handling here involves doing nothing; in most cases, the only
+        # sane response is to return an empty temp file.
+
+        with tempfile.NamedTemporaryFile(prefix='meld-tmp', delete=False) as f:
+            shutil.copyfileobj(vc_file, f)
+        return f.name
+
+    
+    def get_path_for_conflict(self, path, conflict=None):
+        if not path.startswith(self.root + os.path.sep):
+            raise _vc.InvalidVCPath(self, path, "Path not in repository")
+        
+        return "%s%s" % (path, self.conflict_map[conflict])
diff --git a/meld/vc/git.py b/meld/vc/git.py
index 5684256..f2aa5e8 100644
--- a/meld/vc/git.py
+++ b/meld/vc/git.py
@@ -80,6 +80,26 @@ class Vc(_vc.CachedVc):
     def revert_command(self):
         return [self.CMD,"checkout"]
 
+    def get_path_for_conflict(self, path, conflict=None):
+        if not path.startswith(self.root + os.path.sep):
+            raise _vc.InvalidVCPath(self, path, "Path not in repository")
+        path = path[len(self.root) + 1:]
+
+        args = ["git", "show", ":%s:%s" % (conflict, path)]
+        process = subprocess.Popen(args,
+                                   cwd=self.location, stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        vc_file = process.stdout
+
+        # Error handling here involves doing nothing; in most cases, the only
+        # sane response is to return an empty temp file.
+
+        with tempfile.NamedTemporaryFile(
+                                prefix='meld-tmp-%s-' % _vc.conflicts[conflict], 
+                                delete=False) as f:
+            shutil.copyfileobj(vc_file, f)
+        return f.name
+
     def get_path_for_repo_file(self, path, commit=None):
         if commit is None:
             commit = "HEAD"
diff --git a/meld/vc/svn.py b/meld/vc/svn.py
index 49e5ec9..0bf9bde 100644
--- a/meld/vc/svn.py
+++ b/meld/vc/svn.py
@@ -50,6 +50,13 @@ class Vc(_vc.CachedVc):
         "conflicted": _vc.STATE_CONFLICT,
     }
 
+    conflict_map = {
+        _vc.CONFLICT_BASE: '.r1',
+        _vc.CONFLICT_OTHER: '.r2',
+        _vc.CONFLICT_THIS: '.mine',
+        _vc.CONFLICT_MERGED: '',
+    }
+
     def commit_command(self, message):
         return [self.CMD,"commit","-m",message]
     def diff_command(self):
@@ -93,6 +100,12 @@ class Vc(_vc.CachedVc):
 
         return f.name
 
+    def get_path_for_conflict(self, path, conflict=None):
+        if not path.startswith(self.root + os.path.sep):
+            raise _vc.InvalidVCPath(self, path, "Path not in repository")
+        
+        return "%s%s" % (path, self.conflict_map[conflict])
+
     def _repo_version_support(self, version):
         return version < 12
 
@@ -137,13 +150,8 @@ class Vc(_vc.CachedVc):
                     item = status.attrib["item"]
                     if item == "":
                         continue
-                    rev = None
-                    if "revision" in status.attrib:
-                        rev = status.attrib["revision"]
-                    mydir, name = os.path.split(path)
-                    if mydir not in tree_state:
-                        tree_state[mydir] = {}
-                    tree_state[mydir][name] = (item, rev)
+                    tree_state[path] = self.state_map.get(
+                                                    item, _vc.STATE_NORMAL)
 
     def _lookup_tree_cache(self, rootdir):
         # Get a list of all files in rootdir, as well as their status
@@ -161,26 +169,29 @@ class Vc(_vc.CachedVc):
         if not directory in tree:
             return [], []
 
+        svnfiles = {}
         retfiles = []
         retdirs = []
 
-        dirtree = tree[directory]
-
-        for name in sorted(dirtree.keys()):
-            svn_state, rev = dirtree[name]
-            path = os.path.join(directory, name)
-
-            isdir = os.path.isdir(path)
-            if isdir:
-                if os.path.exists(path):
-                    state = _vc.STATE_NORMAL
-                else:
-                    state = _vc.STATE_MISSING
-                # svn adds the directory reported to the status list we get.
-                if name != directory:
-                    retdirs.append( _vc.Dir(path,name,state) )
+        for path, state in tree.items():
+            mydir, name = os.path.split(path)
+            if path.endswith('/'):
+                mydir, name = os.path.split(mydir)
+            if mydir != directory:
+                continue
+            if path.endswith('/'):
+                retdirs.append(_vc.Dir(path[:-1], name, state))
             else:
-                state = self.state_map.get(svn_state, _vc.STATE_NONE)
-                retfiles.append(_vc.File(path, name, state, rev))
+                retfiles.append(_vc.File(path, name, state))
+            svnfiles[name] = 1
+        for f,path in files:
+            if f not in svnfiles:
+                state = _vc.STATE_NORMAL
+                retfiles.append(_vc.File(path, f, state))
+        for d,path in dirs:
+            if d not in svnfiles:
+                state = _vc.STATE_NORMAL
+                retdirs.append( _vc.Dir(path, d, state) )
+        print retdirs, retfiles
 
         return retdirs, retfiles
diff --git a/meld/vcview.py b/meld/vcview.py
index 019cd39..d7de283 100644
--- a/meld/vcview.py
+++ b/meld/vcview.py
@@ -460,15 +460,39 @@ class VcView(melddoc.MeldDoc, gnomeglade.Component):
                     retry_diff = True
             else:
                 for path in path_list:
-                    self.emit("create-diff", [path])
+                    self.emit("create-diff", [path], {})
 
     def run_diff(self, path_list):
         try:
             for path in path_list:
-                comp_path = self.vc.get_path_for_repo_file(path)
-                os.chmod(comp_path, 0o444)
-                _temp_files.append(comp_path)
-                self.emit("create-diff", [comp_path, path])
+                kwargs = {}
+                status = self.vc._get_tree_cache(path).get(path, None)
+                if status == tree.STATE_CONFLICT:
+                    # We use auto merge, so we create a new temp file
+                    # for other, base and this, then set the output to
+                    # the current file.
+                    file1 = self.vc.get_path_for_conflict(
+                                path, conflict=tree.CONFLICT_OTHER)
+                    file2 = self.vc.get_path_for_conflict(
+                                path, conflict=tree.CONFLICT_BASE)
+                    file3 = self.vc.get_path_for_conflict(
+                                path, conflict=tree.CONFLICT_THIS)
+                    os.chmod(file1, 0o444)
+                    os.chmod(file2, 0o444)
+                    os.chmod(file3, 0o444)
+                    _temp_files.append(file1)
+                    _temp_files.append(file2)
+                    _temp_files.append(file3)
+
+                    diffs = [file1, file2, file3]
+                    kwargs['auto_merge'] = True
+                    kwargs['merge_output'] = path
+                else:
+                    file1 = self.vc.get_path_for_repo_file(path)
+                    os.chmod(file1, 0o444)
+                    _temp_files.append(file1)
+                    diffs = [file1, path]
+                self.emit("create-diff", diffs, kwargs)
         except NotImplementedError:
             for path in path_list:
                 self.scheduler.add_task(self.run_diff_iter([path]), atfront=1)
@@ -648,7 +672,7 @@ class VcView(melddoc.MeldDoc, gnomeglade.Component):
         if result == 0:
             for d in diffs:
                 os.chmod(d[0], 0o444)
-                self.emit("create-diff", d)
+                self.emit("create-diff", d, {})
             return True
         elif not silent:
             primary = _("Error fetching original comparison file")
-- 
1.8.0.2

