changeset 5:0be8ec7b9717

add merging of conflicts
author Radomir Dopieralski <sheep-devel@sheep.art.pl>
date Fri, 13 May 2011 23:28:54 +0200
parents 52f6805311fd
children 161239cbd12b
files hgstorage.py test_repo.py
diffstat 2 files changed, 76 insertions(+), 27 deletions(-) [+]
line wrap: on
line diff
--- a/hgstorage.py	Fri May 13 22:14:46 2011 +0200
+++ b/hgstorage.py	Fri May 13 23:28:54 2011 +0200
@@ -13,6 +13,8 @@
 import mercurial.ui
 import mercurial.revlog
 import mercurial.context
+import mercurial.util
+import mercurial.simplemerge
 
 
 class Storage(object):
@@ -72,7 +74,7 @@
 class Session(object):
     def __init__(self, storage):
         self.storage = storage
-        self.update()
+        self._update()
 
     def __contains__(self, key):
         filename = self.storage._key_to_file(key)
@@ -81,47 +83,69 @@
     def __getitem__(self, key):
         return Item(self, key)
 
-    def update(self):
+    def _update(self):
+        """Reset the tip of the repository."""
+
         self.tip = self.storage.repo.changectx('tip')
 
+    def _get_parents(self, filename, parent_rev):
+        """Calculate the parents of the commit."""
+
+        if parent_rev is None:
+            return ('tip', None)
+        try:
+            filetip = self.tip[filename]
+        except mercurial.revlog.LookupError:
+            if parent_rev != -1:
+                raise IndexError("no such parent revision %r" % parent_rev)
+            return ('tip', None)
+
+        last_rev = filetip.filerev()
+        if parent_rev > last_rev:
+            raise IndexError("no such parent revision %r" % parent_rev)
+        if parent_rev == last_rev:
+            return ('tip', None)
+
+        return (parent_rev, last_rev)
+
+    def _merge(self, filename, parent, other, data):
+        """Merge conflicting changes."""
+
+        filetip = self.tip[filename]
+        parent_data = filetip.filectx(parent).data()
+        other_data = filetip.filectx(other).data()
+        if (mercurial.util.binary(data) or
+            mercurial.util.binary(parent_data) or
+            mercurial.util.binary(other_data)):
+            raise ValueError("conflict when saving binary data")
+        m3 = mercurial.simplemerge.Merge3Text(parent_data, other_data, data)
+        return ''.join(m3.merge_lines(start_marker='<<<<<<<',
+                                      mid_marker='=======',
+                                      end_marker='>>>>>>>',
+                                      base_marker='|||||||'))
+
     def save(self, key, data, user=None, message=None, parent_rev=None,
              encoding=None, force=False):
         """Save a new revision of the item."""
 
         if encoding:
             data = data.encode(encoding)
-            is_text = True
-        else:
-            is_text = False
 
         filename = self.storage._key_to_file(key)
-        if parent_rev is None:
-            parents = ('tip', None)
-        else:
-            try:
-                filetip = self.tip[filename]
-                last_rev = filetip.filerev()
-            except mercurial.revlog.LookupError:
-                last_rev = -1
-            if force:
-                parents = (last_rev, parent_rev)
-            elif parent_rev == last_rev:
-                parents = ('tip', None)
-            elif is_text:
-                # XXX merge the data
-                parents = (last_rev, parent_rev)
-            else:
-                raise ValueError("conflict when saving binary data")
+        parent, other = self._get_parents(filename, parent_rev)
+
+        if other is not None and not force:
+            data = self._merge(filename, parent, other, data)
 
         def filectxfn(repo, memctx, path):
             return mercurial.context.memfilectx(path, data, False, False, None)
 
         ctx = mercurial.context.memctx(
-            repo=self.storage.repo, parents=parents,
-            text=message or "Commit Message", files=[filename],
+            repo=self.storage.repo, parents=(parent, other),
+            text=message or "saved", files=[filename],
             filectxfn=filectxfn, user=user or "user")
         self.storage.repo.commitctx(ctx)
-        self.update()
+        self._update()
 
     def filectx(self, key, rev=None):
         filename = self.storage._key_to_file(key)
--- a/test_repo.py	Fri May 13 22:14:46 2011 +0200
+++ b/test_repo.py	Fri May 13 23:28:54 2011 +0200
@@ -65,9 +65,34 @@
 
     def test_double_save(self, storage):
         s = storage.session()
-        s.save('title', 'text1', 'user1', 'message1', -1, encoding='utf8')
-        s.save('title', 'text2', 'user2', 'message2', 0, encoding='utf8')
+        s.save('title', u'text1', 'user1', 'message1', -1, encoding='utf8')
+        s.save('title', u'text2', 'user2', 'message2', 0, encoding='utf8')
         data = s.load('title')
         assert 'text2' == data
         data = s.load('title', 0)
         assert 'text1' == data
+
+    def test_save_merge(self, storage):
+        s = storage.session()
+        s.save('title', u'a\ntext\nb', 'user1', 'message1', -1, encoding='utf8')
+        data = s.load('title')
+        assert 'a\ntext\nb' == data
+        s.save('title', u'a\ntext\nbb', 'user1', 'message1', 0, encoding='utf8')
+        data = s.load('title')
+        assert 'a\ntext\nbb' == data
+        s.save('title', u'aa\ntext\nb', 'user2', 'message2', 0, encoding='utf8')
+        data = s.load('title')
+        assert 'aa\ntext\nbb' == data
+
+    def test_save_conflict(self, storage):
+        s = storage.session()
+        s.save('title', u'a\ntext\nb', 'user1', 'message1', -1, encoding='utf8')
+        data = s.load('title')
+        assert 'a\ntext\nb' == data
+        s.save('title', u'a\ntext\nbb', 'user1', 'message1', 0, encoding='utf8')
+        data = s.load('title')
+        assert 'a\ntext\nbb' == data
+        s.save('title', u'a\ntext\na', 'user2', 'message2', 0, encoding='utf8')
+        data = s.load('title')
+        assert 'a\ntext\n<<<<<<<\nbb|||||||\nb=======\na>>>>>>>\n' == data
+