diff options
Diffstat (limited to 'mercurial/phases.py')
-rw-r--r-- | mercurial/phases.py | 387 |
1 files changed, 0 insertions, 387 deletions
diff --git a/mercurial/phases.py b/mercurial/phases.py deleted file mode 100644 index 614bcbb..0000000 --- a/mercurial/phases.py +++ /dev/null @@ -1,387 +0,0 @@ -""" Mercurial phases support code - - --- - - 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. - - --- - -This module implements most phase logic in mercurial. - - -Basic Concept -============= - -A 'changeset phase' is an indicator that tells us how a changeset is -manipulated and communicated. The details of each phase is described -below, here we describe the properties they have in common. - -Like bookmarks, phases are not stored in history and thus are not -permanent and leave no audit trail. - -First, no changeset can be in two phases at once. Phases are ordered, -so they can be considered from lowest to highest. The default, lowest -phase is 'public' - this is the normal phase of existing changesets. A -child changeset can not be in a lower phase than its parents. - -These phases share a hierarchy of traits: - - immutable shared - public: X X - draft: X - secret: - -Local commits are draft by default. - -Phase Movement and Exchange -=========================== - -Phase data is exchanged by pushkey on pull and push. Some servers have -a publish option set, we call such a server a "publishing server". -Pushing a draft changeset to a publishing server changes the phase to -public. - -A small list of fact/rules define the exchange of phase: - -* old client never changes server states -* pull never changes server states -* publish and old server changesets are seen as public by client -* any secret changeset seen in another repository is lowered to at - least draft - -Here is the final table summing up the 49 possible use cases of phase -exchange: - - server - old publish non-publish - N X N D P N D P - old client - pull - N - X/X - X/D X/P - X/D X/P - X - X/X - X/D X/P - X/D X/P - push - X X/X X/X X/P X/P X/P X/D X/D X/P - new client - pull - N - P/X - P/D P/P - D/D P/P - D - P/X - P/D P/P - D/D P/P - P - P/X - P/D P/P - P/D P/P - push - D P/X P/X P/P P/P P/P D/D D/D P/P - P P/X P/X P/P P/P P/P P/P P/P P/P - -Legend: - - A/B = final state on client / state on server - - * N = new/not present, - * P = public, - * D = draft, - * X = not tracked (i.e., the old client or server has no internal - way of recording the phase.) - - passive = only pushes - - - A cell here can be read like this: - - "When a new client pushes a draft changeset (D) to a publishing - server where it's not present (N), it's marked public on both - sides (P/P)." - -Note: old client behave as a publishing server with draft only content -- other people see it as public -- content is pushed as draft - -""" - -import errno -from node import nullid, nullrev, bin, hex, short -from i18n import _ -import util - -allphases = public, draft, secret = range(3) -trackedphases = allphases[1:] -phasenames = ['public', 'draft', 'secret'] - -def _filterunknown(ui, changelog, phaseroots): - """remove unknown nodes from the phase boundary - - Nothing is lost as unknown nodes only hold data for their descendants. - """ - updated = False - nodemap = changelog.nodemap # to filter unknown nodes - for phase, nodes in enumerate(phaseroots): - missing = [node for node in nodes if node not in nodemap] - if missing: - for mnode in missing: - ui.debug( - 'removing unknown node %s from %i-phase boundary\n' - % (short(mnode), phase)) - nodes.symmetric_difference_update(missing) - updated = True - return updated - -def _readroots(repo, phasedefaults=None): - """Read phase roots from disk - - phasedefaults is a list of fn(repo, roots) callable, which are - executed if the phase roots file does not exist. When phases are - being initialized on an existing repository, this could be used to - set selected changesets phase to something else than public. - - Return (roots, dirty) where dirty is true if roots differ from - what is being stored. - """ - dirty = False - roots = [set() for i in allphases] - try: - f = repo.sopener('phaseroots') - try: - for line in f: - phase, nh = line.split() - roots[int(phase)].add(bin(nh)) - finally: - f.close() - except IOError, inst: - if inst.errno != errno.ENOENT: - raise - if phasedefaults: - for f in phasedefaults: - roots = f(repo, roots) - dirty = True - if _filterunknown(repo.ui, repo.changelog, roots): - dirty = True - return roots, dirty - -class phasecache(object): - def __init__(self, repo, phasedefaults, _load=True): - if _load: - # Cheap trick to allow shallow-copy without copy module - self.phaseroots, self.dirty = _readroots(repo, phasedefaults) - self.opener = repo.sopener - self._phaserevs = None - - def copy(self): - # Shallow copy meant to ensure isolation in - # advance/retractboundary(), nothing more. - ph = phasecache(None, None, _load=False) - ph.phaseroots = self.phaseroots[:] - ph.dirty = self.dirty - ph.opener = self.opener - ph._phaserevs = self._phaserevs - return ph - - def replace(self, phcache): - for a in 'phaseroots dirty opener _phaserevs'.split(): - setattr(self, a, getattr(phcache, a)) - - def getphaserevs(self, repo, rebuild=False): - if rebuild or self._phaserevs is None: - revs = [public] * len(repo.changelog) - for phase in trackedphases: - roots = map(repo.changelog.rev, self.phaseroots[phase]) - if roots: - for rev in roots: - revs[rev] = phase - for rev in repo.changelog.descendants(roots): - revs[rev] = phase - self._phaserevs = revs - return self._phaserevs - - def phase(self, repo, rev): - # We need a repo argument here to be able to build _phaserev - # if necessary. The repository instance is not stored in - # phasecache to avoid reference cycles. The changelog instance - # is not stored because it is a filecache() property and can - # be replaced without us being notified. - if rev == nullrev: - return public - if self._phaserevs is None or rev >= len(self._phaserevs): - self._phaserevs = self.getphaserevs(repo, rebuild=True) - return self._phaserevs[rev] - - def write(self): - if not self.dirty: - return - f = self.opener('phaseroots', 'w', atomictemp=True) - try: - for phase, roots in enumerate(self.phaseroots): - for h in roots: - f.write('%i %s\n' % (phase, hex(h))) - finally: - f.close() - self.dirty = False - - def _updateroots(self, phase, newroots): - self.phaseroots[phase] = newroots - self._phaserevs = None - self.dirty = True - - def advanceboundary(self, repo, targetphase, nodes): - # Be careful to preserve shallow-copied values: do not update - # phaseroots values, replace them. - - delroots = [] # set of root deleted by this path - for phase in xrange(targetphase + 1, len(allphases)): - # filter nodes that are not in a compatible phase already - nodes = [n for n in nodes - if self.phase(repo, repo[n].rev()) >= phase] - if not nodes: - break # no roots to move anymore - olds = self.phaseroots[phase] - roots = set(ctx.node() for ctx in repo.set( - 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes)) - if olds != roots: - self._updateroots(phase, roots) - # some roots may need to be declared for lower phases - delroots.extend(olds - roots) - # declare deleted root in the target phase - if targetphase != 0: - self.retractboundary(repo, targetphase, delroots) - - def retractboundary(self, repo, targetphase, nodes): - # Be careful to preserve shallow-copied values: do not update - # phaseroots values, replace them. - - currentroots = self.phaseroots[targetphase] - newroots = [n for n in nodes - if self.phase(repo, repo[n].rev()) < targetphase] - if newroots: - if nullid in newroots: - raise util.Abort(_('cannot change null revision phase')) - currentroots = currentroots.copy() - currentroots.update(newroots) - ctxs = repo.set('roots(%ln::)', currentroots) - currentroots.intersection_update(ctx.node() for ctx in ctxs) - self._updateroots(targetphase, currentroots) - -def advanceboundary(repo, targetphase, nodes): - """Add nodes to a phase changing other nodes phases if necessary. - - This function move boundary *forward* this means that all nodes - are set in the target phase or kept in a *lower* phase. - - Simplify boundary to contains phase roots only.""" - phcache = repo._phasecache.copy() - phcache.advanceboundary(repo, targetphase, nodes) - repo._phasecache.replace(phcache) - -def retractboundary(repo, targetphase, nodes): - """Set nodes back to a phase changing other nodes phases if - necessary. - - This function move boundary *backward* this means that all nodes - are set in the target phase or kept in a *higher* phase. - - Simplify boundary to contains phase roots only.""" - phcache = repo._phasecache.copy() - phcache.retractboundary(repo, targetphase, nodes) - repo._phasecache.replace(phcache) - -def listphases(repo): - """List phases root for serialization over pushkey""" - keys = {} - value = '%i' % draft - for root in repo._phasecache.phaseroots[draft]: - keys[hex(root)] = value - - if repo.ui.configbool('phases', 'publish', True): - # Add an extra data to let remote know we are a publishing - # repo. Publishing repo can't just pretend they are old repo. - # When pushing to a publishing repo, the client still need to - # push phase boundary - # - # Push do not only push changeset. It also push phase data. - # New phase data may apply to common changeset which won't be - # push (as they are common). Here is a very simple example: - # - # 1) repo A push changeset X as draft to repo B - # 2) repo B make changeset X public - # 3) repo B push to repo A. X is not pushed but the data that - # X as now public should - # - # The server can't handle it on it's own as it has no idea of - # client phase data. - keys['publishing'] = 'True' - return keys - -def pushphase(repo, nhex, oldphasestr, newphasestr): - """List phases root for serialisation over pushkey""" - lock = repo.lock() - try: - currentphase = repo[nhex].phase() - newphase = abs(int(newphasestr)) # let's avoid negative index surprise - oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise - if currentphase == oldphase and newphase < oldphase: - advanceboundary(repo, newphase, [bin(nhex)]) - return 1 - elif currentphase == newphase: - # raced, but got correct result - return 1 - else: - return 0 - finally: - lock.release() - -def analyzeremotephases(repo, subset, roots): - """Compute phases heads and root in a subset of node from root dict - - * subset is heads of the subset - * roots is {<nodeid> => phase} mapping. key and value are string. - - Accept unknown element input - """ - # build list from dictionary - draftroots = [] - nodemap = repo.changelog.nodemap # to filter unknown nodes - for nhex, phase in roots.iteritems(): - if nhex == 'publishing': # ignore data related to publish option - continue - node = bin(nhex) - phase = int(phase) - if phase == 0: - if node != nullid: - repo.ui.warn(_('ignoring inconsistent public root' - ' from remote: %s\n') % nhex) - elif phase == 1: - if node in nodemap: - draftroots.append(node) - else: - repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n') - % (phase, nhex)) - # compute heads - publicheads = newheads(repo, subset, draftroots) - return publicheads, draftroots - -def newheads(repo, heads, roots): - """compute new head of a subset minus another - - * `heads`: define the first subset - * `rroots`: define the second we substract to the first""" - revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))', - heads, roots, roots, heads) - return [c.node() for c in revset] - - -def newcommitphase(ui): - """helper to get the target phase of new commit - - Handle all possible values for the phases.new-commit options. - - """ - v = ui.config('phases', 'new-commit', draft) - try: - return phasenames.index(v) - except ValueError: - try: - return int(v) - except ValueError: - msg = _("phases.new-commit: not a valid phase name ('%s')") - raise error.ConfigError(msg % v) - |