a082b8b498edb71aa3de146456c759192afcbd9c
[redakcja.git] / lib / wlrepo / backend_mercurial.py
1 # -*- encoding: utf-8 -*-
2
3 __author__ = "Ɓukasz Rekucki"
4 __date__ = "$2009-09-18 10:49:24$"
5
6 __doc__ = """RAL implementation over Mercurial"""
7
8 import mercurial
9 from mercurial import localrepo as hglrepo
10 from mercurial import ui as hgui
11 from mercurial.node import nullid
12 import re
13 import wlrepo
14
15 FILTER = re.compile(r"^pub_(.+)\.xml$", re.UNICODE)
16
17 def default_filter(name):
18     m = FILTER.match(name)    
19     if m is not None:
20         return name, m.group(1)
21     return None
22
23 class MercurialLibrary(wlrepo.Library):
24
25     def __init__(self, path, maincabinet="default", ** kwargs):
26         super(wlrepo.Library, self).__init__( ** kwargs)
27
28         self._hgui = hgui.ui()
29         self._hgui.config('ui', 'quiet', 'true')
30         self._hgui.config('ui', 'interactive', 'false')
31
32         import os.path        
33         self._ospath = self._sanitize_string(os.path.realpath(path))
34         
35         maincabinet = self._sanitize_string(maincabinet)
36
37         if os.path.isdir(path):
38             try:
39                 self._hgrepo = hglrepo.localrepository(self._hgui, path)
40             except mercurial.error.RepoError:
41                 raise wlrepo.LibraryException("[HGLibrary] Not a valid repository at path '%s'." % path)
42         elif kwargs.get('create', False):
43             os.makedirs(path)
44             try:
45                 self._hgrepo = hglrepo.localrepository(self._hgui, path, create=1)
46             except mercurial.error.RepoError:
47                 raise wlrepo.LibraryException("[HGLibrary] Can't create a repository on path '%s'." % path)
48         else:
49             raise wlrepo.LibraryException("[HGLibrary] Can't open a library on path '%s'." % path)
50
51         # fetch the main cabinet
52         lock = self._hgrepo.lock()
53         try:
54             btags = self._hgrepo.branchtags()
55             
56             if not self._has_branch(maincabinet):
57                 raise wlrepo.LibraryException("[HGLibrary] No branch named '%s' to init main cabinet" % maincabinet)
58         
59             self._maincab = MercurialCabinet(self, maincabinet)
60         finally:
61             lock.release()
62
63     @property
64     def ospath(self):
65         return self._ospath
66
67     @property
68     def main_cabinet(self):
69         return self._maincab
70
71     def document(self, docid, user, part=None, shelve=None):
72         return self.cabinet(docid, user, create=False).retrieve(part=part, shelve=shelve)
73
74     def cabinet(self, docid, user, create=False):
75         docid = self._sanitize_string(docid)
76         user = self._sanitize_string(user)
77         
78         bname = self._bname(user, docid)
79
80         lock = self._lock(True)
81         try:
82             if self._has_branch(bname):
83                 return MercurialCabinet(self, doc=docid, user=user)
84
85             if not create:
86                 raise wlrepo.CabinetNotFound(bname)
87
88             # check if the docid exists in the main cabinet
89             needs_touch = not self._maincab.exists(docid)            
90             cab = MercurialCabinet(self, doc=docid, user=user)
91
92             name, fileid = cab._filename(None)
93
94             def cleanup_action(l):
95                 if needs_touch:                    
96                     l._fileopener()(fileid, "w").write('')
97                     l._fileadd(fileid)
98                 
99                 garbage = [fid for (fid, did) in l._filelist() if not did.startswith(docid)]                
100                 l._filesrm(garbage)
101                 print "removed: ", garbage
102
103             # create the branch
104             self._create_branch(bname, before_commit=cleanup_action)
105             return MercurialCabinet(self, doc=docid, user=user)
106         finally:
107             lock.release()
108             
109     #
110     # Private methods
111     #
112
113     #
114     # Locking
115     #
116  
117     def _lock(self, write_mode=False):
118         return self._hgrepo.wlock() # no support for read/write mode yet
119
120     def _transaction(self, write_mode, action):
121         lock = self._lock(write_mode)
122         try:
123             return action(self)
124         finally:
125             lock.release()
126             
127     #
128     # Basic repo manipulation
129     #   
130
131     def _checkout(self, rev, force=True):
132         return MergeStatus(mercurial.merge.update(self._hgrepo, rev, False, force, None))
133
134     def _merge(self, rev):
135         """ Merge the revision into current working directory """
136         return MergeStatus(mercurial.merge.update(self._hgrepo, rev, True, False, None))
137     
138     def _common_ancestor(self, revA, revB):
139         return self._hgrepo[revA].ancestor(self.repo[revB])
140
141     def _commit(self, message, user=u"library"):
142         return self._hgrepo.commit(text=message, user=user)
143
144
145     def _fileexists(self, fileid):
146         return (fileid in self._hgrepo[None])
147
148     def _fileadd(self, fileid):
149         return self._hgrepo.add([fileid])
150     
151     def _filesadd(self, fileid_list):
152         return self._hgrepo.add(fileid_list)
153
154     def _filerm(self, fileid):
155         return self._hgrepo.remove([fileid])
156
157     def _filesrm(self, fileid_list):
158         return self._hgrepo.remove(fileid_list)
159
160     def _filelist(self, filter=default_filter):
161         for name in  self._hgrepo[None]:
162             result = filter(name)
163             if result is None: continue
164             
165             yield result
166
167     def _fileopener(self):
168         return self._hgrepo.wopener
169
170     def _filectx(self, fileid, branchid):
171         return self._hgrepo.filectx(fileid, changeid=branchid)
172
173     def _changectx(self, nodeid):
174         return self._hgrepo.changectx(nodeid)
175     
176     #
177     # BASIC BRANCH routines
178     #
179
180     def _bname(self, user, docid):
181         """Returns a branch name for a given document and user."""
182         docid = self._sanitize_string(docid)
183         uname = self._sanitize_string(user)
184         return "personal_" + uname + "_file_" + docid;
185
186     def _has_branch(self, name):
187         return self._hgrepo.branchmap().has_key(self._sanitize_string(name))
188
189     def _branch_tip(self, name):
190         name = self._sanitize_string(name)
191         return self._hgrepo.branchtags()[name]
192
193     def _create_branch(self, name, parent=None, before_commit=None):        
194         name = self._sanitize_string(name)
195
196         if self._has_branch(name): return # just exit
197
198         if parent is None:
199             parent = self._maincab
200
201         parentrev = parent._hgtip()
202
203         self._checkout(parentrev)
204         self._hgrepo.dirstate.setbranch(name)
205
206         if before_commit: before_commit(self)
207
208         self._commit("[AUTO] Initial commit for branch '%s'." % name, user='library')
209         
210         # revert back to main
211         self._checkout(self._maincab._hgtip())
212         return self._branch_tip(name)
213
214     def _switch_to_branch(self, branchname):
215         current = self._hgrepo[None].branch()
216
217         if current == branchname:
218             return current # quick exit
219         
220         self._checkout(self._branch_tip(branchname))
221         return branchname        
222
223     def shelf(self, nodeid=None):
224         if nodeid is None:
225             nodeid = self._maincab._name
226         return MercurialShelf(self, self._changectx(nodeid))   
227
228
229     #
230     # Utils
231     #
232
233     @staticmethod
234     def _sanitize_string(s):        
235         if isinstance(s, unicode):
236             s = s.encode('utf-8')
237         return s
238
239 class MercurialCabinet(wlrepo.Cabinet):
240     
241     def __init__(self, library, branchname=None, doc=None, user=None):
242         if doc and user:
243             super(MercurialCabinet, self).__init__(library, doc=doc, user=user)
244             self._branchname = library._bname(user=user, docid=doc)
245         elif branchname:
246             super(MercurialCabinet, self).__init__(library, name=branchname)
247             self._branchname = branchname
248         else:
249             raise ValueError("Provide either doc/user or branchname")
250
251     def shelf(self):
252         return self._library.shelf(self._branchname)
253
254     def parts(self):
255         return self._execute_in_branch(action=lambda l, c: (e[1] for e in l._filelist()))
256
257     def retrieve(self, part=None, shelf=None):
258         name, fileid = self._filename(part)
259
260         print "Retrieving document %s from cab %s" % (name, self._name)
261
262         if fileid is None:
263             raise wlrepo.LibraryException("Can't retrieve main document from main cabinet.")
264
265         def retrieve_action(l,c):
266             if l._fileexists(fileid):
267                 return MercurialDocument(c, name=name, fileid=fileid)
268             print "File %s not found " % fileid
269             return None
270                 
271         return self._execute_in_branch(retrieve_action)        
272
273     def create(self, name, initial_data):
274         name, fileid = self._filename(name)
275
276         if name is None:
277             raise ValueError("Can't create main doc for maincabinet.")
278
279         def create_action(l, c):
280             if l._fileexists(fileid):
281                 raise wlrepo.LibraryException("Can't create document '%s' in cabinet '%s' - it already exists" % (fileid, c.name))
282
283             fd = l._fileopener()(fileid, "w")
284             fd.write(initial_data)
285             fd.close()
286             l._fileadd(fileid)            
287             l._commit("File '%s' created." % fileid)            
288             return MercurialDocument(c, fileid=fileid, name=name)           
289
290         return self._execute_in_branch(create_action)
291
292     def exists(self, part=None, shelf=None):
293         name, filepath = self._filename(part)
294
295         if filepath is None: return False
296         return self._execute_in_branch(lambda l, c: l._fileexists(filepath))
297     
298     def _execute_in_branch(self, action, write=False):
299         def switch_action(library):
300             old = library._switch_to_branch(self._branchname)
301             try:
302                 return action(library, self)
303             finally:
304                 library._switch_to_branch(old)
305
306         return self._library._transaction(write_mode=write, action=switch_action)
307
308
309     def _filename(self, docid):
310         return self._partname(docid, 'xml')
311     
312     def _partname(self, docid, part):
313         docid = self._library._sanitize_string(part)
314         part = self._library._sanitize_string(part)
315
316         if part is None:
317             part = 'xml'
318
319         if self._maindoc == '' and docid is None:
320             return None
321             
322         return 'pub_' + docid + '.' + part
323             
324     def _fileopener(self):
325         return self._library._fileopener()
326
327     def _hgtip(self):
328         return self._library._branch_tip(self._branchname)
329
330     def _filectx(self, fileid):
331         return self._library._filectx(fileid, self._branchname)
332
333     def ismain(self):
334         return (self._library.main_cabinet == self)
335
336 class MercurialDocument(wlrepo.Document):
337
338     def __init__(self, cabinet, docid):
339         super(MercurialDocument, self).__init__(cabinet, name=docid)
340         self._opener = self._cabinet._fileopener()
341         self._docid = docid
342         self._ctxs = {}
343
344     def _ctx(self, part):
345         if not self._ctxs.has_key(part):            
346             self._ctxs[part] = self._cabinet._filectx(self._fileid())
347         return self._ctxs[part]
348
349     def _fileid(self, part='xml'):
350         return self._cabinet._partname(self._docid, part)
351
352     def read(self, part='xml'):       
353         return self._opener(self._ctx(part).path(), "r").read()
354
355     def write(self, data, part='xml'):
356         return self._opener(self._ctx(part).path(), "w").write(data)
357
358     def commit(self, message, user):
359         """Commit all parts of the document."""
360         self.library._fileadd(self._fileid)
361         self.library._commit(self._fileid, message, user)
362
363     def update(self):
364         """Update parts of the document."""
365         lock = self.library._lock()
366         try:
367             if self._cabinet.ismain():
368                 return True # always up-to-date
369
370             user = self._cabinet.username or 'library'
371             mdoc = self.library.document(self._fileid)
372
373             mshelf = mdoc.shelf()
374             shelf = self.shelf()
375
376             if not mshelf.ancestorof(shelf) and not shelf.parentof(mshelf):
377                 shelf.merge_with(mshelf, user=user)
378
379             return True
380         finally:
381             lock.release()            
382
383     def share(self, message):
384         lock = self.library._lock()
385         try:
386             print "sharing from", self._cabinet, self._cabinet.username
387             
388             if self._cabinet.ismain():
389                 return True # always shared
390
391             if self._cabinet.username is None:
392                 raise ValueError("Can only share documents from personal cabinets.")
393             
394             user = self._cabinet.username
395
396             main = self.library.shelf()
397             local = self.shelf()
398
399             no_changes = True
400
401             # Case 1:
402             #         * local
403             #         |
404             #         * <- can also be here!
405             #        /|
406             #       / |
407             # main *  *
408             #      |  |
409             # The local branch has been recently updated,
410             # so we don't need to update yet again, but we need to
411             # merge down to default branch, even if there was
412             # no commit's since last update
413
414             if main.ancestorof(local):
415                 print "case 1"
416                 main.merge_with(local, user=user, message=message)
417                 no_changes = False
418             # Case 2:
419             #
420             # main *  * local
421             #      |\ |
422             #      | \|
423             #      |  *
424             #      |  |
425             #
426             # Default has no changes, to update from this branch
427             # since the last merge of local to default.
428             elif local.has_common_ancestor(main):
429                 print "case 2"
430                 if not local.parentof(main):
431                     main.merge_with(local, user=user, message=message)
432                     no_changes = False
433
434             # Case 3:
435             # main *
436             #      |
437             #      * <- this case overlaps with previos one
438             #      |\
439             #      | \
440             #      |  * local
441             #      |  |
442             #
443             # There was a recent merge to the defaul branch and
444             # no changes to local branch recently.
445             #
446             # Use the fact, that user is prepared to see changes, to
447             # update his branch if there are any
448             elif local.ancestorof(main):
449                 print "case 3"
450                 if not local.parentof(main):
451                     local.merge_with(main, user=user, message='Local branch update.')
452                     no_changes = False
453             else:
454                 print "case 4"
455                 local.merge_with(main, user=user, message='Local branch update.')
456                 local = self.shelf()
457                 main.merge_with(local, user=user, message=message)
458
459             print "no_changes: ", no_changes
460             return no_changes
461         finally:
462             lock.release()
463                
464     def shared(self):
465         return self.library.main_cabinet.retrieve(self._name)
466
467     def exists(self, part='xml'):
468         return self._cabinet.exists(self._fileid(part))
469
470     @property
471     def size(self):
472         return self._filectx.size()
473     
474     def shelf(self):
475         return self._cabinet.shelf()
476
477     @property
478     def last_modified(self):
479         return self._filectx.date()
480
481     def __str__(self):
482         return u"Document(%s->%s)" % (self._cabinet.name, self._name)
483
484     def __eq__(self, other):
485         return self._filectx == other._filectx
486
487
488
489 class MercurialShelf(wlrepo.Shelf):
490
491     def __init__(self, lib, changectx):
492         super(MercurialShelf, self).__init__(lib)
493
494         if isinstance(changectx, str):
495             self._changectx = lib._changectx(changectx)
496         else:
497             self._changectx = changectx
498
499     @property
500     def _rev(self):
501         return self._changectx.node()
502
503     def __str__(self):
504         return self._changectx.hex()
505
506     def __repr__(self):
507         return "MercurialShelf(%s)" % self._changectx.hex()
508
509     def ancestorof(self, other):
510         nodes = list(other._changectx._parents)
511         while nodes[0].node() != nullid:
512             v = nodes.pop(0)
513             if v == self._changectx:
514                 return True
515             nodes.extend( v._parents )
516         return False
517
518     def parentof(self, other):
519         return self._changectx in other._changectx._parents
520
521     def has_common_ancestor(self, other):
522         a = self._changectx.ancestor(other._changectx)
523         # print a, self._changectx.branch(), a.branch()
524
525         return (a.branch() == self._changectx.branch())
526
527     def merge_with(self, other, user, message):
528         lock = self._library._lock(True)
529         try:
530             self._library._checkout(self._changectx.node())
531             self._library._merge(other._changectx.node())
532             self._library._commit(user=user, message=message)
533         finally:
534             lock.release()
535
536     def __eq__(self, other):
537         return self._changectx.node() == other._changectx.node()
538         
539
540 class MergeStatus(object):
541     def __init__(self, mstatus):
542         self.updated = mstatus[0]
543         self.merged = mstatus[1]
544         self.removed = mstatus[2]
545         self.unresolved = mstatus[3]
546
547     def isclean(self):
548         return self.unresolved == 0
549
550 class UpdateStatus(object):
551
552     def __init__(self, mstatus):
553         self.modified = mstatus[0]
554         self.added = mstatus[1]
555         self.removed = mstatus[2]
556         self.deleted = mstatus[3]
557         self.untracked = mstatus[4]
558         self.ignored = mstatus[5]
559         self.clean = mstatus[6]
560
561     def has_changes(self):
562         return bool(len(self.modified) + len(self.added) + \
563                     len(self.removed) + len(self.deleted))
564
565 __all__ = ["MercurialLibrary"]