DC resource. More merge tests.
[redakcja.git] / lib / wlrepo / backend_mercurial.py
index 38f01da..dfcef09 100644 (file)
@@ -1,4 +1,5 @@
 # -*- encoding: utf-8 -*-
+
 __author__ = "Ɓukasz Rekucki"
 __date__ = "$2009-09-18 10:49:24$"
 
@@ -7,6 +8,7 @@ __doc__ = """RAL implementation over Mercurial"""
 import mercurial
 from mercurial import localrepo as hglrepo
 from mercurial import ui as hgui
+from mercurial.node import nullid
 import re
 import wlrepo
 
@@ -36,8 +38,8 @@ class MercurialLibrary(wlrepo.Library):
             try:
                 self._hgrepo = hglrepo.localrepository(self._hgui, path)
             except mercurial.error.RepoError:
-                raise wlrepo.LibraryException("[HGLibrary]Not a valid repository at path '%s'." % path)
-        elif kwargs['create']:
+                raise wlrepo.LibraryException("[HGLibrary] Not a valid repository at path '%s'." % path)
+        elif kwargs.get('create', False):
             os.makedirs(path)
             try:
                 self._hgrepo = hglrepo.localrepository(self._hgui, path, create=1)
@@ -58,41 +60,49 @@ class MercurialLibrary(wlrepo.Library):
         finally:
             lock.release()
 
+    @property
+    def ospath(self):
+        return self._ospath
+
     @property
     def main_cabinet(self):
         return self._maincab
 
+    def document(self, docid, user):
+        return self.cabinet(docid, user, create=False).retrieve()
+
     def cabinet(self, docid, user, create=False):
+        docid = self._sanitize_string(docid)
+        user = self._sanitize_string(user)
+        
         bname = self._bname(user, docid)
 
         lock = self._lock(True)
         try:
             if self._has_branch(bname):
-                return MercurialCabinet(self, bname, doc=docid, user=user)
+                return MercurialCabinet(self, doc=docid, user=user)
 
             if not create:
-                raise wlrepo.CabinetNotFound(docid, user)
+                raise wlrepo.CabinetNotFound(bname)
 
             # check if the docid exists in the main cabinet
-            needs_touch = not self._maincab.exists(docid)
-            print "Creating branch: ", needs_touch
-            cab = MercurialCabinet(self, bname, doc=docid, user=user)
+            needs_touch = not self._maincab.exists(docid)            
+            cab = MercurialCabinet(self, doc=docid, user=user)
 
-            fileid = cab._fileid(None)
+            name, fileid = cab._filename(None)
 
             def cleanup_action(l):
-                if needs_touch:
-                    print "Touch for file", docid
+                if needs_touch:                    
                     l._fileopener()(fileid, "w").write('')
                     l._fileadd(fileid)
                 
-                garbage = [fid for (fid, did) in l._filelist() if not did.startswith(docid)]
-                print "Garbage: ", garbage
+                garbage = [fid for (fid, did) in l._filelist() if not did.startswith(docid)]                
                 l._filesrm(garbage)
+                print "removed: ", garbage
 
             # create the branch
             self._create_branch(bname, before_commit=cleanup_action)
-            return MercurialCabinet(self, bname, doc=docid, user=user)
+            return MercurialCabinet(self, doc=docid, user=user)
         finally:
             lock.release()
             
@@ -128,10 +138,8 @@ class MercurialLibrary(wlrepo.Library):
     def _common_ancestor(self, revA, revB):
         return self._hgrepo[revA].ancestor(self.repo[revB])
 
-    def _commit(self, message, user="library"):
-        return self._hgrepo.commit(\
-                                   text=self._sanitize_string(message), \
-                                   user=self._sanitize_string(user))
+    def _commit(self, message, user=u"library"):
+        return self._hgrepo.commit(text=message, user=user)
 
 
     def _fileexists(self, fileid):
