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