hgext/states.py
changeset 51 d98e06ab8320
parent 33 dca86448d736
child 54 ad1a4fb0fc49
equal deleted inserted replaced
50:19b22ad56b32 51:d98e06ab8320
       
     1 # states.py - introduce the state concept for mercurial changeset
       
     2 #
       
     3 # Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
       
     4 #                Logilab SA        <contact@logilab.fr>
       
     5 #                Augie Fackler     <durin42@gmail.com>
       
     6 #
       
     7 # This software may be used and distributed according to the terms of the
       
     8 # GNU General Public License version 2 or any later version.
       
     9 
       
    10 '''introduce the state concept for mercurial changeset
       
    11 
       
    12 Change can be in the following state:
       
    13 
       
    14 0 immutable
       
    15 1 mutable
       
    16 2 private
       
    17 
       
    18 name are not fixed yet.
       
    19 '''
       
    20 import os
       
    21 from functools import partial
       
    22 
       
    23 from mercurial.i18n import _
       
    24 from mercurial import cmdutil
       
    25 from mercurial import scmutil
       
    26 from mercurial import context
       
    27 from mercurial import revset
       
    28 from mercurial import templatekw
       
    29 from mercurial import util
       
    30 from mercurial import node
       
    31 from mercurial.node import nullid, hex, short
       
    32 from mercurial import discovery
       
    33 from mercurial import extensions
       
    34 from mercurial import wireproto
       
    35 from mercurial import pushkey
       
    36 from mercurial.lock import release
       
    37 
       
    38 
       
    39 _NOSHARE=2
       
    40 _MUTABLE=1
       
    41 
       
    42 class state(object):
       
    43 
       
    44     def __init__(self, name, properties=0, next=None):
       
    45         self.name = name
       
    46         self.properties = properties
       
    47         assert next is None or self < next
       
    48         self.next = next
       
    49 
       
    50     def __repr__(self):
       
    51         return 'state(%s)' % self.name
       
    52 
       
    53     def __str__(self):
       
    54         return self.name
       
    55 
       
    56     @util.propertycache
       
    57     def trackheads(self):
       
    58         """Do we need to track heads of changeset in this state ?
       
    59 
       
    60         We don't need to track heads for the last state as this is repos heads"""
       
    61         return self.next is not None
       
    62 
       
    63     def __cmp__(self, other):
       
    64         return cmp(self.properties, other.properties)
       
    65 
       
    66     @util.propertycache
       
    67     def _revsetheads(self):
       
    68         """function to be used by revset to finds heads of this states"""
       
    69         assert self.trackheads
       
    70         def revsetheads(repo, subset, x):
       
    71             args = revset.getargs(x, 0, 0, 'publicheads takes no arguments')
       
    72             heads = map(repo.changelog.rev, repo._statesheads[self])
       
    73             heads.sort()
       
    74             return heads
       
    75         return revsetheads
       
    76 
       
    77     @util.propertycache
       
    78     def headssymbol(self):
       
    79         """name of the revset symbols"""
       
    80         if self.trackheads:
       
    81             return "%sheads" % self.name
       
    82         else:
       
    83             return 'heads'
       
    84 
       
    85 ST2 = state('draft', _NOSHARE | _MUTABLE)
       
    86 ST1 = state('ready', _MUTABLE, next=ST2)
       
    87 ST0 = state('published', next=ST1)
       
    88 
       
    89 STATES = (ST0, ST1, ST2)
       
    90 
       
    91 @util.cachefunc
       
    92 def laststatewithout(prop):
       
    93     for state in STATES:
       
    94         if not state.properties & prop:
       
    95             candidate = state
       
    96         else:
       
    97             return candidate
       
    98 
       
    99 # util function
       
   100 #############################
       
   101 def noderange(repo, revsets):
       
   102     return map(repo.changelog.node,
       
   103                scmutil.revrange(repo, revsets))
       
   104 
       
   105 # Patch changectx
       
   106 #############################
       
   107 
       
   108 def state(ctx):
       
   109     if ctx.node()is None:
       
   110         return STATES[-1]
       
   111     return ctx._repo.nodestate(ctx.node())
       
   112 context.changectx.state = state
       
   113 
       
   114 # improve template
       
   115 #############################
       
   116 
       
   117 def showstate(ctx, **args):
       
   118     return ctx.state()
       
   119 
       
   120 
       
   121 # New commands
       
   122 #############################
       
   123 
       
   124 
       
   125 def cmdstates(ui, repo, *states, **opt):
       
   126     """view and modify activated states.
       
   127 
       
   128     With no argument, list activated state.
       
   129 
       
   130     With argument, activate the state in argument.
       
   131 
       
   132     With argument plus the --off switch, deactivate the state in argument.
       
   133 
       
   134     note: published state are alway activated."""
       
   135 
       
   136     if not states:
       
   137         for st in sorted(repo._enabledstates):
       
   138             ui.write('%s\n' % st)
       
   139     else:
       
   140         off = opt.get('off', False)
       
   141         for state_name in states:
       
   142             for st in STATES:
       
   143                 if st.name == state_name:
       
   144                     break
       
   145             else:
       
   146                 ui.write_err(_('no state named %s\n') % state_name)
       
   147                 return 1
       
   148             if off and st in repo._enabledstates:
       
   149                 repo._enabledstates.remove(st)
       
   150             else:
       
   151                 repo._enabledstates.add(st)
       
   152         repo._writeenabledstates()
       
   153     return 0
       
   154 
       
   155 cmdtable = {'states': (cmdstates, [ ('', 'off', False, _('desactivate the state') )], '<state>')}
       
   156 #cmdtable = {'states': (cmdstates, [], '<state>')}
       
   157 
       
   158 def makecmd(state):
       
   159     def cmdmoveheads(ui, repo, *changesets):
       
   160         """set a revision in %s state""" % state
       
   161         revs = scmutil.revrange(repo, changesets)
       
   162         repo.setstate(state, [repo.changelog.node(rev) for rev in revs])
       
   163         return 0
       
   164     return cmdmoveheads
       
   165 
       
   166 for state in STATES:
       
   167     if state.trackheads:
       
   168         cmdmoveheads = makecmd(state)
       
   169         cmdtable[state.name] = (cmdmoveheads, [], '<revset>')
       
   170 
       
   171 # Pushkey mechanism for mutable
       
   172 #########################################
       
   173 
       
   174 def pushimmutableheads(repo, key, old, new):
       
   175     st = ST0
       
   176     w = repo.wlock()
       
   177     try:
       
   178         #print 'pushing', key
       
   179         repo.setstate(ST0, [node.bin(key)])
       
   180     finally:
       
   181         w.release()
       
   182 
       
   183 def listimmutableheads(repo):
       
   184     return dict.fromkeys(map(node.hex, repo.stateheads(ST0)), '1')
       
   185 
       
   186 pushkey.register('immutableheads', pushimmutableheads, listimmutableheads)
       
   187 
       
   188 
       
   189 
       
   190 
       
   191 
       
   192 def uisetup(ui):
       
   193     def filterprivateout(orig, repo, *args,**kwargs):
       
   194         common, heads = orig(repo, *args, **kwargs)
       
   195         return common, repo._reducehead(heads)
       
   196     def filterprivatein(orig, repo, remote, *args, **kwargs):
       
   197         common, anyinc, heads = orig(repo, remote, *args, **kwargs)
       
   198         heads = remote._reducehead(heads)
       
   199         return common, anyinc, heads
       
   200 
       
   201     extensions.wrapfunction(discovery, 'findcommonoutgoing', filterprivateout)
       
   202     extensions.wrapfunction(discovery, 'findcommonincoming', filterprivatein)
       
   203 
       
   204     # Write protocols
       
   205     ####################
       
   206     def heads(repo, proto):
       
   207         st = laststatewithout(_NOSHARE)
       
   208         h = repo.stateheads(st)
       
   209         return wireproto.encodelist(h) + "\n"
       
   210 
       
   211     def _reducehead(wirerepo, heads):
       
   212         """heads filtering is done repo side"""
       
   213         return heads
       
   214 
       
   215     wireproto.wirerepository._reducehead = _reducehead
       
   216     wireproto.commands['heads'] = (heads, '')
       
   217 
       
   218     templatekw.keywords['state'] = showstate
       
   219 
       
   220 def extsetup(ui):
       
   221     for state in STATES:
       
   222         if state.trackheads:
       
   223             revset.symbols[state.headssymbol] = state._revsetheads
       
   224 
       
   225 def reposetup(ui, repo):
       
   226 
       
   227     if not repo.local():
       
   228         return
       
   229 
       
   230     ocancopy =repo.cancopy
       
   231     opull = repo.pull
       
   232     opush = repo.push
       
   233     o_tag = repo._tag
       
   234     orollback = repo.rollback
       
   235     o_writejournal = repo._writejournal
       
   236     class statefulrepo(repo.__class__):
       
   237 
       
   238         def nodestate(self, node):
       
   239             rev = self.changelog.rev(node)
       
   240 
       
   241             for state in STATES:
       
   242                 # XXX avoid for untracked heads
       
   243                 if state.next is not None:
       
   244                     ancestors = map(self.changelog.rev, self.stateheads(state))
       
   245                     ancestors.extend(self.changelog.ancestors(*ancestors))
       
   246                     if rev in ancestors:
       
   247                         break
       
   248             return state
       
   249 
       
   250 
       
   251 
       
   252         def stateheads(self, state):
       
   253             # look for a relevant state
       
   254             while state.trackheads and state.next not in self._enabledstates:
       
   255                 state = state.next
       
   256             # last state have no cached head.
       
   257             if state.trackheads:
       
   258                 return self._statesheads[state]
       
   259             return self.heads()
       
   260 
       
   261         @util.propertycache
       
   262         def _statesheads(self):
       
   263             return self._readstatesheads()
       
   264 
       
   265 
       
   266         def _readheadsfile(self, filename):
       
   267             heads = [nullid]
       
   268             try:
       
   269                 f = self.opener(filename)
       
   270                 try:
       
   271                     heads = sorted([node.bin(n) for n in f.read().split() if n])
       
   272                 finally:
       
   273                     f.close()
       
   274             except IOError:
       
   275                 pass
       
   276             return heads
       
   277 
       
   278         def _readstatesheads(self, undo=False):
       
   279             statesheads = {}
       
   280             for state in STATES:
       
   281                 if state.trackheads:
       
   282                     filemask = 'states/%s-heads'
       
   283                     filename = filemask % state.name
       
   284                     statesheads[state] = self._readheadsfile(filename)
       
   285             return statesheads
       
   286 
       
   287         def _writeheadsfile(self, filename, heads):
       
   288             f = self.opener(filename, 'w', atomictemp=True)
       
   289             try:
       
   290                 for h in heads:
       
   291                     f.write(hex(h) + '\n')
       
   292                 f.rename()
       
   293             finally:
       
   294                 f.close()
       
   295 
       
   296         def _writestateshead(self):
       
   297             # transaction!
       
   298             for state in STATES:
       
   299                 if state.trackheads:
       
   300                     filename = 'states/%s-heads' % state.name
       
   301                     self._writeheadsfile(filename, self._statesheads[state])
       
   302 
       
   303         def setstate(self, state, nodes):
       
   304             """change state of targets changeset and it's ancestors.
       
   305 
       
   306             Simplify the list of head."""
       
   307             assert not isinstance(nodes, basestring)
       
   308             heads = self._statesheads[state]
       
   309             olds = heads[:]
       
   310             heads.extend(nodes)
       
   311             heads[:] = set(heads)
       
   312             heads.sort()
       
   313             if olds != heads:
       
   314                 heads[:] = noderange(repo, ["heads(::%s())" % state.headssymbol])
       
   315                 heads.sort()
       
   316             if olds != heads:
       
   317                 self._writestateshead()
       
   318             if state.next is not None and state.next.trackheads:
       
   319                 self.setstate(state.next, nodes) # cascading
       
   320 
       
   321         def _reducehead(self, candidates):
       
   322             selected = set()
       
   323             st = laststatewithout(_NOSHARE)
       
   324             candidates = set(map(self.changelog.rev, candidates))
       
   325             heads = set(map(self.changelog.rev, self.stateheads(st)))
       
   326             shareable = set(self.changelog.ancestors(*heads))
       
   327             shareable.update(heads)
       
   328             selected = candidates & shareable
       
   329             unselected = candidates - shareable
       
   330             for rev in unselected:
       
   331                 for revh in heads:
       
   332                     if self.changelog.descendant(revh, rev):
       
   333                         selected.add(revh)
       
   334             return sorted(map(self.changelog.node, selected))
       
   335 
       
   336         ### enable // disable logic
       
   337 
       
   338         @util.propertycache
       
   339         def _enabledstates(self):
       
   340             return self._readenabledstates()
       
   341 
       
   342         def _readenabledstates(self):
       
   343             states = set()
       
   344             states.add(ST0)
       
   345             mapping = dict([(st.name, st) for st in STATES])
       
   346             try:
       
   347                 f = self.opener('states/Enabled')
       
   348                 for line in f:
       
   349                     st =  mapping.get(line.strip())
       
   350                     if st is not None:
       
   351                         states.add(st)
       
   352             finally:
       
   353                 return states
       
   354 
       
   355         def _writeenabledstates(self):
       
   356             f = self.opener('states/Enabled', 'w', atomictemp=True)
       
   357             try:
       
   358                 for st in self._enabledstates:
       
   359                     f.write(st.name + '\n')
       
   360                 f.rename()
       
   361             finally:
       
   362                 f.close()
       
   363 
       
   364         ### local clone support
       
   365 
       
   366         def cancopy(self):
       
   367             st = laststatewithout(_NOSHARE)
       
   368             return ocancopy() and (self.stateheads(st) == self.heads())
       
   369 
       
   370         ### pull // push support
       
   371 
       
   372         def pull(self, remote, *args, **kwargs):
       
   373             result = opull(remote, *args, **kwargs)
       
   374             remoteheads = self._pullimmutableheads(remote)
       
   375             #print [node.short(h) for h in remoteheads]
       
   376             self.setstate(ST0, remoteheads)
       
   377             return result
       
   378 
       
   379         def push(self, remote, *args, **opts):
       
   380             result = opush(remote, *args, **opts)
       
   381             remoteheads = self._pullimmutableheads(remote)
       
   382             self.setstate(ST0, remoteheads)
       
   383             if remoteheads != self.stateheads(ST0):
       
   384                 #print 'stuff to push'
       
   385                 #print 'remote', [node.short(h) for h in remoteheads]
       
   386                 #print 'local',  [node.short(h) for h in self._statesheads[ST0]]
       
   387                 self._pushimmutableheads(remote, remoteheads)
       
   388             return result
       
   389 
       
   390         def _pushimmutableheads(self, remote, remoteheads):
       
   391             missing = set(self.stateheads(ST0)) - set(remoteheads)
       
   392             for h in missing:
       
   393                 #print 'missing', node.short(h)
       
   394                 remote.pushkey('immutableheads', node.hex(h), '', '1')
       
   395 
       
   396 
       
   397         def _pullimmutableheads(self, remote):
       
   398             self.ui.debug('checking for immutableheadshg on server')
       
   399             if 'immutableheads' not in remote.listkeys('namespaces'):
       
   400                 self.ui.debug('immutableheads not enabled on the remote server, '
       
   401                               'marking everything as frozen')
       
   402                 remote = remote.heads()
       
   403             else:
       
   404                 self.ui.debug('server has immutableheads enabled, merging lists')
       
   405                 remote = map(node.bin, remote.listkeys('immutableheads'))
       
   406             return remote
       
   407 
       
   408         ### Tag support
       
   409 
       
   410         def _tag(self, names, node, *args, **kwargs):
       
   411             tagnode = o_tag(names, node, *args, **kwargs)
       
   412             if tagnode is not None: # do nothing for local one
       
   413                 self.setstate(ST0, [node, tagnode])
       
   414             return tagnode
       
   415 
       
   416         ### rollback support
       
   417 
       
   418         def _writejournal(self, desc):
       
   419             entries = list(o_writejournal(desc))
       
   420             for state in STATES:
       
   421                 if state.trackheads:
       
   422                     filename = 'states/%s-heads' % state.name
       
   423                     filepath = self.join(filename)
       
   424                     if  os.path.exists(filepath):
       
   425                         journalname = 'states/journal.%s-heads' % state.name
       
   426                         journalpath = self.join(journalname)
       
   427                         util.copyfile(filepath, journalpath)
       
   428                         entries.append(journalpath)
       
   429             return tuple(entries)
       
   430 
       
   431         def rollback(self, dryrun=False):
       
   432             wlock = lock = None
       
   433             try:
       
   434                 wlock = self.wlock()
       
   435                 lock = self.lock()
       
   436                 ret = orollback(dryrun)
       
   437                 if not (ret or dryrun): #rollback did not failed
       
   438                     for state in STATES:
       
   439                         if state.trackheads:
       
   440                             src  = self.join('states/undo.%s-heads') % state.name
       
   441                             dest = self.join('states/%s-heads') % state.name
       
   442                             if os.path.exists(src):
       
   443                                 util.rename(src, dest)
       
   444                             elif os.path.exists(dest): #unlink in any case
       
   445                                 os.unlink(dest)
       
   446                     self.__dict__.pop('_statesheads', None)
       
   447                 return ret
       
   448             finally:
       
   449                 release(lock, wlock)
       
   450 
       
   451     repo.__class__ = statefulrepo
       
   452