@@ -158,6 +166,12 @@ class MercurialLibrary(wlrepo.Library):
 
     def _fileopener(self):
         return self._hgrepo.wopener
+
+    def _filectx(self, fileid, branchid):
+        return self._hgrepo.filectx(fileid, changeid=branchid)
+
+    def _changectx(self, nodeid):
+        return self._hgrepo.changectx(nodeid)
     
     #
     # BASIC BRANCH routines
@@ -191,7 +205,6 @@ class MercurialLibrary(wlrepo.Library):
 
         if before_commit: before_commit(self)
 
-        print "commiting"
         self._commit("[AUTO] Initial commit for branch '%s'." % name, user='library')
         
         # revert back to main
@@ -207,10 +220,10 @@ class MercurialLibrary(wlrepo.Library):
         self._checkout(self._branch_tip(branchname))
         return branchname        
 
-    #
-    # Merges
-    #
-    
+    def shelf(self, nodeid=None):
+        if nodeid is None:
+            nodeid = self._maincab._name
+        return MercurialShelf(self, self._changectx(nodeid))   
 
 
     #
@@ -218,35 +231,47 @@ class MercurialLibrary(wlrepo.Library):
     #
 
     @staticmethod
-    def _sanitize_string(s):
-        if isinstance(s, unicode): #
-            return s.encode('utf-8')
-        else: # it's a string, so we have no idea what encoding it is
-            return s
+    def _sanitize_string(s):        
+        if isinstance(s, unicode):
+            s = s.encode('utf-8')
+        return s
 
 class MercurialCabinet(wlrepo.Cabinet):
     
-    def __init__(self, library, branchname, doc=None, user=None):
+    def __init__(self, library, branchname=None, doc=None, user=None):
         if doc and user:
             super(MercurialCabinet, self).__init__(library, doc=doc, user=user)
-        else:
+            self._branchname = library._bname(user=user, docid=doc)
+        elif branchname:
             super(MercurialCabinet, self).__init__(library, name=branchname)
-            
-        self._branchname = branchname
+            self._branchname = branchname
+        else:
+            raise ValueError("Provide either doc/user or branchname")
+
+    def shelf(self, selector=None):
+        return self._library.shelf(self._branchname)
 
     def documents(self):        
-        return self._execute_in_branch(action=lambda l, c: ( e[1] for e in l._filelist()) )
+        return self._execute_in_branch(action=lambda l, c: (e[1] for e in l._filelist()))
 
-    def retrieve(self, part=None, shelve=None):
-        fileid = self._fileid(part)
+    def retrieve(self, part=None, shelf=None):
+        name, fileid = self._filename(part)
+
+        print "Retrieving document %s from cab %s" % (name, self._name)
 
         if fileid is None:
             raise wlrepo.LibraryException("Can't retrieve main document from main cabinet.")
+
+        def retrieve_action(l,c):
+            if l._fileexists(fileid):
+                return MercurialDocument(c, name=name, fileid=fileid)
+            print "File %s not found " % fileid
+            return None
                 
-        return self._execute_in_branch(lambda l, c: MercurialDocument(c, fileid))
+        return self._execute_in_branch(retrieve_action)        
 
-    def create(self, name, initial_data=''):
-        fileid = self._fileid(name)
+    def create(self, name, initial_data):
+        name, fileid = self._filename(name)
 
         if name is None:
             raise ValueError("Can't create main doc for maincabinet.")
@@ -257,18 +282,18 @@ class MercurialCabinet(wlrepo.Cabinet):
 
             fd = l._fileopener()(fileid, "w")
             fd.write(initial_data)
-            l._fileadd(fileid)
-            l._commit("File '%d' created.")
-
-            return MercurialDocument(c, fileid)
+            fd.close()
+            l._fileadd(fileid)            
+            l._commit("File '%s' created." % fileid)            
+            return MercurialDocument(c, fileid=fileid, name=name)           
 
         return self._execute_in_branch(create_action)
 
