1 # -*- encoding: utf-8 -*-
3 __author__ = "Ćukasz Rekucki"
4 __date__ = "$2009-09-18 10:49:24$"
6 __doc__ = """RAL implementation over Mercurial"""
9 from mercurial import localrepo as hglrepo
10 from mercurial import ui as hgui
11 from mercurial.node import nullid
15 FILTER = re.compile(r"^pub_(.+)\.xml$", re.UNICODE)
17 def default_filter(name):
18 m = FILTER.match(name)
20 return name, m.group(1)
23 class MercurialLibrary(wlrepo.Library):
25 def __init__(self, path, maincabinet="default", ** kwargs):
26 super(wlrepo.Library, self).__init__( ** kwargs)
28 self._hgui = hgui.ui()
29 self._hgui.config('ui', 'quiet', 'true')
30 self._hgui.config('ui', 'interactive', 'false')
33 self._ospath = self._sanitize_string(os.path.realpath(path))
35 maincabinet = self._sanitize_string(maincabinet)
37 if os.path.isdir(path):
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):
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)
49 raise wlrepo.LibraryException("[HGLibrary] Can't open a library on path '%s'." % path)
51 # fetch the main cabinet
52 lock = self._hgrepo.lock()
54 btags = self._hgrepo.branchtags()
56 if not self._has_branch(maincabinet):
57 raise wlrepo.LibraryException("[HGLibrary] No branch named '%s' to init main cabinet" % maincabinet)
59 self._maincab = MercurialCabinet(self, maincabinet)
68 def main_cabinet(self):
71 def document(self, docid, user):
72 return self.cabinet(docid, user, create=False).retrieve()
74 def cabinet(self, docid, user, create=False):
75 docid = self._sanitize_string(docid)
76 user = self._sanitize_string(user)
78 bname = self._bname(user, docid)
80 lock = self._lock(True)
82 if self._has_branch(bname):
83 return MercurialCabinet(self, doc=docid, user=user)
86 raise wlrepo.CabinetNotFound(bname)
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)
92 name, fileid = cab._filename(None)
94 def cleanup_action(l):
96 l._fileopener()(fileid, "w").write('')
99 garbage = [fid for (fid, did) in l._filelist() if not did.startswith(docid)]
101 print "removed: ", garbage
104 self._create_branch(bname, before_commit=cleanup_action)
105 return MercurialCabinet(self, doc=docid, user=user)
117 def _lock(self, write_mode=False):
118 return self._hgrepo.wlock() # no support for read/write mode yet
120 def _transaction(self, write_mode, action):
121 lock = self._lock(write_mode)
128 # Basic repo manipulation
131 def _checkout(self, rev, force=True):
132 return MergeStatus(mercurial.merge.update(self._hgrepo, rev, False, force, None))
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))
138 def _common_ancestor(self, revA, revB):
139 return self._hgrepo[revA].ancestor(self.repo[revB])
141 def _commit(self, message, user=u"library"):
142 return self._hgrepo.commit(text=message, user=user)
145 def _fileexists(self, fileid):
146 return (fileid in self._hgrepo[None])
148 def _fileadd(self, fileid):
149 return self._hgrepo.add([fileid])
151 def _filesadd(self, fileid_list):
152 return self._hgrepo.add(fileid_list)
154 def _filerm(self, fileid):
155 return self._hgrepo.remove([fileid])
157 def _filesrm(self, fileid_list):
158 return self._hgrepo.remove(fileid_list)
160 def _filelist(self, filter=default_filter):
161 for name in self._hgrepo[None]:
162 result = filter(name)
163 if result is None: continue
167 def _fileopener(self):
168 return self._hgrepo.wopener
170 def _filectx(self, fileid, branchid):
171 return self._hgrepo.filectx(fileid, changeid=branchid)
173 def _changectx(self, nodeid):
174 return self._hgrepo.changectx(nodeid)
177 # BASIC BRANCH routines
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;
186 def _has_branch(self, name):
187 return self._hgrepo.branchmap().has_key(self._sanitize_string(name))
189 def _branch_tip(self, name):
190 name = self._sanitize_string(name)
191 return self._hgrepo.branchtags()[name]
193 def _create_branch(self, name, parent=None, before_commit=None):
194 name = self._sanitize_string(name)
196 if self._has_branch(name): return # just exit
199 parent = self._maincab
201 parentrev = parent._hgtip()
203 self._checkout(parentrev)
204 self._hgrepo.dirstate.setbranch(name)
206 if before_commit: before_commit(self)
208 self._commit("[AUTO] Initial commit for branch '%s'." % name, user='library')
210 # revert back to main
211 self._checkout(self._maincab._hgtip())
212 return self._branch_tip(name)
214 def _switch_to_branch(self, branchname):
215 current = self._hgrepo[None].branch()
217 if current == branchname:
218 return current # quick exit
220 self._checkout(self._branch_tip(branchname))
223 def shelf(self, nodeid=None):
225 nodeid = self._maincab._name
226 return MercurialShelf(self, self._changectx(nodeid))
234 def _sanitize_string(s):
235 if isinstance(s, unicode):
236 s = s.encode('utf-8')
239 class MercurialCabinet(wlrepo.Cabinet):
241 def __init__(self, library, branchname=None, doc=None, user=None):
243 super(MercurialCabinet, self).__init__(library, doc=doc, user=user)
244 self._branchname = library._bname(user=user, docid=doc)
246 super(MercurialCabinet, self).__init__(library, name=branchname)
247 self._branchname = branchname
249 raise ValueError("Provide either doc/user or branchname")
251 def shelf(self, selector=None):
252 return self._library.shelf(self._branchname)
255 return self._execute_in_branch(action=lambda l, c: (e[1] for e in l._filelist()))
257 def retrieve(self, part=None, shelf=None):
258 name, fileid = self._filename(part)
260 print "Retrieving document %s from cab %s" % (name, self._name)
263 raise wlrepo.LibraryException("Can't retrieve main document from main cabinet.")
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
271 return self._execute_in_branch(retrieve_action)
273 def create(self, name, initial_data):
274 name, fileid = self._filename(name)
277 raise ValueError("Can't create main doc for maincabinet.")
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))
283 fd = l._fileopener()(fileid, "w")
284 fd.write(initial_data)
287 l._commit("File '%s' created." % fileid)
288 return MercurialDocument(c, fileid=fileid, name=name)
290 return self._execute_in_branch(create_action)
292 def exists(self, part=None, shelf=None):
293 name, filepath = self._filename(part)
295 if filepath is None: return False
296 return self._execute_in_branch(lambda l, c: l._fileexists(filepath))
298 def _execute_in_branch(self, action, write=False):
299 def switch_action(library):
300 old = library._switch_to_branch(self._branchname)
302 return action(library, self)
304 library._switch_to_branch(old)
306 return self._library._transaction(write_mode=write, action=switch_action)
308 def _filename(self, part):
309 part = self._library._sanitize_string(part)
312 if self._maindoc == '':
313 if part is None: rreeturn [None, None]
316 docid = self._maindoc + (('$' + part) if part else '')
318 return docid, 'pub_' + docid + '.xml'
320 def _fileopener(self):
321 return self._library._fileopener()
324 return self._library._branch_tip(self._branchname)
326 def _filectx(self, fileid):
327 return self._library._filectx(fileid, self._branchname)
330 return (self._library.main_cabinet == self)
332 class MercurialDocument(wlrepo.Document):
334 def __init__(self, cabinet, name, fileid):
335 super(MercurialDocument, self).__init__(cabinet, name=name)
336 self._opener = self._cabinet._fileopener()
337 self._fileid = fileid
341 self._filectx = self._cabinet._filectx(self._fileid)
344 return self._opener(self._filectx.path(), "r").read()
346 def write(self, data):
347 return self._opener(self._filectx.path(), "w").write(data)
349 def commit(self, message, user):
350 self.library._fileadd(self._fileid)
351 self.library._commit(self._fileid, message, user)
354 lock = self.library._lock()
356 if self._cabinet.ismain():
357 return True # always up-to-date
359 user = self._cabinet.username or 'library'
360 mdoc = self.library.document(self._fileid)
362 mshelf = mdoc.shelf()
365 if not mshelf.ancestorof(shelf) and not shelf.parentof(mshelf):
366 shelf.merge_with(mshelf, user=user)
372 def share(self, message):
373 lock = self.library._lock()
375 print "sharing from", self._cabinet, self._cabinet.username
377 if self._cabinet.ismain():
378 return True # always shared
380 if self._cabinet.username is None:
381 raise ValueError("Can only share documents from personal cabinets.")
383 user = self._cabinet.username
385 main = self.library.shelf()
393 # * <- can also be here!
398 # The local branch has been recently updated,
399 # so we don't need to update yet again, but we need to
400 # merge down to default branch, even if there was
401 # no commit's since last update
403 if main.ancestorof(local):
405 main.merge_with(local, user=user, message=message)
415 # Default has no changes, to update from this branch
416 # since the last merge of local to default.
417 elif local.has_common_ancestor(main):
419 if not local.parentof(main):
420 main.merge_with(local, user=user, message=message)
426 # * <- this case overlaps with previos one
432 # There was a recent merge to the defaul branch and
433 # no changes to local branch recently.
435 # Use the fact, that user is prepared to see changes, to
436 # update his branch if there are any
437 elif local.ancestorof(main):
439 if not local.parentof(main):
440 local.merge_with(main, user=user, message='Local branch update.')
444 local.merge_with(main, user=user, message='Local branch update.')
446 main.merge_with(local, user=user, message=message)
448 print "no_changes: ", no_changes
454 return self.library.main_cabinet.retrieve(self._name)
457 return self._cabinet.exists(self._fileid)
461 return self._filectx.size()
464 return MercurialShelf(self.library, self._filectx.node())
467 def last_modified(self):
468 return self._filectx.date()
471 return u"Document(%s->%s)" % (self._cabinet.name, self._name)
473 def __eq__(self, other):
474 return self._filectx == other._filectx
478 class MercurialShelf(wlrepo.Shelf):
480 def __init__(self, lib, changectx):
481 super(MercurialShelf, self).__init__(lib)
483 if isinstance(changectx, str):
484 self._changectx = lib._changectx(changectx)
486 self._changectx = changectx
490 return self._changectx.node()
493 return self._changectx.hex()
496 return "MercurialShelf(%s)" % self._changectx.hex()
498 def ancestorof(self, other):
499 nodes = list(other._changectx._parents)
500 while nodes[0].node() != nullid:
502 if v == self._changectx:
504 nodes.extend( v._parents )
507 def parentof(self, other):
508 return self._changectx in other._changectx._parents
510 def has_common_ancestor(self, other):
511 a = self._changectx.ancestor(other._changectx)
512 # print a, self._changectx.branch(), a.branch()
514 return (a.branch() == self._changectx.branch())
516 def merge_with(self, other, user, message):
517 lock = self._library._lock(True)
519 self._library._checkout(self._changectx.node())
520 self._library._merge(other._changectx.node())
521 self._library._commit(user=user, message=message)
525 def __eq__(self, other):
526 return self._changectx.node() == other._changectx.node()
529 class MergeStatus(object):
530 def __init__(self, mstatus):
531 self.updated = mstatus[0]
532 self.merged = mstatus[1]
533 self.removed = mstatus[2]
534 self.unresolved = mstatus[3]
537 return self.unresolved == 0
539 class UpdateStatus(object):
541 def __init__(self, mstatus):
542 self.modified = mstatus[0]
543 self.added = mstatus[1]
544 self.removed = mstatus[2]
545 self.deleted = mstatus[3]
546 self.untracked = mstatus[4]
547 self.ignored = mstatus[5]
548 self.clean = mstatus[6]
550 def has_changes(self):
551 return bool(len(self.modified) + len(self.added) + \
552 len(self.removed) + len(self.deleted))
554 __all__ = ["MercurialLibrary"]