Mercurial > evolve
changeset 51:d98e06ab8320
move extensions in a hgext directory
author | Pierre-Yves David <pierre-yves.david@logilab.fr> |
---|---|
date | Thu, 08 Sep 2011 17:15:20 +0200 |
parents | 19b22ad56b32 |
children | 62bdc2567099 |
files | hgext/obsolete.py hgext/states.py obsolete.py states.py tests/test-draft.t tests/test-obsolete.t tests/test-ready.t tests/test-states.t |
diffstat | 8 files changed, 693 insertions(+), 693 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/obsolete.py Thu Sep 08 17:15:20 2011 +0200 @@ -0,0 +1,237 @@ +# obsolete.py - introduce the obsolete concept in mercurial. +# +# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org> +# Logilab SA <contact@logilab.fr> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from mercurial import util +from mercurial import context +from mercurial import revset +from mercurial import scmutil +from mercurial import extensions +from mercurial import pushkey +from mercurial import discovery +from mercurial import error +from mercurial.node import hex, bin + +# Patch changectx +############################# + +def obsolete(ctx): + """is the changeset obsolete by other""" + if ctx.node()is None: + return False + return bool(ctx._repo.obsoletedby(ctx.node())) + +context.changectx.obsolete = obsolete + +ohidden = context.changectx.hidden +def hidden(ctx): + # hack to fill hiddenrevs + # compute hidden (XXX should move elsewhere) + if not getattr(ctx._repo.changelog, 'hiddeninit', False): + basicquery = 'obsolete() - (ancestors(not obsolete() or . or bookmark()))' + for rev in scmutil.revrange(ctx._repo, [basicquery]): + ctx._repo.changelog.hiddenrevs.add(rev) + ctx._repo.changelog.hiddeninit = True + + return ohidden(ctx) +context.changectx.hidden = hidden + +# revset +############################# + +def revsetobsolete(repo, subset, x): + args = revset.getargs(x, 0, 0, 'publicheads takes no arguments') + return [r for r in subset if repo[r].obsolete()] # XXX slow + +def extsetup(ui): + revset.symbols["obsolete"] = revsetobsolete + + def filterobsoleteout(orig, repo, remote, *args,**kwargs): + common, heads = orig(repo, remote, *args, **kwargs) + + # filter obsolete + heads = set(map(repo.changelog.rev, heads)) + obsoletes = set() + for obj in repo._obsobjrels: + try: + obsoletes.add(repo.changelog.rev(obj)) + except error.LookupError: + pass # we don't have this node locally + + outgoing = set(repo.changelog.ancestors(*heads)) + outgoing.update(heads) + + selected = outgoing - obsoletes + heads = sorted(map(repo.changelog.node, selected)) + + return common, heads + + extensions.wrapfunction(discovery, 'findcommonoutgoing', filterobsoleteout) + + try: + rebase = extensions.find('rebase') + if rebase: + extensions.wrapfunction(rebase, 'concludenode', concludenode) + except KeyError: + pass # rebase not found + +# Pushkey mechanism for mutable +######################################### + +def pushobsolete(repo, key, old, relations): + assert key == "relations" + w = repo.wlock() + try: + for sub, objs in relations.iteritems(): + for obj in objs: + repo.addobsolete(sub, obj) + finally: + w.release() + +def listobsolete(repo): + return {'relations': repo._obssubrels} + +pushkey.register('obsolete', pushobsolete, listobsolete) + +# New commands +############################# + + +def cmddebugobsolete(ui, repo, subject, object): + """Add an obsolete relation between a too node + + The subject is expected to be a newer version of the object""" + sub = repo[subject] + obj = repo[object] + repo.addobsolete(sub.node(), obj.node()) + return 0 + +cmdtable = {'debugobsolete': (cmddebugobsolete, [], '<subject> <object>')} + +def reposetup(ui, repo): + + if not repo.local(): + return + + opull = repo.pull + opush = repo.push + + class obsoletingrepo(repo.__class__): + + + ### Hidden revision support + @util.propertycache + def hiddenrevs(self): + # It's a property because It simpler that to handle the __init__ + revs = set() + return revs + + ### obsolete storage + @util.propertycache + def _obsobjrels(self): + """{<old-node> -> set(<new-node>)} + + also compute hidden revision""" + #reverse sub -> objs mapping + objrels = {} + for sub, objs in self._obssubrels.iteritems(): + for obj in objs: + objrels.setdefault(obj, set()).add(sub) + return objrels + + @util.propertycache + def _obssubrels(self): + """{<new-node> -> set(<old-node>)}""" + return self._readobsrels() + + + ### Disk IO + def _readobsrels(self): + """Write obsolete relation on disk""" + # XXX handle lock + rels = {} + try: + f = self.opener('obsolete-relations') + try: + for line in f: + subhex, objhex = line.split() + rels.setdefault(bin(subhex), set()).add(bin(objhex)) + finally: + f.close() + except IOError: + pass + return rels + + def _writeobsrels(self): + """Write obsolete relation on disk""" + # XXX handle lock + f = self.opener('obsolete-relations', 'w', atomictemp=True) + try: + for sub, objs in self._obssubrels.iteritems(): + for obj in objs: + f.write('%s %s\n' % (hex(sub), hex(obj))) + f.rename() + finally: + f.close() + + ### local clone support + + def cancopy(self): + return not bool(self._obsobjrels) # you can't copy if there is obsolete + + ### pull // push support + + def pull(self, remote, *args, **kwargs): + obskey = remote.listkeys('obsolete') + obsrels = obskey.get('relations', {}) + result = opull(remote, *args, **kwargs) + for sub, objs in obsrels.iteritems(): + for obj in objs: + self.addobsolete(sub, obj) + return result + + def push(self, remote, *args, **opts): + obskey = remote.listkeys('obsolete') + obssupport = 'relations' in obskey + result = opush(remote, *args, **opts) + if obssupport: + remote.pushkey('obsolete', 'relations', {}, self._obssubrels) + return result + + + ### Public method + def obsoletedby(self, node): + """return the set of node that make <node> obsolete (obj)""" + return self._obsobjrels.get(node, set()) + + def obsolete(self, node): + """return the set of node that <node> make obsolete (sub)""" + return self._obssubrels.get(node, set()) + + def addobsolete(self, sub, obj): + """Add a relation marking that node <sub> is a new version of <obj>""" + self._obssubrels.setdefault(sub, set()).add(obj) + self._obsobjrels.setdefault(obj, set()).add(sub) + try: + self.changelog.hiddenrevs.add(repo[obj].rev()) + except error.RepoLookupError: + pass #unknow revision (but keep propagating the data + self._writeobsrels() + + repo.__class__ = obsoletingrepo + + +### Other Extension compat +############################ + +def concludenode(orig, repo, rev, *args, **kwargs): + newrev = orig(repo, rev, *args, **kwargs) + oldnode = repo[rev].node() + newnode = repo[newrev].node() + repo.addobsolete(newnode, oldnode) + return newrev +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/states.py Thu Sep 08 17:15:20 2011 +0200 @@ -0,0 +1,452 @@ +# states.py - introduce the state concept for mercurial changeset +# +# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org> +# Logilab SA <contact@logilab.fr> +# Augie Fackler <durin42@gmail.com> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''introduce the state concept for mercurial changeset + +Change can be in the following state: + +0 immutable +1 mutable +2 private + +name are not fixed yet. +''' +import os +from functools import partial + +from mercurial.i18n import _ +from mercurial import cmdutil +from mercurial import scmutil +from mercurial import context +from mercurial import revset +from mercurial import templatekw +from mercurial import util +from mercurial import node +from mercurial.node import nullid, hex, short +from mercurial import discovery +from mercurial import extensions +from mercurial import wireproto +from mercurial import pushkey +from mercurial.lock import release + + +_NOSHARE=2 +_MUTABLE=1 + +class state(object): + + def __init__(self, name, properties=0, next=None): + self.name = name + self.properties = properties + assert next is None or self < next + self.next = next + + def __repr__(self): + return 'state(%s)' % self.name + + def __str__(self): + return self.name + + @util.propertycache + def trackheads(self): + """Do we need to track heads of changeset in this state ? + + We don't need to track heads for the last state as this is repos heads""" + return self.next is not None + + def __cmp__(self, other): + return cmp(self.properties, other.properties) + + @util.propertycache + def _revsetheads(self): + """function to be used by revset to finds heads of this states""" + assert self.trackheads + def revsetheads(repo, subset, x): + args = revset.getargs(x, 0, 0, 'publicheads takes no arguments') + heads = map(repo.changelog.rev, repo._statesheads[self]) + heads.sort() + return heads + return revsetheads + + @util.propertycache + def headssymbol(self): + """name of the revset symbols""" + if self.trackheads: + return "%sheads" % self.name + else: + return 'heads' + +ST2 = state('draft', _NOSHARE | _MUTABLE) +ST1 = state('ready', _MUTABLE, next=ST2) +ST0 = state('published', next=ST1) + +STATES = (ST0, ST1, ST2) + +@util.cachefunc +def laststatewithout(prop): + for state in STATES: + if not state.properties & prop: + candidate = state + else: + return candidate + +# util function +############################# +def noderange(repo, revsets): + return map(repo.changelog.node, + scmutil.revrange(repo, revsets)) + +# Patch changectx +############################# + +def state(ctx): + if ctx.node()is None: + return STATES[-1] + return ctx._repo.nodestate(ctx.node()) +context.changectx.state = state + +# improve template +############################# + +def showstate(ctx, **args): + return ctx.state() + + +# New commands +############################# + + +def cmdstates(ui, repo, *states, **opt): + """view and modify activated states. + + With no argument, list activated state. + + With argument, activate the state in argument. + + With argument plus the --off switch, deactivate the state in argument. + + note: published state are alway activated.""" + + if not states: + for st in sorted(repo._enabledstates): + ui.write('%s\n' % st) + else: + off = opt.get('off', False) + for state_name in states: + for st in STATES: + if st.name == state_name: + break + else: + ui.write_err(_('no state named %s\n') % state_name) + return 1 + if off and st in repo._enabledstates: + repo._enabledstates.remove(st) + else: + repo._enabledstates.add(st) + repo._writeenabledstates() + return 0 + +cmdtable = {'states': (cmdstates, [ ('', 'off', False, _('desactivate the state') )], '<state>')} +#cmdtable = {'states': (cmdstates, [], '<state>')} + +def makecmd(state): + def cmdmoveheads(ui, repo, *changesets): + """set a revision in %s state""" % state + revs = scmutil.revrange(repo, changesets) + repo.setstate(state, [repo.changelog.node(rev) for rev in revs]) + return 0 + return cmdmoveheads + +for state in STATES: + if state.trackheads: + cmdmoveheads = makecmd(state) + cmdtable[state.name] = (cmdmoveheads, [], '<revset>') + +# Pushkey mechanism for mutable +######################################### + +def pushimmutableheads(repo, key, old, new): + st = ST0 + w = repo.wlock() + try: + #print 'pushing', key + repo.setstate(ST0, [node.bin(key)]) + finally: + w.release() + +def listimmutableheads(repo): + return dict.fromkeys(map(node.hex, repo.stateheads(ST0)), '1') + +pushkey.register('immutableheads', pushimmutableheads, listimmutableheads) + + + + + +def uisetup(ui): + def filterprivateout(orig, repo, *args,**kwargs): + common, heads = orig(repo, *args, **kwargs) + return common, repo._reducehead(heads) + def filterprivatein(orig, repo, remote, *args, **kwargs): + common, anyinc, heads = orig(repo, remote, *args, **kwargs) + heads = remote._reducehead(heads) + return common, anyinc, heads + + extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout) + extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein) + + # Write protocols + #################### + def heads(repo, proto): + st = laststatewithout(_NOSHARE) + h = repo.stateheads(st) + return wireproto.encodelist(h) + "\n" + + def _reducehead(wirerepo, heads): + """heads filtering is done repo side""" + return heads + + wireproto.wirerepository._reducehead = _reducehead + wireproto.commands['heads'] = (heads, '') + + templatekw.keywords['state'] = showstate + +def extsetup(ui): + for state in STATES: + if state.trackheads: + revset.symbols[state.headssymbol] = state._revsetheads + +def reposetup(ui, repo): + + if not repo.local(): + return + + ocancopy =repo.cancopy + opull = repo.pull + opush = repo.push + o_tag = repo._tag + orollback = repo.rollback + o_writejournal = repo._writejournal + class statefulrepo(repo.__class__): + + def nodestate(self, node): + rev = self.changelog.rev(node) + + for state in STATES: + # XXX avoid for untracked heads + if state.next is not None: + ancestors = map(self.changelog.rev, self.stateheads(state)) + ancestors.extend(self.changelog.ancestors(*ancestors)) + if rev in ancestors: + break + return state + + + + def stateheads(self, state): + # look for a relevant state + while state.trackheads and state.next not in self._enabledstates: + state = state.next + # last state have no cached head. + if state.trackheads: + return self._statesheads[state] + return self.heads() + + @util.propertycache + def _statesheads(self): + return self._readstatesheads() + + + def _readheadsfile(self, filename): + heads = [nullid] + try: + f = self.opener(filename) + try: + heads = sorted([node.bin(n) for n in f.read().split() if n]) + finally: + f.close() + except IOError: + pass + return heads + + def _readstatesheads(self, undo=False): + statesheads = {} + for state in STATES: + if state.trackheads: + filemask = 'states/%s-heads' + filename = filemask % state.name + statesheads[state] = self._readheadsfile(filename) + return statesheads + + def _writeheadsfile(self, filename, heads): + f = self.opener(filename, 'w', atomictemp=True) + try: + for h in heads: + f.write(hex(h) + '\n') + f.rename() + finally: + f.close() + + def _writestateshead(self): + # transaction! + for state in STATES: + if state.trackheads: + filename = 'states/%s-heads' % state.name + self._writeheadsfile(filename, self._statesheads[state]) + + def setstate(self, state, nodes): + """change state of targets changeset and it's ancestors. + + Simplify the list of head.""" + assert not isinstance(nodes, basestring) + heads = self._statesheads[state] + olds = heads[:] + heads.extend(nodes) + heads[:] = set(heads) + heads.sort() + if olds != heads: + heads[:] = noderange(repo, ["heads(::%s())" % state.headssymbol]) + heads.sort() + if olds != heads: + self._writestateshead() + if state.next is not None and state.next.trackheads: + self.setstate(state.next, nodes) # cascading + + def _reducehead(self, candidates): + selected = set() + st = laststatewithout(_NOSHARE) + candidates = set(map(self.changelog.rev, candidates)) + heads = set(map(self.changelog.rev, self.stateheads(st))) + shareable = set(self.changelog.ancestors(*heads)) + shareable.update(heads) + selected = candidates & shareable + unselected = candidates - shareable + for rev in unselected: + for revh in heads: + if self.changelog.descendant(revh, rev): + selected.add(revh) + return sorted(map(self.changelog.node, selected)) + + ### enable // disable logic + + @util.propertycache + def _enabledstates(self): + return self._readenabledstates() + + def _readenabledstates(self): + states = set() + states.add(ST0) + mapping = dict([(st.name, st) for st in STATES]) + try: + f = self.opener('states/Enabled') + for line in f: + st = mapping.get(line.strip()) + if st is not None: + states.add(st) + finally: + return states + + def _writeenabledstates(self): + f = self.opener('states/Enabled', 'w', atomictemp=True) + try: + for st in self._enabledstates: + f.write(st.name + '\n') + f.rename() + finally: + f.close() + + ### local clone support + + def cancopy(self): + st = laststatewithout(_NOSHARE) + return ocancopy() and (self.stateheads(st) == self.heads()) + + ### pull // push support + + def pull(self, remote, *args, **kwargs): + result = opull(remote, *args, **kwargs) + remoteheads = self._pullimmutableheads(remote) + #print [node.short(h) for h in remoteheads] + self.setstate(ST0, remoteheads) + return result + + def push(self, remote, *args, **opts): + result = opush(remote, *args, **opts) + remoteheads = self._pullimmutableheads(remote) + self.setstate(ST0, remoteheads) + if remoteheads != self.stateheads(ST0): + #print 'stuff to push' + #print 'remote', [node.short(h) for h in remoteheads] + #print 'local', [node.short(h) for h in self._statesheads[ST0]] + self._pushimmutableheads(remote, remoteheads) + return result + + def _pushimmutableheads(self, remote, remoteheads): + missing = set(self.stateheads(ST0)) - set(remoteheads) + for h in missing: + #print 'missing', node.short(h) + remote.pushkey('immutableheads', node.hex(h), '', '1') + + + def _pullimmutableheads(self, remote): + self.ui.debug('checking for immutableheadshg on server') + if 'immutableheads' not in remote.listkeys('namespaces'): + self.ui.debug('immutableheads not enabled on the remote server, ' + 'marking everything as frozen') + remote = remote.heads() + else: + self.ui.debug('server has immutableheads enabled, merging lists') + remote = map(node.bin, remote.listkeys('immutableheads')) + return remote + + ### Tag support + + def _tag(self, names, node, *args, **kwargs): + tagnode = o_tag(names, node, *args, **kwargs) + if tagnode is not None: # do nothing for local one + self.setstate(ST0, [node, tagnode]) + return tagnode + + ### rollback support + + def _writejournal(self, desc): + entries = list(o_writejournal(desc)) + for state in STATES: + if state.trackheads: + filename = 'states/%s-heads' % state.name + filepath = self.join(filename) + if os.path.exists(filepath): + journalname = 'states/journal.%s-heads' % state.name + journalpath = self.join(journalname) + util.copyfile(filepath, journalpath) + entries.append(journalpath) + return tuple(entries) + + def rollback(self, dryrun=False): + wlock = lock = None + try: + wlock = self.wlock() + lock = self.lock() + ret = orollback(dryrun) + if not (ret or dryrun): #rollback did not failed + for state in STATES: + if state.trackheads: + src = self.join('states/undo.%s-heads') % state.name + dest = self.join('states/%s-heads') % state.name + if os.path.exists(src): + util.rename(src, dest) + elif os.path.exists(dest): #unlink in any case + os.unlink(dest) + self.__dict__.pop('_statesheads', None) + return ret + finally: + release(lock, wlock) + + repo.__class__ = statefulrepo +
--- a/obsolete.py Thu Sep 08 17:11:31 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,237 +0,0 @@ -# obsolete.py - introduce the obsolete concept in mercurial. -# -# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org> -# Logilab SA <contact@logilab.fr> -# -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. - -from mercurial import util -from mercurial import context -from mercurial import revset -from mercurial import scmutil -from mercurial import extensions -from mercurial import pushkey -from mercurial import discovery -from mercurial import error -from mercurial.node import hex, bin - -# Patch changectx -############################# - -def obsolete(ctx): - """is the changeset obsolete by other""" - if ctx.node()is None: - return False - return bool(ctx._repo.obsoletedby(ctx.node())) - -context.changectx.obsolete = obsolete - -ohidden = context.changectx.hidden -def hidden(ctx): - # hack to fill hiddenrevs - # compute hidden (XXX should move elsewhere) - if not getattr(ctx._repo.changelog, 'hiddeninit', False): - basicquery = 'obsolete() - (ancestors(not obsolete() or . or bookmark()))' - for rev in scmutil.revrange(ctx._repo, [basicquery]): - ctx._repo.changelog.hiddenrevs.add(rev) - ctx._repo.changelog.hiddeninit = True - - return ohidden(ctx) -context.changectx.hidden = hidden - -# revset -############################# - -def revsetobsolete(repo, subset, x): - args = revset.getargs(x, 0, 0, 'publicheads takes no arguments') - return [r for r in subset if repo[r].obsolete()] # XXX slow - -def extsetup(ui): - revset.symbols["obsolete"] = revsetobsolete - - def filterobsoleteout(orig, repo, remote, *args,**kwargs): - common, heads = orig(repo, remote, *args, **kwargs) - - # filter obsolete - heads = set(map(repo.changelog.rev, heads)) - obsoletes = set() - for obj in repo._obsobjrels: - try: - obsoletes.add(repo.changelog.rev(obj)) - except error.LookupError: - pass # we don't have this node locally - - outgoing = set(repo.changelog.ancestors(*heads)) - outgoing.update(heads) - - selected = outgoing - obsoletes - heads = sorted(map(repo.changelog.node, selected)) - - return common, heads - - extensions.wrapfunction(discovery, 'findcommonoutgoing', filterobsoleteout) - - try: - rebase = extensions.find('rebase') - if rebase: - extensions.wrapfunction(rebase, 'concludenode', concludenode) - except KeyError: - pass # rebase not found - -# Pushkey mechanism for mutable -######################################### - -def pushobsolete(repo, key, old, relations): - assert key == "relations" - w = repo.wlock() - try: - for sub, objs in relations.iteritems(): - for obj in objs: - repo.addobsolete(sub, obj) - finally: - w.release() - -def listobsolete(repo): - return {'relations': repo._obssubrels} - -pushkey.register('obsolete', pushobsolete, listobsolete) - -# New commands -############################# - - -def cmddebugobsolete(ui, repo, subject, object): - """Add an obsolete relation between a too node - - The subject is expected to be a newer version of the object""" - sub = repo[subject] - obj = repo[object] - repo.addobsolete(sub.node(), obj.node()) - return 0 - -cmdtable = {'debugobsolete': (cmddebugobsolete, [], '<subject> <object>')} - -def reposetup(ui, repo): - - if not repo.local(): - return - - opull = repo.pull - opush = repo.push - - class obsoletingrepo(repo.__class__): - - - ### Hidden revision support - @util.propertycache - def hiddenrevs(self): - # It's a property because It simpler that to handle the __init__ - revs = set() - return revs - - ### obsolete storage - @util.propertycache - def _obsobjrels(self): - """{<old-node> -> set(<new-node>)} - - also compute hidden revision""" - #reverse sub -> objs mapping - objrels = {} - for sub, objs in self._obssubrels.iteritems(): - for obj in objs: - objrels.setdefault(obj, set()).add(sub) - return objrels - - @util.propertycache - def _obssubrels(self): - """{<new-node> -> set(<old-node>)}""" - return self._readobsrels() - - - ### Disk IO - def _readobsrels(self): - """Write obsolete relation on disk""" - # XXX handle lock - rels = {} - try: - f = self.opener('obsolete-relations') - try: - for line in f: - subhex, objhex = line.split() - rels.setdefault(bin(subhex), set()).add(bin(objhex)) - finally: - f.close() - except IOError: - pass - return rels - - def _writeobsrels(self): - """Write obsolete relation on disk""" - # XXX handle lock - f = self.opener('obsolete-relations', 'w', atomictemp=True) - try: - for sub, objs in self._obssubrels.iteritems(): - for obj in objs: - f.write('%s %s\n' % (hex(sub), hex(obj))) - f.rename() - finally: - f.close() - - ### local clone support - - def cancopy(self): - return not bool(self._obsobjrels) # you can't copy if there is obsolete - - ### pull // push support - - def pull(self, remote, *args, **kwargs): - obskey = remote.listkeys('obsolete') - obsrels = obskey.get('relations', {}) - result = opull(remote, *args, **kwargs) - for sub, objs in obsrels.iteritems(): - for obj in objs: - self.addobsolete(sub, obj) - return result - - def push(self, remote, *args, **opts): - obskey = remote.listkeys('obsolete') - obssupport = 'relations' in obskey - result = opush(remote, *args, **opts) - if obssupport: - remote.pushkey('obsolete', 'relations', {}, self._obssubrels) - return result - - - ### Public method - def obsoletedby(self, node): - """return the set of node that make <node> obsolete (obj)""" - return self._obsobjrels.get(node, set()) - - def obsolete(self, node): - """return the set of node that <node> make obsolete (sub)""" - return self._obssubrels.get(node, set()) - - def addobsolete(self, sub, obj): - """Add a relation marking that node <sub> is a new version of <obj>""" - self._obssubrels.setdefault(sub, set()).add(obj) - self._obsobjrels.setdefault(obj, set()).add(sub) - try: - self.changelog.hiddenrevs.add(repo[obj].rev()) - except error.RepoLookupError: - pass #unknow revision (but keep propagating the data - self._writeobsrels() - - repo.__class__ = obsoletingrepo - - -### Other Extension compat -############################ - -def concludenode(orig, repo, rev, *args, **kwargs): - newrev = orig(repo, rev, *args, **kwargs) - oldnode = repo[rev].node() - newnode = repo[newrev].node() - repo.addobsolete(newnode, oldnode) - return newrev -
--- a/states.py Thu Sep 08 17:11:31 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,452 +0,0 @@ -# states.py - introduce the state concept for mercurial changeset -# -# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org> -# Logilab SA <contact@logilab.fr> -# Augie Fackler <durin42@gmail.com> -# -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. - -'''introduce the state concept for mercurial changeset - -Change can be in the following state: - -0 immutable -1 mutable -2 private - -name are not fixed yet. -''' -import os -from functools import partial - -from mercurial.i18n import _ -from mercurial import cmdutil -from mercurial import scmutil -from mercurial import context -from mercurial import revset -from mercurial import templatekw -from mercurial import util -from mercurial import node -from mercurial.node import nullid, hex, short -from mercurial import discovery -from mercurial import extensions -from mercurial import wireproto -from mercurial import pushkey -from mercurial.lock import release - - -_NOSHARE=2 -_MUTABLE=1 - -class state(object): - - def __init__(self, name, properties=0, next=None): - self.name = name - self.properties = properties - assert next is None or self < next - self.next = next - - def __repr__(self): - return 'state(%s)' % self.name - - def __str__(self): - return self.name - - @util.propertycache - def trackheads(self): - """Do we need to track heads of changeset in this state ? - - We don't need to track heads for the last state as this is repos heads""" - return self.next is not None - - def __cmp__(self, other): - return cmp(self.properties, other.properties) - - @util.propertycache - def _revsetheads(self): - """function to be used by revset to finds heads of this states""" - assert self.trackheads - def revsetheads(repo, subset, x): - args = revset.getargs(x, 0, 0, 'publicheads takes no arguments') - heads = map(repo.changelog.rev, repo._statesheads[self]) - heads.sort() - return heads - return revsetheads - - @util.propertycache - def headssymbol(self): - """name of the revset symbols""" - if self.trackheads: - return "%sheads" % self.name - else: - return 'heads' - -ST2 = state('draft', _NOSHARE | _MUTABLE) -ST1 = state('ready', _MUTABLE, next=ST2) -ST0 = state('published', next=ST1) - -STATES = (ST0, ST1, ST2) - -@util.cachefunc -def laststatewithout(prop): - for state in STATES: - if not state.properties & prop: - candidate = state - else: - return candidate - -# util function -############################# -def noderange(repo, revsets): - return map(repo.changelog.node, - scmutil.revrange(repo, revsets)) - -# Patch changectx -############################# - -def state(ctx): - if ctx.node()is None: - return STATES[-1] - return ctx._repo.nodestate(ctx.node()) -context.changectx.state = state - -# improve template -############################# - -def showstate(ctx, **args): - return ctx.state() - - -# New commands -############################# - - -def cmdstates(ui, repo, *states, **opt): - """view and modify activated states. - - With no argument, list activated state. - - With argument, activate the state in argument. - - With argument plus the --off switch, deactivate the state in argument. - - note: published state are alway activated.""" - - if not states: - for st in sorted(repo._enabledstates): - ui.write('%s\n' % st) - else: - off = opt.get('off', False) - for state_name in states: - for st in STATES: - if st.name == state_name: - break - else: - ui.write_err(_('no state named %s\n') % state_name) - return 1 - if off and st in repo._enabledstates: - repo._enabledstates.remove(st) - else: - repo._enabledstates.add(st) - repo._writeenabledstates() - return 0 - -cmdtable = {'states': (cmdstates, [ ('', 'off', False, _('desactivate the state') )], '<state>')} -#cmdtable = {'states': (cmdstates, [], '<state>')} - -def makecmd(state): - def cmdmoveheads(ui, repo, *changesets): - """set a revision in %s state""" % state - revs = scmutil.revrange(repo, changesets) - repo.setstate(state, [repo.changelog.node(rev) for rev in revs]) - return 0 - return cmdmoveheads - -for state in STATES: - if state.trackheads: - cmdmoveheads = makecmd(state) - cmdtable[state.name] = (cmdmoveheads, [], '<revset>') - -# Pushkey mechanism for mutable -######################################### - -def pushimmutableheads(repo, key, old, new): - st = ST0 - w = repo.wlock() - try: - #print 'pushing', key - repo.setstate(ST0, [node.bin(key)]) - finally: - w.release() - -def listimmutableheads(repo): - return dict.fromkeys(map(node.hex, repo.stateheads(ST0)), '1') - -pushkey.register('immutableheads', pushimmutableheads, listimmutableheads) - - - - - -def uisetup(ui): - def filterprivateout(orig, repo, *args,**kwargs): - common, heads = orig(repo, *args, **kwargs) - return common, repo._reducehead(heads) - def filterprivatein(orig, repo, remote, *args, **kwargs): - common, anyinc, heads = orig(repo, remote, *args, **kwargs) - heads = remote._reducehead(heads) - return common, anyinc, heads - - extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout) - extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein) - - # Write protocols - #################### - def heads(repo, proto): - st = laststatewithout(_NOSHARE) - h = repo.stateheads(st) - return wireproto.encodelist(h) + "\n" - - def _reducehead(wirerepo, heads): - """heads filtering is done repo side""" - return heads - - wireproto.wirerepository._reducehead = _reducehead - wireproto.commands['heads'] = (heads, '') - - templatekw.keywords['state'] = showstate - -def extsetup(ui): - for state in STATES: - if state.trackheads: - revset.symbols[state.headssymbol] = state._revsetheads - -def reposetup(ui, repo): - - if not repo.local(): - return - - ocancopy =repo.cancopy - opull = repo.pull - opush = repo.push - o_tag = repo._tag - orollback = repo.rollback - o_writejournal = repo._writejournal - class statefulrepo(repo.__class__): - - def nodestate(self, node): - rev = self.changelog.rev(node) - - for state in STATES: - # XXX avoid for untracked heads - if state.next is not None: - ancestors = map(self.changelog.rev, self.stateheads(state)) - ancestors.extend(self.changelog.ancestors(*ancestors)) - if rev in ancestors: - break - return state - - - - def stateheads(self, state): - # look for a relevant state - while state.trackheads and state.next not in self._enabledstates: - state = state.next - # last state have no cached head. - if state.trackheads: - return self._statesheads[state] - return self.heads() - - @util.propertycache - def _statesheads(self): - return self._readstatesheads() - - - def _readheadsfile(self, filename): - heads = [nullid] - try: - f = self.opener(filename) - try: - heads = sorted([node.bin(n) for n in f.read().split() if n]) - finally: - f.close() - except IOError: - pass - return heads - - def _readstatesheads(self, undo=False): - statesheads = {} - for state in STATES: - if state.trackheads: - filemask = 'states/%s-heads' - filename = filemask % state.name - statesheads[state] = self._readheadsfile(filename) - return statesheads - - def _writeheadsfile(self, filename, heads): - f = self.opener(filename, 'w', atomictemp=True) - try: - for h in heads: - f.write(hex(h) + '\n') - f.rename() - finally: - f.close() - - def _writestateshead(self): - # transaction! - for state in STATES: - if state.trackheads: - filename = 'states/%s-heads' % state.name - self._writeheadsfile(filename, self._statesheads[state]) - - def setstate(self, state, nodes): - """change state of targets changeset and it's ancestors. - - Simplify the list of head.""" - assert not isinstance(nodes, basestring) - heads = self._statesheads[state] - olds = heads[:] - heads.extend(nodes) - heads[:] = set(heads) - heads.sort() - if olds != heads: - heads[:] = noderange(repo, ["heads(::%s())" % state.headssymbol]) - heads.sort() - if olds != heads: - self._writestateshead() - if state.next is not None and state.next.trackheads: - self.setstate(state.next, nodes) # cascading - - def _reducehead(self, candidates): - selected = set() - st = laststatewithout(_NOSHARE) - candidates = set(map(self.changelog.rev, candidates)) - heads = set(map(self.changelog.rev, self.stateheads(st))) - shareable = set(self.changelog.ancestors(*heads)) - shareable.update(heads) - selected = candidates & shareable - unselected = candidates - shareable - for rev in unselected: - for revh in heads: - if self.changelog.descendant(revh, rev): - selected.add(revh) - return sorted(map(self.changelog.node, selected)) - - ### enable // disable logic - - @util.propertycache - def _enabledstates(self): - return self._readenabledstates() - - def _readenabledstates(self): - states = set() - states.add(ST0) - mapping = dict([(st.name, st) for st in STATES]) - try: - f = self.opener('states/Enabled') - for line in f: - st = mapping.get(line.strip()) - if st is not None: - states.add(st) - finally: - return states - - def _writeenabledstates(self): - f = self.opener('states/Enabled', 'w', atomictemp=True) - try: - for st in self._enabledstates: - f.write(st.name + '\n') - f.rename() - finally: - f.close() - - ### local clone support - - def cancopy(self): - st = laststatewithout(_NOSHARE) - return ocancopy() and (self.stateheads(st) == self.heads()) - - ### pull // push support - - def pull(self, remote, *args, **kwargs): - result = opull(remote, *args, **kwargs) - remoteheads = self._pullimmutableheads(remote) - #print [node.short(h) for h in remoteheads] - self.setstate(ST0, remoteheads) - return result - - def push(self, remote, *args, **opts): - result = opush(remote, *args, **opts) - remoteheads = self._pullimmutableheads(remote) - self.setstate(ST0, remoteheads) - if remoteheads != self.stateheads(ST0): - #print 'stuff to push' - #print 'remote', [node.short(h) for h in remoteheads] - #print 'local', [node.short(h) for h in self._statesheads[ST0]] - self._pushimmutableheads(remote, remoteheads) - return result - - def _pushimmutableheads(self, remote, remoteheads): - missing = set(self.stateheads(ST0)) - set(remoteheads) - for h in missing: - #print 'missing', node.short(h) - remote.pushkey('immutableheads', node.hex(h), '', '1') - - - def _pullimmutableheads(self, remote): - self.ui.debug('checking for immutableheadshg on server') - if 'immutableheads' not in remote.listkeys('namespaces'): - self.ui.debug('immutableheads not enabled on the remote server, ' - 'marking everything as frozen') - remote = remote.heads() - else: - self.ui.debug('server has immutableheads enabled, merging lists') - remote = map(node.bin, remote.listkeys('immutableheads')) - return remote - - ### Tag support - - def _tag(self, names, node, *args, **kwargs): - tagnode = o_tag(names, node, *args, **kwargs) - if tagnode is not None: # do nothing for local one - self.setstate(ST0, [node, tagnode]) - return tagnode - - ### rollback support - - def _writejournal(self, desc): - entries = list(o_writejournal(desc)) - for state in STATES: - if state.trackheads: - filename = 'states/%s-heads' % state.name - filepath = self.join(filename) - if os.path.exists(filepath): - journalname = 'states/journal.%s-heads' % state.name - journalpath = self.join(journalname) - util.copyfile(filepath, journalpath) - entries.append(journalpath) - return tuple(entries) - - def rollback(self, dryrun=False): - wlock = lock = None - try: - wlock = self.wlock() - lock = self.lock() - ret = orollback(dryrun) - if not (ret or dryrun): #rollback did not failed - for state in STATES: - if state.trackheads: - src = self.join('states/undo.%s-heads') % state.name - dest = self.join('states/%s-heads') % state.name - if os.path.exists(src): - util.rename(src, dest) - elif os.path.exists(dest): #unlink in any case - os.unlink(dest) - self.__dict__.pop('_statesheads', None) - return ret - finally: - release(lock, wlock) - - repo.__class__ = statefulrepo -
--- a/tests/test-draft.t Thu Sep 08 17:11:31 2011 +0200 +++ b/tests/test-draft.t Thu Sep 08 17:15:20 2011 +0200 @@ -4,7 +4,7 @@ > allow_push = * > [extensions] > EOF - $ echo "states=$(echo $(dirname $TESTDIR))/states.py" >> $HGRCPATH + $ echo "states=$(echo $(dirname $TESTDIR))/hgext/states.py" >> $HGRCPATH $ hg init local $ hg init remote1
--- a/tests/test-obsolete.t Thu Sep 08 17:11:31 2011 +0200 +++ b/tests/test-obsolete.t Thu Sep 08 17:15:20 2011 +0200 @@ -4,7 +4,7 @@ > allow_push = * > [extensions] > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/obsolete.py" >> $HGRCPATH + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1"
--- a/tests/test-ready.t Thu Sep 08 17:11:31 2011 +0200 +++ b/tests/test-ready.t Thu Sep 08 17:15:20 2011 +0200 @@ -4,7 +4,7 @@ > [extensions] > graphlog= > EOF - $ echo "states=$(echo $(dirname $TESTDIR))/states.py" >> $HGRCPATH + $ echo "states=$(echo $(dirname $TESTDIR))/hgext/states.py" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1"
--- a/tests/test-states.t Thu Sep 08 17:11:31 2011 +0200 +++ b/tests/test-states.t Thu Sep 08 17:15:20 2011 +0200 @@ -5,7 +5,7 @@ > allow_push = * > [extensions] > EOF - $ echo "states=$(echo $(dirname $TESTDIR))/states.py" >> $HGRCPATH + $ echo "states=$(echo $(dirname $TESTDIR))/hgext/states.py" >> $HGRCPATH $ hg init local $ hg init other