-    def exists(self, part=None, shelve=None):
-        fileid = self._fileid(part)
+    def exists(self, part=None, shelf=None):
+        name, filepath = self._filename(part)
 
-        if fileid is None: return false
-        return self._execute_in_branch(lambda l, c: l._fileexists(fileid))
+        if filepath is None: return False
+        return self._execute_in_branch(lambda l, c: l._fileexists(filepath))
     
     def _execute_in_branch(self, action, write=False):
         def switch_action(library):
@@ -280,16 +305,17 @@ class MercurialCabinet(wlrepo.Cabinet):
 
         return self._library._transaction(write_mode=write, action=switch_action)
 
-    def _fileid(self, part):
-        fileid = None
+    def _filename(self, part):
+        part = self._library._sanitize_string(part)
+        docid = None
 
         if self._maindoc == '':
-            if part is None: return None              
-            fileid = part
+            if part is None: rreeturn [None, None]
+            docid = part
         else:
-            fileid = self._maindoc + (('$' + part) if part else '')
+            docid = self._maindoc + (('$' + part) if part else '')
 
-        return 'pub_' + fileid + '.xml'        
+        return docid, 'pub_' + docid + '.xml'
 
     def _fileopener(self):
         return self._library._fileopener()
@@ -297,18 +323,208 @@ class MercurialCabinet(wlrepo.Cabinet):
     def _hgtip(self):
         return self._library._branch_tip(self._branchname)
 
+    def _filectx(self, fileid):
+        return self._library._filectx(fileid, self._branchname)
+
+    def ismain(self):
+        return (self._library.main_cabinet == self)
+
 class MercurialDocument(wlrepo.Document):
 
-    def __init__(self, cabinet, fileid):
-        super(MercurialDocument, self).__init__(cabinet, fileid)
-        self._opener = self._cabinet._fileopener()        
+    def __init__(self, cabinet, name, fileid):
+        super(MercurialDocument, self).__init__(cabinet, name=name)
+        self._opener = self._cabinet._fileopener()
+        self._fileid = fileid
+        self.refresh()
+
+    def refresh(self):
+        self._filectx = self._cabinet._filectx(self._fileid)        
 
     def read(self):
-        return self._opener(self._name, "r").read()
+        return self._opener(self._filectx.path(), "r").read()
 
     def write(self, data):
-        return self._opener(self._name, "w").write(data)
+        return self._opener(self._filectx.path(), "w").write(data)
+
+    def commit(self, message, user):
+        self.library._fileadd(self._fileid)
+        self.library._commit(self._fileid, message, user)
+
+    def update(self):
+        lock = self.library._lock()
+        try:
+            if self._cabinet.ismain():
+                return True # always up-to-date
 
