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 hex as to_hex
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)
64 def main_cabinet(self):
67 def document(self, docid, user):
68 return self.cabinet(docid, user, create=False).retrieve()
70 def cabinet(self, docid, user, create=False):
71 bname = self._bname(user, docid)
73 lock = self._lock(True)
75 if self._has_branch(bname):
76 return MercurialCabinet(self, bname, doc=docid, user=user)
79 raise wlrepo.CabinetNotFound(bname)
81 # check if the docid exists in the main cabinet
82 needs_touch = not self._maincab.exists(docid)
83 cab = MercurialCabinet(self, bname, doc=docid, user=user)
85 name, fileid = cab._filename(None)
87 def cleanup_action(l):
89 l._fileopener()(fileid, "w").write('')
92 garbage = [fid for (fid, did) in l._filelist() if not did.startswith(docid)]
96 self._create_branch(bname, before_commit=cleanup_action)
97 return MercurialCabinet(self, bname, doc=docid, user=user)
109 def _lock(self, write_mode=False):
110 return self._hgrepo.wlock() # no support for read/write mode yet
112 def _transaction(self, write_mode, action):
113 lock = self._lock(write_mode)
120 # Basic repo manipulation
123 def _checkout(self, rev, force=True):
124 return MergeStatus(mercurial.merge.update(self._hgrepo, rev, False, force, None))
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))
130 def _common_ancestor(self, revA, revB):
131 return self._hgrepo[revA].ancestor(self.repo[revB])
133 def _commit(self, message, user=u"library"):
134 return self._hgrepo.commit(text=message, user=user)
137 def _fileexists(self, fileid):
138 return (fileid in self._hgrepo[None])
140 def _fileadd(self, fileid):
141 return self._hgrepo.add([fileid])
143 def _filesadd(self, fileid_list):
144 return self._hgrepo.add(fileid_list)
146 def _filerm(self, fileid):
147 return self._hgrepo.remove([fileid])
149 def _filesrm(self, fileid_list):
150 return self._hgrepo.remove(fileid_list)
152 def _filelist(self, filter=default_filter):
153 for name in self._hgrepo[None]:
154 result = filter(name)
155 if result is None: continue
159 def _fileopener(self):
160 return self._hgrepo.wopener
162 def _filectx(self, fileid, branchid):
163 return self._hgrepo.filectx(fileid, changeid=branchid)
165 def _changectx(self, nodeid):
166 return self._hgrepo.changectx(nodeid)
169 # BASIC BRANCH routines
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;
178 def _has_branch(self, name):
179 return self._hgrepo.branchmap().has_key(self._sanitize_string(name))
181 def _branch_tip(self, name):
182 name = self._sanitize_string(name)
183 return self._hgrepo.branchtags()[name]
185 def _create_branch(self, name, parent=None, before_commit=None):
186 name = self._sanitize_string(name)
188 if self._has_branch(name): return # just exit
191 parent = self._maincab
193 parentrev = parent._hgtip()
195 self._checkout(parentrev)
196 self._hgrepo.dirstate.setbranch(name)
198 if before_commit: before_commit(self)
200 self._commit("[AUTO] Initial commit for branch '%s'." % name, user='library')
202 # revert back to main
203 self._checkout(self._maincab._hgtip())
204 return self._branch_tip(name)
206 def _switch_to_branch(self, branchname):
207 current = self._hgrepo[None].branch()
209 if current == branchname:
210 return current # quick exit
212 self._checkout(self._branch_tip(branchname))
226 def _sanitize_string(s):
227 if isinstance(s, unicode):
228 s = s.encode('utf-8')
231 class MercurialCabinet(wlrepo.Cabinet):
233 def __init__(self, library, branchname, doc=None, user=None):
235 super(MercurialCabinet, self).__init__(library, doc=doc, user=user)
237 super(MercurialCabinet, self).__init__(library, name=branchname)
239 self._branchname = branchname
241 def shelf(self, selector=None):
242 if selector is not None:
243 raise NotImplementedException()
245 return MercurialShelf(self, self._library._changectx(self._branchname))
248 return self._execute_in_branch(action=lambda l, c: (e[1] for e in l._filelist()))
250 def retrieve(self, part=None, shelf=None):
251 name, fileid = self._filename(part)
254 raise wlrepo.LibraryException("Can't retrieve main document from main cabinet.")
256 return self._execute_in_branch(lambda l, c: MercurialDocument(c, name, fileid))
258 def create(self, name, initial_data):
259 name, fileid = self._filename(name)
262 raise ValueError("Can't create main doc for maincabinet.")
264 def create_action(l, c):
265 if l._fileexists(fileid):
266 raise wlrepo.LibraryException("Can't create document '%s' in cabinet '%s' - it already exists" % (fileid, c.name))
268 fd = l._fileopener()(fileid, "w")
269 fd.write(initial_data)
272 l._commit("File '%s' created." % fileid)
273 return MercurialDocument(c, fileid=fileid, name=name)
275 return self._execute_in_branch(create_action)
277 def exists(self, part=None, shelf=None):
278 name, filepath = self._filename(part)
280 if filepath is None: return False
281 return self._execute_in_branch(lambda l, c: l._fileexists(filepath))
283 def _execute_in_branch(self, action, write=False):
284 def switch_action(library):
285 old = library._switch_to_branch(self._branchname)
287 return action(library, self)
289 library._switch_to_branch(old)
291 return self._library._transaction(write_mode=write, action=switch_action)
293 def _filename(self, part):
294 part = self._library._sanitize_string(part)
297 if self._maindoc == '':
298 if part is None: return [None, None]
301 fileid = self._maindoc + (('$' + part) if part else '')
303 return fileid, 'pub_' + fileid + '.xml'
305 def _fileopener(self):
306 return self._library._fileopener()
309 return self._library._branch_tip(self._branchname)
311 def _filectx(self, fileid):
312 return self._library._filectx(fileid, self._branchname)
314 class MercurialDocument(wlrepo.Document):
316 def __init__(self, cabinet, name, fileid):
317 super(MercurialDocument, self).__init__(cabinet, name=name)
318 self._opener = self._cabinet._fileopener()
319 self._fileid = fileid
323 self._filectx = self._cabinet._filectx(self._fileid)
324 self._shelf = MercurialShelf(self, self._filectx.node())
327 return self._opener(self._filectx.path(), "r").read()
329 def write(self, data):
330 return self._opener(self._filectx.path(), "w").write(data)
332 def commit(self, message, user):
333 self.library._fileadd(self._fileid)
334 self.library._commit(self._fileid, message, user)
337 if self._cabinet.is_main():
338 return True # always up-to-date
340 mdoc = self.library.document(self._fileid)
342 mshelf = mdoc.shelf()
345 if not mshelf.ancestorof(shelf) and not shelf.parentof(mshelf):
346 shelf.merge_with(mshelf)
350 def share(self, message, user):
351 if self._cabinet.is_main():
352 return True # always shared
354 main = self.shared_version()
362 # * <- can also be here!
367 # The local branch has been recently updated,
368 # so we don't need to update yet again, but we need to
369 # merge down to default branch, even if there was
370 # no commit's since last update
372 if main.ancestorof(local):
373 main.merge_with(local, user=user, message=message)
384 # Default has no changes, to update from this branch
385 # since the last merge of local to default.
386 elif main.has_common_ancestor(local):
387 if not local.parentof(main):
388 main.merge_with(local, user=user, message=message)
394 # * <- this case overlaps with previos one
400 # There was a recent merge to the defaul branch and
401 # no changes to local branch recently.
403 # Use the fact, that user is prepared to see changes, to
404 # update his branch if there are any
405 elif local.ancestorof(main):
406 if not local.parentof(main):
407 local.merge_with(main, user=user, message='Local branch update.')
410 local.merge_with(main, user=user, message='Local branch update.')
415 main.merge_with(local, user=user, message=message)
418 return self.library.document(self._fileid)
422 return self._filectx.size()
429 def last_modified(self):
430 return self._filectx.date()
433 return u"Document(%s->%s)" % (self._cabinet.name, self._name)
436 class MercurialShelf(wlrepo.Shelf):
438 def __init__(self, cabinet, changectx):
439 super(MercurialShelf, self).__init__(cabinet)
440 self._changectx = changectx
444 return self._changectx.node()
447 return self._changectx.hex()
450 def ancestorof(self, other):
453 class MergeStatus(object):
454 def __init__(self, mstatus):
455 self.updated = mstatus[0]
456 self.merged = mstatus[1]
457 self.removed = mstatus[2]
458 self.unresolved = mstatus[3]
461 return self.unresolved == 0
463 class UpdateStatus(object):
465 def __init__(self, mstatus):
466 self.modified = mstatus[0]
467 self.added = mstatus[1]
468 self.removed = mstatus[2]
469 self.deleted = mstatus[3]
470 self.untracked = mstatus[4]
471 self.ignored = mstatus[5]
472 self.clean = mstatus[6]
474 def has_changes(self):
475 return bool(len(self.modified) + len(self.added) + \
476 len(self.removed) + len(self.deleted))
478 __all__ = ["MercurialLibrary"]