Add a few information about obsolete relation.
# 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