+            user = self._cabinet.username or 'library'
+            mdoc = self.library.document(self._fileid)
+
+            mshelf = mdoc.shelf()
+            shelf = self.shelf()
+
+            if not mshelf.ancestorof(shelf) and not shelf.parentof(mshelf):
+                shelf.merge_with(mshelf, user=user)
+
+            return True
+        finally:
+            lock.release()            
+
+    def share(self, message):
+        lock = self.library._lock()
+        try:
+            print "sharing from", self._cabinet, self._cabinet.username
+            
+            if self._cabinet.ismain():
+                return True # always shared
+
+            if self._cabinet.username is None:
+                raise ValueError("Can only share documents from personal cabinets.")
+            
+            user = self._cabinet.username
+
+            main = self.library.shelf()
+            local = self.shelf()
+
+            no_changes = True
+
+            # Case 1:
+            #         * local
+            #         |
+            #         * <- can also be here!
+            #        /|
+            #       / |
+            # main *  *
+            #      |  |
+            # The local branch has been recently updated,
+            # so we don't need to update yet again, but we need to
+            # merge down to default branch, even if there was
+            # no commit's since last update
+
+            if main.ancestorof(local):
+                print "case 1"
+                main.merge_with(local, user=user, message=message)
+                no_changes = False
+            # Case 2:
+            #
+            # main *  * local
+            #      |\ |
+            #      | \|
+            #      |  *
+            #      |  |
+            #
+            # Default has no changes, to update from this branch
+            # since the last merge of local to default.
+            elif local.has_common_ancestor(main):
+                print "case 2"
+                if not local.parentof(main):
+                    main.merge_with(local, user=user, message=message)
+                    no_changes = False
+
+            # Case 3:
+            # main *
+            #      |
+            #      * <- this case overlaps with previos one
+            #      |\
+            #      | \
+            #      |  * local
+            #      |  |
+            #
+            # There was a recent merge to the defaul branch and
+            # no changes to local branch recently.
+            #
+            # Use the fact, that user is prepared to see changes, to
+            # update his branch if there are any
+            elif local.ancestorof(main):
+                print "case 3"
+                if not local.parentof(main):
+                    local.merge_with(main, user=user, message='Local branch update.')
+                    no_changes = False
+            else:
+                print "case 4"
+                local.merge_with(main, user=user, message='Local branch update.')
+                local = self.shelf()
+                main.merge_with(local, user=user, message=message)
+
+            print "no_changes: ", no_changes
+            return no_changes
+        finally:
+            lock.release()
+               
+    def shared(self):
+        return self.library.main_cabinet.retrieve(self._name)
+
+    def exists(self):
+        return self._cabinet.exists(self._fileid)
+
+    @property
+    def size(self):
+        return self._filectx.size()
+    
+    def shelf(self):
+        return MercurialShelf(self.library, self._filectx.node())
+
+    @property
+    def last_modified(self):
+        return self._filectx.date()
+
+    def __str__(self):
+        return u"Document(%s->%s)" % (self._cabinet.name, self._name)
+
+    def __eq__(self, other):
+        return self._filectx == other._filectx
+
+
+
+class MercurialShelf(wlrepo.Shelf):
+
+    def __init__(self, lib, changectx):
+        super(MercurialShelf, self).__init__(lib)
+
+        if isinstance(changectx, str):
+            self._changectx = lib._changectx(changectx)
+        else:
+            self._changectx = changectx
+
+    @property
+    def _rev(self):
+        return self._changectx.node()
+
+    def __str__(self):
+        return self._changectx.hex()
+
+    def __repr__(self):
+        return "MercurialShelf(%s)" % self._changectx.hex()
+
+    def ancestorof(self, other):
+        nodes = list(other._changectx._parents)
+        while nodes[0].node() != nullid:
+            v = nodes.pop(0)
+            if v == self._changectx:
+                return True
+            nodes.extend( v._parents )
+        return False
+
+    def parentof(self, other):
+        return self._changectx in other._changectx._parents
+
+    def has_common_ancestor(self, other):
+        a = self._changectx.ancestor(other._changectx)
+        # print a, self._changectx.branch(), a.branch()
+
+        return (a.branch() == self._changectx.branch())
+
+    def merge_with(self, other, user, message):
+        lock = self._library._lock(True)
+        try:
+            self._library._checkout(self._changectx.node())
+            self._library._merge(other._changectx.node())
+            self._library._commit(user=user, message=message)
+        finally:
+            lock.release()
+
+    def __eq__(self, other):
+        return self._changectx.node() == other._changectx.node()
+        
 
 class MergeStatus(object):
     def __init__(self, mstatus):
@@ -335,5 +551,4 @@ class UpdateStatus(object):
         return bool(len(self.modified) + len(self.added) + \
                     len(self.removed) + len(self.deleted))
 
-
-__all__ = ["MercurialLibrary", "MercurialCabinet", "MercurialDocument"]
\ No newline at end of file
+__all__ = ["MercurialLibrary"]
\ No newline at end of file