--- a/MANIFEST.in Tue Feb 28 17:22:21 2017 +0100
+++ b/MANIFEST.in Tue Feb 28 17:27:44 2017 +0100
@@ -1,8 +1,6 @@
exclude contrib/nopushpublish.py
-exclude hgext/directaccess.py
-exclude hgext/drophack.py
-exclude hgext/inhibit.py
-exclude hgext/obsolete.py
+exclude hgext3rd/evolve/hack/*.py
+exclude hgext3rd/evolve/legacy.py
exclude Makefile
exclude tests/test-drop.t
exclude tests/test-inhibit.t
@@ -15,10 +13,9 @@
include docs/*.rst
include docs/static/*.svg
include docs/tutorials/*.t
-include hgext/evolve.py
-include hgext/__init__.py
-include hgext/pushexperiment.py
-include hgext/simple4server.py
+include hgext3rd/__init__.py
+include hgext3rd/evolve/__init__.py
+include hgext3rd/evolve/serveronly.py
include MANIFEST.in
include README
include setup.py
--- a/README Tue Feb 28 17:22:21 2017 +0100
+++ b/README Tue Feb 28 17:27:44 2017 +0100
@@ -56,6 +56,11 @@
Changelog
=========
+6.0.0 --
+
+ - removed old (unpackaged) pushexperiment extension.
+ - move all extensions in the official 'hgext3rd' namespace package
+
5.6.1 -- 2017-02-28
- fix a crash that sometime happened when evolving merges.
--- a/hgext/__init__.py Tue Feb 28 17:22:21 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-from __future__ import absolute_import
-import pkgutil
-__path__ = pkgutil.extend_path(__path__, __name__)
-
--- a/hgext/directaccess.py Tue Feb 28 17:22:21 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,194 +0,0 @@
-""" This extension provides direct access
-It is the ability to refer and access hidden sha in commands provided that you
-know their value.
-For example hg log -r xxx where xxx is a commit has should work whether xxx is
-hidden or not as we assume that the user knows what he is doing when referring
-to xxx.
-"""
-from mercurial import extensions
-from mercurial import cmdutil
-from mercurial import repoview
-from mercurial import branchmap
-from mercurial import revset
-from mercurial import error
-from mercurial import commands
-from mercurial import hg
-from mercurial import util
-from mercurial.i18n import _
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
-# By default, all the commands have directaccess with warnings
-# List of commands that have no directaccess and directaccess with no warning
-directaccesslevel = [
- # Format:
- # ('nowarning', 'evolve', 'prune'),
- # means: no directaccess warning, for the command in evolve named prune
- #
- # ('error', None, 'serve'),
- # means: no directaccess for the command in core named serve
- #
- # The list is ordered alphabetically by command names, starting with all
- # the commands in core then all the commands in the extensions
- #
- # The general guideline is:
- # - remove directaccess warnings for read only commands
- # - no direct access for commands with consequences outside of the repo
- # - leave directaccess warnings for all the other commands
- #
- ('nowarning', None, 'annotate'),
- ('nowarning', None, 'archive'),
- ('nowarning', None, 'bisect'),
- ('nowarning', None, 'bookmarks'),
- ('nowarning', None, 'bundle'),
- ('nowarning', None, 'cat'),
- ('nowarning', None, 'diff'),
- ('nowarning', None, 'export'),
- ('nowarning', None, 'identify'),
- ('nowarning', None, 'incoming'),
- ('nowarning', None, 'log'),
- ('nowarning', None, 'manifest'),
- ('error', None, 'outgoing'), # confusing if push errors and not outgoing
- ('error', None, 'push'), # destructive
- ('nowarning', None, 'revert'),
- ('error', None, 'serve'),
- ('nowarning', None, 'tags'),
- ('nowarning', None, 'unbundle'),
- ('nowarning', None, 'update'),
-]
-
-def reposetup(ui, repo):
- repo._explicitaccess = set()
-
-def _computehidden(repo):
- hidden = repoview.filterrevs(repo, 'visible')
- cl = repo.changelog
- dynamic = hidden & repo._explicitaccess
- if dynamic:
- blocked = cl.ancestors(dynamic, inclusive=True)
- hidden = frozenset(r for r in hidden if r not in blocked)
- return hidden
-
-def setupdirectaccess():
- """ Add two new filtername that behave like visible to provide direct access
- and direct access with warning. Wraps the commands to setup direct access
- """
- repoview.filtertable.update({'visible-directaccess-nowarn': _computehidden})
- repoview.filtertable.update({'visible-directaccess-warn': _computehidden})
- branchmap.subsettable['visible-directaccess-nowarn'] = 'visible'
- branchmap.subsettable['visible-directaccess-warn'] = 'visible'
-
- for warn, ext, cmd in directaccesslevel:
- try:
- cmdtable = extensions.find(ext).cmdtable if ext else commands.table
- wrapper = wrapwitherror if warn == 'error' else wrapwithoutwarning
- extensions.wrapcommand(cmdtable, cmd, wrapper)
- except (error.UnknownCommand, KeyError):
- pass
-
-def wrapwitherror(orig, ui, repo, *args, **kwargs):
- if repo and repo.filtername == 'visible-directaccess-warn':
- repo = repo.filtered('visible')
- return orig(ui, repo, *args, **kwargs)
-
-def wrapwithoutwarning(orig, ui, repo, *args, **kwargs):
- if repo and repo.filtername == 'visible-directaccess-warn':
- repo = repo.filtered("visible-directaccess-nowarn")
- return orig(ui, repo, *args, **kwargs)
-
-def uisetup(ui):
- """ Change ordering of extensions to ensure that directaccess extsetup comes
- after the one of the extensions in the loadsafter list """
- loadsafter = ui.configlist('directaccess','loadsafter')
- order = list(extensions._order)
- directaccesidx = order.index('directaccess')
-
- # The min idx for directaccess to load after all the extensions in loadafter
- minidxdirectaccess = directaccesidx
-
- for ext in loadsafter:
- try:
- minidxdirectaccess = max(minidxdirectaccess, order.index(ext))
- except ValueError:
- pass # extension not loaded
-
- if minidxdirectaccess > directaccesidx:
- order.insert(minidxdirectaccess + 1, 'directaccess')
- order.remove('directaccess')
- extensions._order = order
-
-def _repository(orig, *args, **kwargs):
- """Make visible-directaccess-warn the default filter for new repos"""
- repo = orig(*args, **kwargs)
- return repo.filtered("visible-directaccess-warn")
-
-def extsetup(ui):
- extensions.wrapfunction(revset, 'posttreebuilthook', _posttreebuilthook)
- extensions.wrapfunction(hg, 'repository', _repository)
- setupdirectaccess()
-
-hashre = util.re.compile('[0-9a-fA-F]{1,40}')
-
-_listtuple = ('symbol', '_list')
-
-def _ishashsymbol(symbol, maxrev):
- # Returns true if symbol looks like a hash
- try:
- n = int(symbol)
- if n <= maxrev:
- # It's a rev number
- return False
- except ValueError:
- pass
- return hashre.match(symbol)
-
-def gethashsymbols(tree, maxrev):
- # Returns the list of symbols of the tree that look like hashes
- # for example for the revset 3::abe3ff it will return ('abe3ff')
- if not tree:
- return []
-
- results = []
- if len(tree) == 2 and tree[0] == "symbol":
- results.append(tree[1])
- elif tree[0] == "func" and tree[1] == _listtuple:
- # the optimiser will group sequence of hash request
- results += tree[2][1].split('\0')
- elif len(tree) >= 3:
- for subtree in tree[1:]:
- results += gethashsymbols(subtree, maxrev)
- # return directly, we don't need to filter symbols again
- return results
- return [s for s in results if _ishashsymbol(s, maxrev)]
-
-def _posttreebuilthook(orig, tree, repo):
- # This is use to enabled direct hash access
- # We extract the symbols that look like hashes and add them to the
- # explicitaccess set
- orig(tree, repo)
- filternm = ""
- if repo is not None:
- filternm = repo.filtername
- if filternm is not None and filternm.startswith('visible-directaccess'):
- prelength = len(repo._explicitaccess)
- accessbefore = set(repo._explicitaccess)
- cl = repo.unfiltered().changelog
- repo.symbols = gethashsymbols(tree, len(cl))
- for node in repo.symbols:
- try:
- node = cl._partialmatch(node)
- except error.LookupError:
- node = None
- if node is not None:
- rev = cl.rev(node)
- if rev not in repo.changelog:
- repo._explicitaccess.add(rev)
- if prelength != len(repo._explicitaccess):
- if repo.filtername != 'visible-directaccess-nowarn':
- unhiddencommits = repo._explicitaccess - accessbefore
- repo.ui.warn(_("Warning: accessing hidden changesets %s "
- "for write operation\n") %
- (",".join([str(repo.unfiltered()[l])
- for l in unhiddencommits])))
- repo.invalidatevolatilesets()
--- a/hgext/drophack.py Tue Feb 28 17:22:21 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-'''This extension add a hacky command to drop changeset during review
-
-This extension is intended as a temporary hack to allow Matt Mackall to use
-evolve in the Mercurial review it self. You should probably not use it if your
-name is not Matt Mackall.
-'''
-
-import os
-import time
-import contextlib
-
-from mercurial.i18n import _
-from mercurial import cmdutil
-from mercurial import repair
-from mercurial import scmutil
-from mercurial import lock as lockmod
-from mercurial import util
-from mercurial import commands
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
-
-@contextlib.contextmanager
-def timed(ui, caption):
- ostart = os.times()
- cstart = time.time()
- yield
- cstop = time.time()
- ostop = os.times()
- wall = cstop - cstart
- user = ostop[0] - ostart[0]
- sys = ostop[1] - ostart[1]
- comb = user + sys
- ui.write("%s: wall %f comb %f user %f sys %f\n"
- % (caption, wall, comb, user, sys))
-
-def obsmarkerchainfrom(obsstore, nodes):
- """return all marker chain starting from node
-
- Starting from mean "use as successors"."""
- # XXX need something smarter for descendant of bumped changeset
- seennodes = set(nodes)
- seenmarkers = set()
- pendingnodes = set(nodes)
- precursorsmarkers = obsstore.precursors
- while pendingnodes:
- current = pendingnodes.pop()
- new = set()
- for precmark in precursorsmarkers.get(current, ()):
- if precmark in seenmarkers:
- continue
- seenmarkers.add(precmark)
- new.add(precmark[0])
- yield precmark
- new -= seennodes
- pendingnodes |= new
-
-def stripmarker(ui, repo, markers):
- """remove <markers> from the repo obsstore
-
- The old obsstore content is saved in a `obsstore.prestrip` file
- """
- repo = repo.unfiltered()
- repo.destroying()
- oldmarkers = list(repo.obsstore._all)
- util.rename(repo.sjoin('obsstore'),
- repo.join('obsstore.prestrip'))
- del repo.obsstore # drop the cache
- newstore = repo.obsstore
- assert not newstore # should be empty after rename
- newmarkers = [m for m in oldmarkers if m not in markers]
- tr = repo.transaction('drophack')
- try:
- newstore.add(tr, newmarkers)
- tr.close()
- finally:
- tr.release()
- repo.destroyed()
-
-
-@command('drop', [('r', 'rev', [], 'revision to update')], _('[-r] revs'))
-def cmddrop(ui, repo, *revs, **opts):
- """I'm hacky do not use me!
-
- This command strip a changeset, its precursors and all obsolescence marker
- associated to its chain.
-
- There is no way to limit the extend of the purge yet. You may have to
- repull from other source to get some changeset and obsolescence marker
- back.
-
- This intended for Matt Mackall usage only. do not use me.
- """
- revs = list(revs)
- revs.extend(opts['rev'])
- if not revs:
- revs = ['.']
- # get the changeset
- revs = scmutil.revrange(repo, revs)
- if not revs:
- ui.write_err('no revision to drop\n')
- return 1
- # lock from the beginning to prevent race
- wlock = lock = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- # check they have no children
- if repo.revs('%ld and public()', revs):
- ui.write_err('cannot drop public revision')
- return 1
- if repo.revs('children(%ld) - %ld', revs, revs):
- ui.write_err('cannot drop revision with children')
- return 1
- if repo.revs('. and %ld', revs):
- newrevs = repo.revs('max(::. - %ld)', revs)
- if newrevs:
- assert len(newrevs) == 1
- newrev = newrevs.first()
- else:
- newrev = -1
- commands.update(ui, repo, newrev)
- ui.status(_('working directory now at %s\n') % repo[newrev])
- # get all markers and successors up to root
- nodes = [repo[r].node() for r in revs]
- with timed(ui, 'search obsmarker'):
- markers = set(obsmarkerchainfrom(repo.obsstore, nodes))
- ui.write('%i obsmarkers found\n' % len(markers))
- cl = repo.unfiltered().changelog
- with timed(ui, 'search nodes'):
- allnodes = set(nodes)
- allnodes.update(m[0] for m in markers if cl.hasnode(m[0]))
- ui.write('%i nodes found\n' % len(allnodes))
- cl = repo.changelog
- visiblenodes = set(n for n in allnodes if cl.hasnode(n))
- # check constraint again
- if repo.revs('%ln and public()', visiblenodes):
- ui.write_err('cannot drop public revision')
- return 1
- if repo.revs('children(%ln) - %ln', visiblenodes, visiblenodes):
- ui.write_err('cannot drop revision with children')
- return 1
-
- if markers:
- # strip them
- with timed(ui, 'strip obsmarker'):
- stripmarker(ui, repo, markers)
- # strip the changeset
- with timed(ui, 'strip nodes'):
- repair.strip(ui, repo, list(allnodes), backup="all",
- topic='drophack')
-
- finally:
- lockmod.release(lock, wlock)
-
- # rewrite the whole file.
- # print data.
- # - time to compute the chain
- # - time to strip the changeset
- # - time to strip the obs marker.
--- a/hgext/evolve.py Tue Feb 28 17:22:21 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,4187 +0,0 @@
-# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
-# Logilab SA <contact@logilab.fr>
-# Pierre-Yves David <pierre-yves.david@ens-lyon.org>
-# Patrick Mezard <patrick@mezard.eu>
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-
-'''extends Mercurial feature related to Changeset Evolution
-
-This extension provides several commands to mutate history and deal with
-resulting issues.
-
-It also:
-
- - enables the "Changeset Obsolescence" feature of Mercurial,
- - alters core commands and extensions that rewrite history to use
- this feature,
- - improves some aspect of the early implementation in Mercurial core
-'''
-
-__version__ = '5.6.0'
-testedwith = '3.4.3 3.5.2 3.6.2 3.7.3 3.8.1 3.9 4.0 4.1'
-buglink = 'https://bz.mercurial-scm.org/'
-
-
-evolutionhelptext = """
-Obsolescence markers make it possible to mark changesets that have been
-deleted or superset in a new version of the changeset.
-
-Unlike the previous way of handling such changes, by stripping the old
-changesets from the repository, obsolescence markers can be propagated
-between repositories. This allows for a safe and simple way of exchanging
-mutable history and altering it after the fact. Changeset phases are
-respected, such that only draft and secret changesets can be altered (see
-:hg:`help phases` for details).
-
-Obsolescence is tracked using "obsolete markers", a piece of metadata
-tracking which changesets have been made obsolete, potential successors for
-a given changeset, the moment the changeset was marked as obsolete, and the
-user who performed the rewriting operation. The markers are stored
-separately from standard changeset data can be exchanged without any of the
-precursor changesets, preventing unnecessary exchange of obsolescence data.
-
-The complete set of obsolescence markers describes a history of changeset
-modifications that is orthogonal to the repository history of file
-modifications. This changeset history allows for detection and automatic
-resolution of edge cases arising from multiple users rewriting the same part
-of history concurrently.
-
-Current feature status
-======================
-
-This feature is still in development. If you see this help, you have enabled an
-extension that turned this feature on.
-
-Obsolescence markers will be exchanged between repositories that explicitly
-assert support for the obsolescence feature (this can currently only be done
-via an extension).""".strip()
-
-
-import sys, os
-import random
-try:
- import StringIO as io
- StringIO = io.StringIO
-except ImportError:
- import io
- StringIO = io.StringIO
-import re
-import collections
-import socket
-import errno
-import hashlib
-import struct
-sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
-
-import mercurial
-from mercurial import util
-from mercurial import repair
-
-try:
- from mercurial import obsolete
- if not obsolete._enabled:
- obsolete._enabled = True
- from mercurial import wireproto
- gboptslist = getattr(wireproto, 'gboptslist', None)
- gboptsmap = getattr(wireproto, 'gboptsmap', None)
-except (ImportError, AttributeError):
- gboptslist = gboptsmap = None
-
-# Flags for enabling optional parts of evolve
-commandopt = 'allnewcommands'
-
-from mercurial import bookmarks as bookmarksmod
-from mercurial import cmdutil
-from mercurial import commands
-from mercurial import context
-from mercurial import copies
-from mercurial import error
-from mercurial import exchange
-from mercurial import extensions
-from mercurial import help
-from mercurial import httppeer
-from mercurial import hg
-from mercurial import lock as lockmod
-from mercurial import merge
-from mercurial import node
-from mercurial import phases
-from mercurial import patch
-from mercurial import revset
-from mercurial import scmutil
-from mercurial import templatekw
-from mercurial.i18n import _
-from mercurial.commands import walkopts, commitopts, commitopts2, mergetoolopts
-from mercurial.node import nullid
-from mercurial import wireproto
-from mercurial import localrepo
-from mercurial.hgweb import hgweb_mod
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
-_pack = struct.pack
-_unpack = struct.unpack
-
-if gboptsmap is not None:
- memfilectx = context.memfilectx
-elif gboptslist is not None:
- oldmemfilectx = context.memfilectx
- def memfilectx(repo, *args, **kwargs):
- return oldmemfilectx(*args, **kwargs)
-else:
- raise ImportError('evolve needs version %s or above' %
- min(testedwith.split()))
-
-aliases, entry = cmdutil.findcmd('commit', commands.table)
-hasinteractivemode = any(['interactive' in e for e in entry[1]])
-if hasinteractivemode:
- interactiveopt = [['i', 'interactive', None, _('use interactive mode')]]
-else:
- interactiveopt = []
-# This extension contains the following code
-#
-# - Extension Helper code
-# - Obsolescence cache
-# - ...
-# - Older format compat
-
-
-#####################################################################
-### Extension helper ###
-#####################################################################
-
-class exthelper(object):
- """Helper for modular extension setup
-
- A single helper should be instantiated for each extension. Helper
- methods are then used as decorators for various purpose.
-
- All decorators return the original function and may be chained.
- """
-
- def __init__(self):
- self._uicallables = []
- self._extcallables = []
- self._repocallables = []
- self._revsetsymbols = []
- self._templatekws = []
- self._commandwrappers = []
- self._extcommandwrappers = []
- self._functionwrappers = []
- self._duckpunchers = []
-
- def final_uisetup(self, ui):
- """Method to be used as the extension uisetup
-
- The following operations belong here:
-
- - Changes to ui.__class__ . The ui object that will be used to run the
- command has not yet been created. Changes made here will affect ui
- objects created after this, and in particular the ui that will be
- passed to runcommand
- - Command wraps (extensions.wrapcommand)
- - Changes that need to be visible to other extensions: because
- initialization occurs in phases (all extensions run uisetup, then all
- run extsetup), a change made here will be visible to other extensions
- during extsetup
- - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch
- module members
- - Setup of pre-* and post-* hooks
- - pushkey setup
- """
- for cont, funcname, func in self._duckpunchers:
- setattr(cont, funcname, func)
- for command, wrapper, opts in self._commandwrappers:
- entry = extensions.wrapcommand(commands.table, command, wrapper)
- if opts:
- for short, long, val, msg in opts:
- entry[1].append((short, long, val, msg))
- for cont, funcname, wrapper in self._functionwrappers:
- extensions.wrapfunction(cont, funcname, wrapper)
- for c in self._uicallables:
- c(ui)
-
- def final_extsetup(self, ui):
- """Method to be used as a the extension extsetup
-
- The following operations belong here:
-
- - Changes depending on the status of other extensions. (if
- extensions.find('mq'))
- - Add a global option to all commands
- - Register revset functions
- """
- knownexts = {}
- for name, symbol in self._revsetsymbols:
- revset.symbols[name] = symbol
- for name, kw in self._templatekws:
- templatekw.keywords[name] = kw
- for ext, command, wrapper, opts in self._extcommandwrappers:
- if ext not in knownexts:
- try:
- e = extensions.find(ext)
- except KeyError:
- # Extension isn't enabled, so don't bother trying to wrap
- # it.
- continue
- knownexts[ext] = e.cmdtable
- entry = extensions.wrapcommand(knownexts[ext], command, wrapper)
- if opts:
- for short, long, val, msg in opts:
- entry[1].append((short, long, val, msg))
-
- for c in self._extcallables:
- c(ui)
-
- def final_reposetup(self, ui, repo):
- """Method to be used as the extension reposetup
-
- The following operations belong here:
-
- - All hooks but pre-* and post-*
- - Modify configuration variables
- - Changes to repo.__class__, repo.dirstate.__class__
- """
- for c in self._repocallables:
- c(ui, repo)
-
- def uisetup(self, call):
- """Decorated function will be executed during uisetup
-
- example::
-
- @eh.uisetup
- def setupbabar(ui):
- print 'this is uisetup!'
- """
- self._uicallables.append(call)
- return call
-
- def extsetup(self, call):
- """Decorated function will be executed during extsetup
-
- example::
-
- @eh.extsetup
- def setupcelestine(ui):
- print 'this is extsetup!'
- """
- self._extcallables.append(call)
- return call
-
- def reposetup(self, call):
- """Decorated function will be executed during reposetup
-
- example::
-
- @eh.reposetup
- def setupzephir(ui, repo):
- print 'this is reposetup!'
- """
- self._repocallables.append(call)
- return call
-
- def revset(self, symbolname):
- """Decorated function is a revset symbol
-
- The name of the symbol must be given as the decorator argument.
- The symbol is added during `extsetup`.
-
- example::
-
- @eh.revset('hidden')
- def revsetbabar(repo, subset, x):
- args = revset.getargs(x, 0, 0, 'babar accept no argument')
- return [r for r in subset if 'babar' in repo[r].description()]
- """
- def dec(symbol):
- self._revsetsymbols.append((symbolname, symbol))
- return symbol
- return dec
-
-
- def templatekw(self, keywordname):
- """Decorated function is a template keyword
-
- The name of the keyword must be given as the decorator argument.
- The symbol is added during `extsetup`.
-
- example::
-
- @eh.templatekw('babar')
- def kwbabar(ctx):
- return 'babar'
- """
- def dec(keyword):
- self._templatekws.append((keywordname, keyword))
- return keyword
- return dec
-
- def wrapcommand(self, command, extension=None, opts=[]):
- """Decorated function is a command wrapper
-
- The name of the command must be given as the decorator argument.
- The wrapping is installed during `uisetup`.
-
- If the second option `extension` argument is provided, the wrapping
- will be applied in the extension commandtable. This argument must be a
- string that will be searched using `extension.find` if not found and
- Abort error is raised. If the wrapping applies to an extension, it is
- installed during `extsetup`.
-
- example::
-
- @eh.wrapcommand('summary')
- def wrapsummary(orig, ui, repo, *args, **kwargs):
- ui.note('Barry!')
- return orig(ui, repo, *args, **kwargs)
-
- The `opts` argument allows specifying additional arguments for the
- command.
-
- """
- def dec(wrapper):
- if extension is None:
- self._commandwrappers.append((command, wrapper, opts))
- else:
- self._extcommandwrappers.append((extension, command, wrapper,
- opts))
- return wrapper
- return dec
-
- def wrapfunction(self, container, funcname):
- """Decorated function is a function wrapper
-
- This function takes two arguments, the container and the name of the
- function to wrap. The wrapping is performed during `uisetup`.
- (there is no extension support)
-
- example::
-
- @eh.function(discovery, 'checkheads')
- def wrapfunction(orig, *args, **kwargs):
- ui.note('His head smashed in and his heart cut out')
- return orig(*args, **kwargs)
- """
- def dec(wrapper):
- self._functionwrappers.append((container, funcname, wrapper))
- return wrapper
- return dec
-
- def addattr(self, container, funcname):
- """Decorated function is to be added to the container
-
- This function takes two arguments, the container and the name of the
- function to wrap. The wrapping is performed during `uisetup`.
-
- example::
-
- @eh.function(context.changectx, 'babar')
- def babar(ctx):
- return 'babar' in ctx.description
- """
- def dec(func):
- self._duckpunchers.append((container, funcname, func))
- return func
- return dec
-
-eh = exthelper()
-uisetup = eh.final_uisetup
-extsetup = eh.final_extsetup
-reposetup = eh.final_reposetup
-
-#####################################################################
-### Option configuration ###
-#####################################################################
-
-@eh.reposetup # must be the first of its kin.
-def _configureoptions(ui, repo):
- # If no capabilities are specified, enable everything.
- # This is so existing evolve users don't need to change their config.
- evolveopts = ui.configlist('experimental', 'evolution')
- if not evolveopts:
- evolveopts = ['all']
- ui.setconfig('experimental', 'evolution', evolveopts, 'evolve')
-
-@eh.uisetup
-def _configurecmdoptions(ui):
- # Unregister evolve commands if the command capability is not specified.
- #
- # This must be in the same function as the option configuration above to
- # guarantee it happens after the above configuration, but before the
- # extsetup functions.
- evolvecommands = ui.configlist('experimental', 'evolutioncommands')
- evolveopts = ui.configlist('experimental', 'evolution')
- if evolveopts and (commandopt not in evolveopts and
- 'all' not in evolveopts):
- # We build whitelist containing the commands we want to enable
- whitelist = set()
- for cmd in evolvecommands:
- matchingevolvecommands = [e for e in cmdtable.keys() if cmd in e]
- if not matchingevolvecommands:
- raise error.Abort(_('unknown command: %s') % cmd)
- elif len(matchingevolvecommands) > 1:
- msg = _('ambiguous command specification: "%s" matches %r')
- raise error.Abort(msg % (cmd, matchingevolvecommands))
- else:
- whitelist.add(matchingevolvecommands[0])
- for disabledcmd in set(cmdtable) - whitelist:
- del cmdtable[disabledcmd]
-
-#####################################################################
-### experimental behavior ###
-#####################################################################
-
-commitopts3 = [
- ('D', 'current-date', None,
- _('record the current date as commit date')),
- ('U', 'current-user', None,
- _('record the current user as committer')),
-]
-
-def _resolveoptions(ui, opts):
- """modify commit options dict to handle related options
-
- For now, all it does is figure out the commit date: respect -D unless
- -d was supplied.
- """
- # N.B. this is extremely similar to setupheaderopts() in mq.py
- if not opts.get('date') and opts.get('current_date'):
- opts['date'] = '%d %d' % util.makedate()
- if not opts.get('user') and opts.get('current_user'):
- opts['user'] = ui.username()
-
-getrevs = obsolete.getrevs
-
-#####################################################################
-### Additional Utilities ###
-#####################################################################
-
-# This section contains a lot of small utility function and method
-
-# - Function to create markers
-# - useful alias pstatus and pdiff (should probably go in evolve)
-# - "troubles" method on changectx
-# - function to travel through the obsolescence graph
-# - function to find useful changeset to stabilize
-
-
-### Useful alias
-
-@eh.uisetup
-def _installalias(ui):
- if ui.config('alias', 'pstatus', None) is None:
- ui.setconfig('alias', 'pstatus', 'status --rev .^', 'evolve')
- if ui.config('alias', 'pdiff', None) is None:
- ui.setconfig('alias', 'pdiff', 'diff --rev .^', 'evolve')
- if ui.config('alias', 'olog', None) is None:
- ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden",
- 'evolve')
- if ui.config('alias', 'odiff', None) is None:
- ui.setconfig('alias', 'odiff',
- "diff --hidden --rev 'limit(precursors(.),1)' --rev .",
- 'evolve')
- if ui.config('alias', 'grab', None) is None:
- if os.name == 'nt':
- ui.setconfig('alias', 'grab',
- "! " + util.hgexecutable() + " rebase --dest . --rev $@ && "
- + util.hgexecutable() + " up tip",
- 'evolve')
- else:
- ui.setconfig('alias', 'grab',
- "! $HG rebase --dest . --rev $@ && $HG up tip",
- 'evolve')
-
-
-### Troubled revset symbol
-
-@eh.revset('troubled')
-def revsettroubled(repo, subset, x):
- """``troubled()``
- Changesets with troubles.
- """
- revset.getargs(x, 0, 0, 'troubled takes no arguments')
- troubled = set()
- troubled.update(getrevs(repo, 'unstable'))
- troubled.update(getrevs(repo, 'bumped'))
- troubled.update(getrevs(repo, 'divergent'))
- troubled = revset.baseset(troubled)
- troubled.sort() # set is non-ordered, enforce order
- return subset & troubled
-
-### Obsolescence graph
-
-# XXX SOME MAJOR CLEAN UP TO DO HERE XXX
-
-def _precursors(repo, s):
- """Precursor of a changeset"""
- cs = set()
- nm = repo.changelog.nodemap
- markerbysubj = repo.obsstore.precursors
- node = repo.changelog.node
- for r in s:
- for p in markerbysubj.get(node(r), ()):
- pr = nm.get(p[0])
- if pr is not None:
- cs.add(pr)
- cs -= repo.changelog.filteredrevs # nodemap has no filtering
- return cs
-
-def _allprecursors(repo, s): # XXX we need a better naming
- """transitive precursors of a subset"""
- node = repo.changelog.node
- toproceed = [node(r) for r in s]
- seen = set()
- allsubjects = repo.obsstore.precursors
- while toproceed:
- nc = toproceed.pop()
- for mark in allsubjects.get(nc, ()):
- np = mark[0]
- if np not in seen:
- seen.add(np)
- toproceed.append(np)
- nm = repo.changelog.nodemap
- cs = set()
- for p in seen:
- pr = nm.get(p)
- if pr is not None:
- cs.add(pr)
- cs -= repo.changelog.filteredrevs # nodemap has no filtering
- return cs
-
-def _successors(repo, s):
- """Successors of a changeset"""
- cs = set()
- node = repo.changelog.node
- nm = repo.changelog.nodemap
- markerbyobj = repo.obsstore.successors
- for r in s:
- for p in markerbyobj.get(node(r), ()):
- for sub in p[1]:
- sr = nm.get(sub)
- if sr is not None:
- cs.add(sr)
- cs -= repo.changelog.filteredrevs # nodemap has no filtering
- return cs
-
-def _allsuccessors(repo, s, haltonflags=0): # XXX we need a better naming
- """transitive successors of a subset
-
- haltonflags allows to provide flags which prevent the evaluation of a
- marker. """
- node = repo.changelog.node
- toproceed = [node(r) for r in s]
- seen = set()
- allobjects = repo.obsstore.successors
- while toproceed:
- nc = toproceed.pop()
- for mark in allobjects.get(nc, ()):
- if mark[2] & haltonflags:
- continue
- for sub in mark[1]:
- if sub == nullid:
- continue # should not be here!
- if sub not in seen:
- seen.add(sub)
- toproceed.append(sub)
- nm = repo.changelog.nodemap
- cs = set()
- for s in seen:
- sr = nm.get(s)
- if sr is not None:
- cs.add(sr)
- cs -= repo.changelog.filteredrevs # nodemap has no filtering
- return cs
-
-
-
-
-#####################################################################
-### Extending revset and template ###
-#####################################################################
-
-# this section add several useful revset symbol not yet in core.
-# they are subject to changes
-
-
-### XXX I'm not sure this revset is useful
-@eh.revset('suspended')
-def revsetsuspended(repo, subset, x):
- """``suspended()``
- Obsolete changesets with non-obsolete descendants.
- """
- revset.getargs(x, 0, 0, 'suspended takes no arguments')
- suspended = revset.baseset(getrevs(repo, 'suspended'))
- suspended.sort()
- return subset & suspended
-
-
-@eh.revset('precursors')
-def revsetprecursors(repo, subset, x):
- """``precursors(set)``
- Immediate precursors of changesets in set.
- """
- s = revset.getset(repo, revset.fullreposet(repo), x)
- s = revset.baseset(_precursors(repo, s))
- s.sort()
- return subset & s
-
-
-@eh.revset('allprecursors')
-def revsetallprecursors(repo, subset, x):
- """``allprecursors(set)``
- Transitive precursors of changesets in set.
- """
- s = revset.getset(repo, revset.fullreposet(repo), x)
- s = revset.baseset(_allprecursors(repo, s))
- s.sort()
- return subset & s
-
-
-@eh.revset('successors')
-def revsetsuccessors(repo, subset, x):
- """``successors(set)``
- Immediate successors of changesets in set.
- """
- s = revset.getset(repo, revset.fullreposet(repo), x)
- s = revset.baseset(_successors(repo, s))
- s.sort()
- return subset & s
-
-@eh.revset('allsuccessors')
-def revsetallsuccessors(repo, subset, x):
- """``allsuccessors(set)``
- Transitive successors of changesets in set.
- """
- s = revset.getset(repo, revset.fullreposet(repo), x)
- s = revset.baseset(_allsuccessors(repo, s))
- s.sort()
- return subset & s
-
-### template keywords
-# XXX it does not handle troubles well :-/
-
-@eh.templatekw('obsolete')
-def obsoletekw(repo, ctx, templ, **args):
- """:obsolete: String. Whether the changeset is ``obsolete``.
- """
- if ctx.obsolete():
- return 'obsolete'
- return ''
-
-@eh.templatekw('troubles')
-def showtroubles(repo, ctx, **args):
- """:troubles: List of strings. Evolution troubles affecting the changeset
- (zero or more of "unstable", "divergent" or "bumped")."""
- return templatekw.showlist('trouble', ctx.troubles(), plural='troubles',
- **args)
-
-#####################################################################
-### Various trouble warning ###
-#####################################################################
-
-# This section take care of issue warning to the user when troubles appear
-
-
-def _warnobsoletewc(ui, repo):
- if repo['.'].obsolete():
- ui.warn(_('working directory parent is obsolete!\n'))
- if (not ui.quiet) and obsolete.isenabled(repo, commandopt):
- ui.warn(_("(use 'hg evolve' to update to its successor)\n"))
-
-@eh.wrapcommand("update")
-@eh.wrapcommand("pull")
-def wrapmayobsoletewc(origfn, ui, repo, *args, **opts):
- """Warn that the working directory parent is an obsolete changeset"""
- def warnobsolete():
- _warnobsoletewc(ui, repo)
- wlock = None
- try:
- wlock = repo.wlock()
- repo._afterlock(warnobsolete)
- res = origfn(ui, repo, *args, **opts)
- finally:
- lockmod.release(wlock)
- return res
-
-@eh.wrapcommand("parents")
-def wrapparents(origfn, ui, repo, *args, **opts):
- res = origfn(ui, repo, *args, **opts)
- _warnobsoletewc(ui, repo)
- return res
-
-# XXX this could wrap transaction code
-# XXX (but this is a bit a layer violation)
-@eh.wrapcommand("commit")
-@eh.wrapcommand("import")
-@eh.wrapcommand("push")
-@eh.wrapcommand("pull")
-@eh.wrapcommand("graft")
-@eh.wrapcommand("phase")
-@eh.wrapcommand("unbundle")
-def warnobserrors(orig, ui, repo, *args, **kwargs):
- """display warning is the command resulted in more instable changeset"""
- # part of the troubled stuff may be filtered (stash ?)
- # This needs a better implementation but will probably wait for core.
- filtered = repo.changelog.filteredrevs
- priorunstables = len(set(getrevs(repo, 'unstable')) - filtered)
- priorbumpeds = len(set(getrevs(repo, 'bumped')) - filtered)
- priordivergents = len(set(getrevs(repo, 'divergent')) - filtered)
- ret = orig(ui, repo, *args, **kwargs)
- # workaround phase stupidity
- #phases._filterunknown(ui, repo.changelog, repo._phasecache.phaseroots)
- filtered = repo.changelog.filteredrevs
- newunstables = \
- len(set(getrevs(repo, 'unstable')) - filtered) - priorunstables
- newbumpeds = \
- len(set(getrevs(repo, 'bumped')) - filtered) - priorbumpeds
- newdivergents = \
- len(set(getrevs(repo, 'divergent')) - filtered) - priordivergents
- if newunstables > 0:
- ui.warn(_('%i new unstable changesets\n') % newunstables)
- if newbumpeds > 0:
- ui.warn(_('%i new bumped changesets\n') % newbumpeds)
- if newdivergents > 0:
- ui.warn(_('%i new divergent changesets\n') % newdivergents)
- return ret
-
-@eh.wrapfunction(mercurial.exchange, 'push')
-def push(orig, repo, *args, **opts):
- """Add a hint for "hg evolve" when troubles make push fails
- """
- try:
- return orig(repo, *args, **opts)
- except error.Abort as ex:
- hint = _("use 'hg evolve' to get a stable history "
- "or --force to ignore warnings")
- if (len(ex.args) >= 1
- and ex.args[0].startswith('push includes ')
- and ex.hint is None):
- ex.hint = hint
- raise
-
-def summaryhook(ui, repo):
- def write(fmt, count):
- s = fmt % count
- if count:
- ui.write(s)
- else:
- ui.note(s)
-
- # util.versiontuple was introduced in 3.6.2
- if not util.safehasattr(util, 'versiontuple'):
- nbunstable = len(getrevs(repo, 'unstable'))
- nbbumped = len(getrevs(repo, 'bumped'))
- nbdivergent = len(getrevs(repo, 'divergent'))
- write('unstable: %i changesets\n', nbunstable)
- write('bumped: %i changesets\n', nbbumped)
- write('divergent: %i changesets\n', nbdivergent)
- else:
- # In 3.6.2, summary in core gained this feature, no need to display it
- pass
- state = _evolvestateread(repo)
- if state is not None:
- # i18n: column positioning for "hg summary"
- ui.write(_('evolve: (evolve --continue)\n'))
-
-@eh.extsetup
-def obssummarysetup(ui):
- cmdutil.summaryhooks.add('evolve', summaryhook)
-
-
-#####################################################################
-### Core Other extension compat ###
-#####################################################################
-
-
-@eh.extsetup
-def _rebasewrapping(ui):
- # warning about more obsolete
- try:
- rebase = extensions.find('rebase')
- if rebase:
- extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors)
- except KeyError:
- pass # rebase not found
- try:
- histedit = extensions.find('histedit')
- if histedit:
- extensions.wrapcommand(histedit.cmdtable, 'histedit', warnobserrors)
- except KeyError:
- pass # histedit not found
-
-#####################################################################
-### Old Evolve extension content ###
-#####################################################################
-
-# XXX need clean up and proper sorting in other section
-
-### util function
-#############################
-
-### changeset rewriting logic
-#############################
-
-def rewrite(repo, old, updates, head, newbases, commitopts):
- """Return (nodeid, created) where nodeid is the identifier of the
- changeset generated by the rewrite process, and created is True if
- nodeid was actually created. If created is False, nodeid
- references a changeset existing before the rewrite call.
- """
- wlock = lock = tr = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- tr = repo.transaction('rewrite')
- if len(old.parents()) > 1: #XXX remove this unnecessary limitation.
- raise error.Abort(_('cannot amend merge changesets'))
- base = old.p1()
- updatebookmarks = _bookmarksupdater(repo, old.node(), tr)
-
- # commit a new version of the old changeset, including the update
- # collect all files which might be affected
- files = set(old.files())
- for u in updates:
- files.update(u.files())
-
- # Recompute copies (avoid recording a -> b -> a)
- copied = copies.pathcopies(base, head)
-
-
- # prune files which were reverted by the updates
- def samefile(f):
- if f in head.manifest():
- a = head.filectx(f)
- if f in base.manifest():
- b = base.filectx(f)
- return (a.data() == b.data()
- and a.flags() == b.flags())
- else:
- return False
- else:
- return f not in base.manifest()
- files = [f for f in files if not samefile(f)]
- # commit version of these files as defined by head
- headmf = head.manifest()
- def filectxfn(repo, ctx, path):
- if path in headmf:
- fctx = head[path]
- flags = fctx.flags()
- mctx = memfilectx(repo, fctx.path(), fctx.data(),
- islink='l' in flags,
- isexec='x' in flags,
- copied=copied.get(path))
- return mctx
- return None
-
- message = cmdutil.logmessage(repo.ui, commitopts)
- if not message:
- message = old.description()
-
- user = commitopts.get('user') or old.user()
- date = commitopts.get('date') or None # old.date()
- extra = dict(commitopts.get('extra', old.extra()))
- extra['branch'] = head.branch()
-
- new = context.memctx(repo,
- parents=newbases,
- text=message,
- files=files,
- filectxfn=filectxfn,
- user=user,
- date=date,
- extra=extra)
-
- if commitopts.get('edit'):
- new._text = cmdutil.commitforceeditor(repo, new, [])
- revcount = len(repo)
- newid = repo.commitctx(new)
- new = repo[newid]
- created = len(repo) != revcount
- updatebookmarks(newid)
-
- tr.close()
- return newid, created
- finally:
- lockmod.release(tr, lock, wlock)
-
-class MergeFailure(error.Abort):
- pass
-
-def relocate(repo, orig, dest, pctx=None, keepbranch=False):
- """rewrite <rev> on dest"""
- if orig.rev() == dest.rev():
- raise error.Abort(_('tried to relocate a node on top of itself'),
- hint=_("This shouldn't happen. If you still "
- "need to move changesets, please do so "
- "manually with nothing to rebase - working "
- "directory parent is also destination"))
-
- if pctx is None:
- if len(orig.parents()) == 2:
- raise error.Abort(_("tried to relocate a merge commit without "
- "specifying which parent should be moved"),
- hint=_("Specify the parent by passing in pctx"))
- pctx = orig.p1()
-
- destbookmarks = repo.nodebookmarks(dest.node())
- nodesrc = orig.node()
- destphase = repo[nodesrc].phase()
- commitmsg = orig.description()
-
- cache = {}
- sha1s = re.findall(sha1re, commitmsg)
- unfi = repo.unfiltered()
- for sha1 in sha1s:
- ctx = None
- try:
- ctx = unfi[sha1]
- except error.RepoLookupError:
- continue
-
- if not ctx.obsolete():
- continue
-
- successors = obsolete.successorssets(repo, ctx.node(), cache)
-
- # We can't make any assumptions about how to update the hash if the
- # cset in question was split or diverged.
- if len(successors) == 1 and len(successors[0]) == 1:
- newsha1 = node.hex(successors[0][0])
- commitmsg = commitmsg.replace(sha1, newsha1[:len(sha1)])
- else:
- repo.ui.note(_('The stale commit message reference to %s could '
- 'not be updated\n') % sha1)
-
- tr = repo.currenttransaction()
- assert tr is not None
- try:
- r = _evolvemerge(repo, orig, dest, pctx, keepbranch)
- if r[-1]: #some conflict
- raise error.Abort(
- 'unresolved merge conflicts (see hg help resolve)')
- nodenew = _relocatecommit(repo, orig, commitmsg)
- except error.Abort as exc:
- repo.dirstate.beginparentchange()
- repo.setparents(repo['.'].node(), nullid)
- writedirstate(repo.dirstate, tr)
- # fix up dirstate for copies and renames
- copies.duplicatecopies(repo, dest.rev(), orig.p1().rev())
- repo.dirstate.endparentchange()
- class LocalMergeFailure(MergeFailure, exc.__class__):
- pass
- exc.__class__ = LocalMergeFailure
- tr.close() # to keep changes in this transaction (e.g. dirstate)
- raise
- oldbookmarks = repo.nodebookmarks(nodesrc)
- _finalizerelocate(repo, orig, dest, nodenew, tr)
- return nodenew
-
-def _bookmarksupdater(repo, oldid, tr):
- """Return a callable update(newid) updating the current bookmark
- and bookmarks bound to oldid to newid.
- """
- def updatebookmarks(newid):
- dirty = False
- oldbookmarks = repo.nodebookmarks(oldid)
- if oldbookmarks:
- for b in oldbookmarks:
- repo._bookmarks[b] = newid
- dirty = True
- if dirty:
- repo._bookmarks.recordchange(tr)
- return updatebookmarks
-
-### bookmarks api compatibility layer ###
-def bmdeactivate(repo):
- try:
- return bookmarksmod.deactivate(repo)
- except AttributeError:
- return bookmarksmod.unsetcurrent(repo)
-def bmactivate(repo, book):
- try:
- return bookmarksmod.activate(repo, book)
- except AttributeError:
- return bookmarksmod.setcurrent(repo, book)
-
-def bmactive(repo):
- try:
- return repo._activebookmark
- except AttributeError:
- return repo._bookmarkcurrent
-
-### dirstate compatibility layer < hg 3.6
-
-def writedirstate(dirstate, tr):
- if dirstate.write.func_code.co_argcount != 1: # mercurial 3.6 and above
- return dirstate.write(tr)
- return dirstate.write()
-
-
-
-### new command
-#############################
-metadataopts = [
- ('d', 'date', '',
- _('record the specified date in metadata'), _('DATE')),
- ('u', 'user', '',
- _('record the specified user in metadata'), _('USER')),
-]
-
-@eh.uisetup
-def _installimportobsolete(ui):
- entry = cmdutil.findcmd('import', commands.table)[1]
- entry[1].append(('', 'obsolete', False,
- _('mark the old node as obsoleted by '
- 'the created commit')))
-
-@eh.wrapfunction(mercurial.cmdutil, 'tryimportone')
-def tryimportone(orig, ui, repo, hunk, parents, opts, *args, **kwargs):
- extracted = patch.extract(ui, hunk)
- if util.safehasattr(extracted, 'get'):
- # mercurial 3.6 return a dictionary there
- expected = extracted.get('nodeid')
- else:
- expected = extracted[5]
- if expected is not None:
- expected = node.bin(expected)
- oldextract = patch.extract
- try:
- patch.extract = lambda ui, hunk: extracted
- ret = orig(ui, repo, hunk, parents, opts, *args, **kwargs)
- finally:
- patch.extract = oldextract
- created = ret[1]
- if (opts['obsolete'] and None not in (created, expected)
- and created != expected):
- tr = repo.transaction('import-obs')
- try:
- metadata = {'user': ui.username()}
- repo.obsstore.create(tr, expected, (created,),
- metadata=metadata)
- tr.close()
- finally:
- tr.release()
- return ret
-
-
-def _deprecatealias(oldalias, newalias):
- '''Deprecates an alias for a command in favour of another
-
- Creates a new entry in the command table for the old alias. It creates a
- wrapper that has its synopsis set to show that is has been deprecated.
- The documentation will be replace with a pointer to the new alias.
- If a user invokes the command a deprecation warning will be printed and
- the command of the *new* alias will be invoked.
-
- This function is loosely based on the extensions.wrapcommand function.
- '''
- try:
- aliases, entry = cmdutil.findcmd(newalias, cmdtable)
- except error.UnknownCommand:
- # Commands may be disabled
- return
- for alias, e in cmdtable.items():
- if e is entry:
- break
-
- synopsis = '(DEPRECATED)'
- if len(entry) > 2:
- fn, opts, _syn = entry
- else:
- fn, opts, = entry
- deprecationwarning = _('%s have been deprecated in favor of %s\n') % (
- oldalias, newalias)
- def newfn(*args, **kwargs):
- ui = args[0]
- ui.warn(deprecationwarning)
- util.checksignature(fn)(*args, **kwargs)
- newfn.__doc__ = deprecationwarning
- cmdwrapper = command(oldalias, opts, synopsis)
- cmdwrapper(newfn)
-
-@eh.extsetup
-def deprecatealiases(ui):
- _deprecatealias('gup', 'next')
- _deprecatealias('gdown', 'previous')
-
-@command('debugrecordpruneparents', [], '')
-def cmddebugrecordpruneparents(ui, repo):
- """add parent data to prune markers when possible
-
- This command searches the repo for prune markers without parent information.
- If the pruned node is locally known, it creates a new marker with parent
- data.
- """
- pgop = 'reading markers'
-
- # lock from the beginning to prevent race
- wlock = lock = tr = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- tr = repo.transaction('recordpruneparents')
- unfi = repo.unfiltered()
- nm = unfi.changelog.nodemap
- store = repo.obsstore
- pgtotal = len(store._all)
- for idx, mark in enumerate(list(store._all)):
- if not mark[1]:
- rev = nm.get(mark[0])
- if rev is not None:
- ctx = unfi[rev]
- parents = tuple(p.node() for p in ctx.parents())
- before = len(store._all)
- store.create(tr, mark[0], mark[1], mark[2], mark[3],
- parents=parents)
- if len(store._all) - before:
- ui.write(_('created new markers for %i\n') % rev)
- ui.progress(pgop, idx, total=pgtotal)
- tr.close()
- ui.progress(pgop, None)
- finally:
- lockmod.release(tr, lock, wlock)
-
-@command('debugobsstorestat', [], '')
-def cmddebugobsstorestat(ui, repo):
- """print statistics about obsolescence markers in the repo"""
- def _updateclustermap(nodes, mark, clustersmap):
- c = (set(nodes), set([mark]))
- toproceed = set(nodes)
- while toproceed:
- n = toproceed.pop()
- other = clustersmap.get(n)
- if (other is not None
- and other is not c):
- other[0].update(c[0])
- other[1].update(c[1])
- for on in c[0]:
- if on in toproceed:
- continue
- clustersmap[on] = other
- c = other
- clustersmap[n] = c
-
- store = repo.obsstore
- unfi = repo.unfiltered()
- nm = unfi.changelog.nodemap
- ui.write(_('markers total: %9i\n') % len(store._all))
- sucscount = [0, 0 , 0, 0]
- known = 0
- parentsdata = 0
- metakeys = {}
- # node -> cluster mapping
- # a cluster is a (set(nodes), set(markers)) tuple
- clustersmap = {}
- # same data using parent information
- pclustersmap = {}
- for mark in store:
- if mark[0] in nm:
- known += 1
- nbsucs = len(mark[1])
- sucscount[min(nbsucs, 3)] += 1
- meta = mark[3]
- for key, value in meta:
- metakeys.setdefault(key, 0)
- metakeys[key] += 1
- meta = dict(meta)
- parents = [meta.get('p1'), meta.get('p2')]
- parents = [node.bin(p) for p in parents if p is not None]
- if parents:
- parentsdata += 1
- # cluster handling
- nodes = set(mark[1])
- nodes.add(mark[0])
- _updateclustermap(nodes, mark, clustersmap)
- # same with parent data
- nodes.update(parents)
- _updateclustermap(nodes, mark, pclustersmap)
-
- # freezing the result
- for c in clustersmap.values():
- fc = (frozenset(c[0]), frozenset(c[1]))
- for n in fc[0]:
- clustersmap[n] = fc
- # same with parent data
- for c in pclustersmap.values():
- fc = (frozenset(c[0]), frozenset(c[1]))
- for n in fc[0]:
- pclustersmap[n] = fc
- ui.write((' for known precursors: %9i\n' % known))
- ui.write((' with parents data: %9i\n' % parentsdata))
- # successors data
- ui.write(('markers with no successors: %9i\n' % sucscount[0]))
- ui.write((' 1 successors: %9i\n' % sucscount[1]))
- ui.write((' 2 successors: %9i\n' % sucscount[2]))
- ui.write((' more than 2 successors: %9i\n' % sucscount[3]))
- # meta data info
- ui.write((' available keys:\n'))
- for key in sorted(metakeys):
- ui.write((' %15s: %9i\n' % (key, metakeys[key])))
-
- allclusters = list(set(clustersmap.values()))
- allclusters.sort(key=lambda x: len(x[1]))
- ui.write(('disconnected clusters: %9i\n' % len(allclusters)))
-
- ui.write(' any known node: %9i\n'
- % len([c for c in allclusters
- if [n for n in c[0] if nm.get(n) is not None]]))
- if allclusters:
- nbcluster = len(allclusters)
- ui.write((' smallest length: %9i\n' % len(allclusters[0][1])))
- ui.write((' longer length: %9i\n'
- % len(allclusters[-1][1])))
- median = len(allclusters[nbcluster//2][1])
- ui.write((' median length: %9i\n' % median))
- mean = sum(len(x[1]) for x in allclusters) // nbcluster
- ui.write((' mean length: %9i\n' % mean))
- allpclusters = list(set(pclustersmap.values()))
- allpclusters.sort(key=lambda x: len(x[1]))
- ui.write((' using parents data: %9i\n' % len(allpclusters)))
- ui.write(' any known node: %9i\n'
- % len([c for c in allclusters
- if [n for n in c[0] if nm.get(n) is not None]]))
- if allpclusters:
- nbcluster = len(allpclusters)
- ui.write((' smallest length: %9i\n'
- % len(allpclusters[0][1])))
- ui.write((' longer length: %9i\n'
- % len(allpclusters[-1][1])))
- median = len(allpclusters[nbcluster//2][1])
- ui.write((' median length: %9i\n' % median))
- mean = sum(len(x[1]) for x in allpclusters) // nbcluster
- ui.write((' mean length: %9i\n' % mean))
-
-def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category):
- """Resolve the troubles affecting one revision"""
- wlock = lock = tr = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- tr = repo.transaction("evolve")
- if 'unstable' == category:
- result = _solveunstable(ui, repo, ctx, dryrun, confirm, progresscb)
- elif 'bumped' == category:
- result = _solvebumped(ui, repo, ctx, dryrun, confirm, progresscb)
- elif 'divergent' == category:
- result = _solvedivergent(ui, repo, ctx, dryrun, confirm,
- progresscb)
- else:
- assert False, "unknown trouble category: %s" % (category)
- tr.close()
- return result
- finally:
- lockmod.release(tr, lock, wlock)
-
-def _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat):
- """Used by the evolve function to display an error message when
- no troubles can be resolved"""
- troublecategories = ['bumped', 'divergent', 'unstable']
- unselectedcategories = [c for c in troublecategories if c != targetcat]
- msg = None
- hint = None
-
- troubled = {
- "unstable": repo.revs("unstable()"),
- "divergent": repo.revs("divergent()"),
- "bumped": repo.revs("bumped()"),
- "all": repo.revs("troubled()"),
- }
-
-
- hintmap = {
- 'bumped': _("do you want to use --bumped"),
- 'bumped+divergent': _("do you want to use --bumped or --divergent"),
- 'bumped+unstable': _("do you want to use --bumped or --unstable"),
- 'divergent': _("do you want to use --divergent"),
- 'divergent+unstable': _("do you want to use --divergent"
- " or --unstable"),
- 'unstable': _("do you want to use --unstable"),
- 'any+bumped': _("do you want to use --any (or --rev) and --bumped"),
- 'any+bumped+divergent': _("do you want to use --any (or --rev) and"
- " --bumped or --divergent"),
- 'any+bumped+unstable': _("do you want to use --any (or --rev) and"
- "--bumped or --unstable"),
- 'any+divergent': _("do you want to use --any (or --rev) and"
- " --divergent"),
- 'any+divergent+unstable': _("do you want to use --any (or --rev)"
- " and --divergent or --unstable"),
- 'any+unstable': _("do you want to use --any (or --rev)"
- "and --unstable"),
- }
-
- if revopt:
- revs = scmutil.revrange(repo, revopt)
- if not revs:
- msg = _("set of specified revisions is empty")
- else:
- msg = _("no %s changesets in specified revisions") % targetcat
- othertroubles = []
- for cat in unselectedcategories:
- if revs & troubled[cat]:
- othertroubles.append(cat)
- if othertroubles:
- hint = hintmap['+'.join(othertroubles)]
-
- elif anyopt:
- msg = _("no %s changesets to evolve") % targetcat
- othertroubles = []
- for cat in unselectedcategories:
- if troubled[cat]:
- othertroubles.append(cat)
- if othertroubles:
- hint = hintmap['+'.join(othertroubles)]
-
- else:
- # evolve without any option = relative to the current wdir
- if targetcat == 'unstable':
- msg = _("nothing to evolve on current working copy parent")
- else:
- msg = _("current working copy parent is not %s") % targetcat
-
- p1 = repo['.'].rev()
- othertroubles = []
- for cat in unselectedcategories:
- if p1 in troubled[cat]:
- othertroubles.append(cat)
- if othertroubles:
- hint = hintmap['+'.join(othertroubles)]
- else:
- l = len(troubled[targetcat])
- if l:
- hint = _("%d other %s in the repository, do you want --any "
- "or --rev") % (l, targetcat)
- else:
- othertroubles = []
- for cat in unselectedcategories:
- if troubled[cat]:
- othertroubles.append(cat)
- if othertroubles:
- hint = hintmap['any+'+('+'.join(othertroubles))]
- else:
- msg = _("no troubled changesets")
-
- assert msg is not None
- ui.write_err(msg+"\n")
- if hint:
- ui.write_err("("+hint+")\n")
- return 2
- else:
- return 1
-
-def _cleanup(ui, repo, startnode, showprogress):
- if showprogress:
- ui.progress(_('evolve'), None)
- if repo['.'] != startnode:
- ui.status(_('working directory is now at %s\n') % repo['.'])
-
-class MultipleSuccessorsError(RuntimeError):
- """Exception raised by _singlesuccessor when multiple successor sets exists
-
- The object contains the list of successorssets in its 'successorssets'
- attribute to call to easily recover.
- """
-
- def __init__(self, successorssets):
- self.successorssets = successorssets
-
-def _singlesuccessor(repo, p):
- """returns p (as rev) if not obsolete or its unique latest successors
-
- fail if there are no such successor"""
-
- if not p.obsolete():
- return p.rev()
- obs = repo[p]
- ui = repo.ui
- newer = obsolete.successorssets(repo, obs.node())
- # search of a parent which is not killed
- while not newer:
- ui.debug("stabilize target %s is plain dead,"
- " trying to stabilize on its parent\n" %
- obs)
- obs = obs.parents()[0]
- newer = obsolete.successorssets(repo, obs.node())
- if len(newer) > 1 or len(newer[0]) > 1:
- raise MultipleSuccessorsError(newer)
-
- return repo[newer[0][0]].rev()
-
-def builddependencies(repo, revs):
- """returns dependency graphs giving an order to solve instability of revs
- (see _orderrevs for more information on usage)"""
-
- # For each troubled revision we keep track of what instability if any should
- # be resolved in order to resolve it. Example:
- # dependencies = {3: [6], 6:[]}
- # Means that: 6 has no dependency, 3 depends on 6 to be solved
- dependencies = {}
- # rdependencies is the inverted dict of dependencies
- rdependencies = collections.defaultdict(set)
-
- for r in revs:
- dependencies[r] = set()
- for p in repo[r].parents():
- try:
- succ = _singlesuccessor(repo, p)
- except MultipleSuccessorsError as exc:
- dependencies[r] = exc.successorssets
- continue
- if succ in revs:
- dependencies[r].add(succ)
- rdependencies[succ].add(r)
- return dependencies, rdependencies
-
-def _dedupedivergents(repo, revs):
- """Dedupe the divergents revs in revs to get one from each group with the
- lowest revision numbers
- """
- repo = repo.unfiltered()
- res = set()
- # To not reevaluate divergents of the same group once one is encountered
- discarded = set()
- for rev in revs:
- if rev in discarded:
- continue
- divergent = repo[rev]
- base, others = divergentdata(divergent)
- othersrevs = [o.rev() for o in others]
- res.add(min([divergent.rev()] + othersrevs))
- discarded.update(othersrevs)
- return res
-
-def _selectrevs(repo, allopt, revopt, anyopt, targetcat):
- """select troubles in repo matching according to given options"""
- revs = set()
- if allopt or revopt:
- revs = repo.revs(targetcat+'()')
- if revopt:
- revs = scmutil.revrange(repo, revopt) & revs
- elif not anyopt:
- topic = getattr(repo, 'currenttopic', '')
- if topic:
- revs = repo.revs('topic(%s)', topic) & revs
- elif targetcat == 'unstable':
- revs = _aspiringdescendant(repo,
- repo.revs('(.::) - obsolete()::'))
- revs = set(revs)
- if targetcat == 'divergent':
- # Pick one divergent per group of divergents
- revs = _dedupedivergents(repo, revs)
- elif anyopt:
- revs = repo.revs('first(%s())' % (targetcat))
- elif targetcat == 'unstable':
- revs = set(_aspiringchildren(repo, repo.revs('(.::) - obsolete()::')))
- if 1 < len(revs):
- msg = "multiple evolve candidates"
- hint = (_("select one of %s with --rev")
- % ', '.join([str(repo[r]) for r in sorted(revs)]))
- raise error.Abort(msg, hint=hint)
- elif targetcat in repo['.'].troubles():
- revs = set([repo['.'].rev()])
- return revs
-
-
-def _orderrevs(repo, revs):
- """Compute an ordering to solve instability for the given revs
-
- revs is a list of unstable revisions.
-
- Returns the same revisions ordered to solve their instability from the
- bottom to the top of the stack that the stabilization process will produce
- eventually.
-
- This ensures the minimal number of stabilizations, as we can stabilize each
- revision on its final stabilized destination.
- """
- # Step 1: Build the dependency graph
- dependencies, rdependencies = builddependencies(repo, revs)
- # Step 2: Build the ordering
- # Remove the revisions with no dependency(A) and add them to the ordering.
- # Removing these revisions leads to new revisions with no dependency (the
- # one depending on A) that we can remove from the dependency graph and add
- # to the ordering. We progress in a similar fashion until the ordering is
- # built
- solvablerevs = collections.deque([r for r in sorted(dependencies.keys())
- if not dependencies[r]])
- ordering = []
- while solvablerevs:
- rev = solvablerevs.popleft()
- for dependent in rdependencies[rev]:
- dependencies[dependent].remove(rev)
- if not dependencies[dependent]:
- solvablerevs.append(dependent)
- del dependencies[rev]
- ordering.append(rev)
-
- ordering.extend(sorted(dependencies))
- return ordering
-
-def divergentsets(repo, ctx):
- """Compute sets of commits divergent with a given one"""
- cache = {}
- succsets = {}
- base = {}
- for n in obsolete.allprecursors(repo.obsstore, [ctx.node()]):
- if n == ctx.node():
- # a node can't be a base for divergence with itself
- continue
- nsuccsets = obsolete.successorssets(repo, n, cache)
- for nsuccset in nsuccsets:
- if ctx.node() in nsuccset:
- # we are only interested in *other* successor sets
- continue
- if tuple(nsuccset) in base:
- # we already know the latest base for this divergency
- continue
- base[tuple(nsuccset)] = n
- divergence = []
- for divset, b in base.iteritems():
- divergence.append({
- 'divergentnodes': divset,
- 'commonprecursor': b
- })
-
- return divergence
-
-def _preparelistctxs(items, condition):
- return [item.hex() for item in items if condition(item)]
-
-def _formatctx(fm, ctx):
- fm.data(node=ctx.hex())
- fm.data(desc=ctx.description())
- fm.data(date=ctx.date())
- fm.data(user=ctx.user())
-
-def listtroubles(ui, repo, troublecategories, **opts):
- """Print all the troubles for the repo (or given revset)"""
- troublecategories = troublecategories or ['divergent', 'unstable', 'bumped']
- showunstable = 'unstable' in troublecategories
- showbumped = 'bumped' in troublecategories
- showdivergent = 'divergent' in troublecategories
-
- revs = repo.revs('+'.join("%s()" % t for t in troublecategories))
- if opts.get('rev'):
- revs = revs & repo.revs(opts.get('rev'))
-
- fm = ui.formatter('evolvelist', opts)
- for rev in revs:
- ctx = repo[rev]
- unpars = _preparelistctxs(ctx.parents(), lambda p: p.unstable())
- obspars = _preparelistctxs(ctx.parents(), lambda p: p.obsolete())
- imprecs = _preparelistctxs(repo.set("allprecursors(%n)", ctx.node()),
- lambda p: not p.mutable())
- dsets = divergentsets(repo, ctx)
-
- fm.startitem()
- # plain formatter section
- hashlen, desclen = 12, 60
- desc = ctx.description()
- if desc:
- desc = desc.splitlines()[0]
- desc = (desc[:desclen] + '...') if len(desc) > desclen else desc
- fm.plain('%s: ' % ctx.hex()[:hashlen])
- fm.plain('%s\n' % desc)
- fm.data(node=ctx.hex(), rev=ctx.rev(), desc=desc, phase=ctx.phasestr())
-
- for unpar in unpars if showunstable else []:
- fm.plain(' unstable: %s (unstable parent)\n' % unpar[:hashlen])
- for obspar in obspars if showunstable else []:
- fm.plain(' unstable: %s (obsolete parent)\n' % obspar[:hashlen])
- for imprec in imprecs if showbumped else []:
- fm.plain(' bumped: %s (immutable precursor)\n' % imprec[:hashlen])
-
- if dsets and showdivergent:
- for dset in dsets:
- fm.plain(' divergent: ')
- first = True
- for n in dset['divergentnodes']:
- t = "%s (%s)" if first else " %s (%s)"
- first = False
- fm.plain(t % (node.hex(n)[:hashlen], repo[n].phasestr()))
- comprec = node.hex(dset['commonprecursor'])[:hashlen]
- fm.plain(" (precursor %s)\n" % comprec)
- fm.plain("\n")
-
- # templater-friendly section
- _formatctx(fm, ctx)
- troubles = []
- for unpar in unpars:
- troubles.append({'troubletype': 'unstable', 'sourcenode': unpar,
- 'sourcetype': 'unstableparent'})
- for obspar in obspars:
- troubles.append({'troubletype': 'unstable', 'sourcenode': obspar,
- 'sourcetype': 'obsoleteparent'})
- for imprec in imprecs:
- troubles.append({'troubletype': 'bumped', 'sourcenode': imprec,
- 'sourcetype': 'immutableprecursor'})
- for dset in dsets:
- divnodes = [{'node': node.hex(n),
- 'phase': repo[n].phasestr(),
- } for n in dset['divergentnodes']]
- troubles.append({'troubletype': 'divergent',
- 'commonprecursor': node.hex(dset['commonprecursor']),
- 'divergentnodes': divnodes})
- fm.data(troubles=troubles)
-
- fm.end()
-
-@command('^evolve|stabilize|solve',
- [('n', 'dry-run', False,
- _('do not perform actions, just print what would be done')),
- ('', 'confirm', False,
- _('ask for confirmation before performing the action')),
- ('A', 'any', False,
- _('also consider troubled changesets unrelated to current working '
- 'directory')),
- ('r', 'rev', [], _('solves troubles of these revisions')),
- ('', 'bumped', False, _('solves only bumped changesets')),
- ('', 'divergent', False, _('solves only divergent changesets')),
- ('', 'unstable', False, _('solves only unstable changesets (default)')),
- ('a', 'all', False, _('evolve all troubled changesets related to the '
- 'current working directory and its descendants')),
- ('c', 'continue', False, _('continue an interrupted evolution')),
- ('l', 'list', False, 'provide details on troubled changesets in the repo'),
- ] + mergetoolopts,
- _('[OPTIONS]...'))
-def evolve(ui, repo, **opts):
- """solve troubled changesets in your repository
-
- Modifying history can lead to various types of troubled changesets:
- unstable, bumped, or divergent. The evolve command resolves your troubles
- by executing one of the following actions:
-
- - update working copy to a successor
- - rebase an unstable changeset
- - extract the desired changes from a bumped changeset
- - fuse divergent changesets back together
-
- If you pass no arguments, evolve works in automatic mode: it will execute a
- single action to reduce instability related to your working copy. There are
- two cases for this action. First, if the parent of your working copy is
- obsolete, evolve updates to the parent's successor. Second, if the working
- copy parent is not obsolete but has obsolete predecessors, then evolve
- determines if there is an unstable changeset that can be rebased onto the
- working copy parent in order to reduce instability.
- If so, evolve rebases that changeset. If not, evolve refuses to guess your
- intention, and gives a hint about what you might want to do next.
-
- Any time evolve creates a changeset, it updates the working copy to the new
- changeset. (Currently, every successful evolve operation involves an update
- as well; this may change in future.)
-
- Automatic mode only handles common use cases. For example, it avoids taking
- action in the case of ambiguity, and it ignores unstable changesets that
- are not related to your working copy.
- It also refuses to solve bumped or divergent changesets unless you explicity
- request such behavior (see below).
-
- Eliminating all instability around your working copy may require multiple
- invocations of :hg:`evolve`. Alternately, use ``--all`` to recursively
- select and evolve all unstable changesets that can be rebased onto the
- working copy parent.
- This is more powerful than successive invocations, since ``--all`` handles
- ambiguous cases (e.g. unstable changesets with multiple children) by
- evolving all branches.
-
- When your repository cannot be handled by automatic mode, you might need to
- use ``--rev`` to specify a changeset to evolve. For example, if you have
- an unstable changeset that is not related to the working copy parent,
- you could use ``--rev`` to evolve it. Or, if some changeset has multiple
- unstable children, evolve in automatic mode refuses to guess which one to
- evolve; you have to use ``--rev`` in that case.
-
- Alternately, ``--any`` makes evolve search for the next evolvable changeset
- regardless of whether it is related to the working copy parent.
-
- You can supply multiple revisions to evolve multiple troubled changesets
- in a single invocation. In revset terms, ``--any`` is equivalent to ``--rev
- first(unstable())``. ``--rev`` and ``--all`` are mutually exclusive, as are
- ``--rev`` and ``--any``.
-
- ``hg evolve --any --all`` is useful for cleaning up instability across all
- branches, letting evolve figure out the appropriate order and destination.
-
- When you have troubled changesets that are not unstable, :hg:`evolve`
- refuses to consider them unless you specify the category of trouble you
- wish to resolve, with ``--bumped`` or ``--divergent``. These options are
- currently mutually exclusive with each other and with ``--unstable``
- (the default). You can combine ``--bumped`` or ``--divergent`` with
- ``--rev``, ``--all``, or ``--any``.
-
- You can also use the evolve command to list the troubles affecting your
- repository by using the --list flag. You can choose to display only some
- categories of troubles with the --unstable, --divergent or --bumped flags.
- """
-
- # Options
- listopt = opts['list']
- contopt = opts['continue']
- anyopt = opts['any']
- allopt = opts['all']
- startnode = repo['.']
- dryrunopt = opts['dry_run']
- confirmopt = opts['confirm']
- revopt = opts['rev']
- troublecategories = ['bumped', 'divergent', 'unstable']
- specifiedcategories = [t for t in troublecategories if opts[t]]
- if listopt:
- listtroubles(ui, repo, specifiedcategories, **opts)
- return
-
- targetcat = 'unstable'
- if 1 < len(specifiedcategories):
- msg = _('cannot specify more than one trouble category to solve (yet)')
- raise error.Abort(msg)
- elif len(specifiedcategories) == 1:
- targetcat = specifiedcategories[0]
- elif repo['.'].obsolete():
- displayer = cmdutil.show_changeset(ui, repo,
- {'template': shorttemplate})
- # no args and parent is obsolete, update to successors
- try:
- ctx = repo[_singlesuccessor(repo, repo['.'])]
- except MultipleSuccessorsError as exc:
- repo.ui.write_err('parent is obsolete with multiple successors:\n')
- for ln in exc.successorssets:
- for n in ln:
- displayer.show(repo[n])
- return 2
-
-
- ui.status(_('update:'))
- if not ui.quiet:
- displayer.show(ctx)
-
- if dryrunopt:
- return 0
- res = hg.update(repo, ctx.rev())
- if ctx != startnode:
- ui.status(_('working directory is now at %s\n') % ctx)
- return res
-
- ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'evolve')
- troubled = set(repo.revs('troubled()'))
-
- # Progress handling
- seen = 1
- count = allopt and len(troubled) or 1
- showprogress = allopt
-
- def progresscb():
- if revopt or allopt:
- ui.progress(_('evolve'), seen, unit=_('changesets'), total=count)
-
- # Continuation handling
- if contopt:
- if anyopt:
- raise error.Abort('cannot specify both "--any" and "--continue"')
- if allopt:
- raise error.Abort('cannot specify both "--all" and "--continue"')
- state = _evolvestateread(repo)
- if state is None:
- raise error.Abort('no evolve to continue')
- orig = repo[state['current']]
- # XXX This is a terrible terrible hack, please get rid of it.
- lock = repo.wlock()
- try:
- repo.opener.write('graftstate', orig.hex() + '\n')
- try:
- graftcmd = commands.table['graft'][0]
- ret = graftcmd(ui, repo, old_obsolete=True, **{'continue': True})
- _evolvestatedelete(repo)
- return ret
- finally:
- util.unlinkpath(repo.join('graftstate'), ignoremissing=True)
- finally:
- lock.release()
- cmdutil.bailifchanged(repo)
-
-
- if revopt and allopt:
- raise error.Abort('cannot specify both "--rev" and "--all"')
- if revopt and anyopt:
- raise error.Abort('cannot specify both "--rev" and "--any"')
-
- revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat)
-
- if not revs:
- return _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat)
-
- # For the progress bar to show
- count = len(revs)
- # Order the revisions
- if targetcat == 'unstable':
- revs = _orderrevs(repo, revs)
- for rev in revs:
- progresscb()
- _solveone(ui, repo, repo[rev], dryrunopt, confirmopt,
- progresscb, targetcat)
- seen += 1
- progresscb()
- _cleanup(ui, repo, startnode, showprogress)
-
-def _possibledestination(repo, rev):
- """return all changesets that may be a new parent for REV"""
- tonode = repo.changelog.node
- parents = repo.changelog.parentrevs
- torev = repo.changelog.rev
- dest = set()
- tovisit = list(parents(rev))
- while tovisit:
- r = tovisit.pop()
- succsets = obsolete.successorssets(repo, tonode(r))
- if not succsets:
- tovisit.extend(parents(r))
- else:
- # We should probably pick only one destination from split
- # (case where '1 < len(ss)'), This could be the currently tipmost
- # but logic is less clear when result of the split are now on
- # multiple branches.
- for ss in succsets:
- for n in ss:
- dest.add(torev(n))
- return dest
-
-def _aspiringchildren(repo, revs):
- """Return a list of changectx which can be stabilized on top of pctx or
- one of its descendants. Empty list if none can be found."""
- target = set(revs)
- result = []
- for r in repo.revs('unstable() - %ld', revs):
- dest = _possibledestination(repo, r)
- if target & dest:
- result.append(r)
- return result
-
-def _aspiringdescendant(repo, revs):
- """Return a list of changectx which can be stabilized on top of pctx or
- one of its descendants recursively. Empty list if none can be found."""
- target = set(revs)
- result = set(target)
- paths = collections.defaultdict(set)
- for r in repo.revs('unstable() - %ld', revs):
- for d in _possibledestination(repo, r):
- paths[d].add(r)
-
- result = set(target)
- tovisit = list(revs)
- while tovisit:
- base = tovisit.pop()
- for unstable in paths[base]:
- if unstable not in result:
- tovisit.append(unstable)
- result.add(unstable)
- return sorted(result - target)
-
-def _solveunstable(ui, repo, orig, dryrun=False, confirm=False,
- progresscb=None):
- """Stabilize an unstable changeset"""
- pctx = orig.p1()
- if len(orig.parents()) == 2:
- if not pctx.obsolete():
- pctx = orig.p2() # second parent is obsolete ?
- elif orig.p2().obsolete():
- hint = _("Redo the merge (%s) and use `hg prune <old> "
- "--succ <new>` to obsolete the old one") % orig.hex()[:12]
- ui.warn(_("warning: no support for evolving merge changesets "
- "with two obsolete parents yet\n") +
- _("(%s)\n") % hint)
- return False
-
- if not pctx.obsolete():
- ui.warn(_("cannot solve instability of %s, skipping\n") % orig)
- return False
- obs = pctx
- newer = obsolete.successorssets(repo, obs.node())
- # search of a parent which is not killed
- while not newer or newer == [()]:
- ui.debug("stabilize target %s is plain dead,"
- " trying to stabilize on its parent\n" %
- obs)
- obs = obs.parents()[0]
- newer = obsolete.successorssets(repo, obs.node())
- if len(newer) > 1:
- msg = _("skipping %s: divergent rewriting. can't choose "
- "destination\n") % obs
- ui.write_err(msg)
- return 2
- targets = newer[0]
- assert targets
- if len(targets) > 1:
- # split target, figure out which one to pick, are they all in line?
- targetrevs = [repo[r].rev() for r in targets]
- roots = repo.revs('roots(%ld)', targetrevs)
- heads = repo.revs('heads(%ld)', targetrevs)
- if len(roots) > 1 or len(heads) > 1:
- msg = "cannot solve split accross two branches\n"
- ui.write_err(msg)
- return 2
- target = repo[heads.first()]
- else:
- target = targets[0]
- displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
- target = repo[target]
- if not ui.quiet or confirm:
- repo.ui.write(_('move:'))
- displayer.show(orig)
- repo.ui.write(_('atop:'))
- displayer.show(target)
- if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
- raise error.Abort(_('evolve aborted by user'))
- if progresscb: progresscb()
- todo = 'hg rebase -r %s -d %s\n' % (orig, target)
- if dryrun:
- repo.ui.write(todo)
- else:
- repo.ui.note(todo)
- if progresscb: progresscb()
- keepbranch = orig.p1().branch() != orig.branch()
- try:
- relocate(repo, orig, target, pctx, keepbranch)
- except MergeFailure:
- _evolvestatewrite(repo, {'current': orig.node()})
- repo.ui.write_err(_('evolve failed!\n'))
- repo.ui.write_err(
- _("fix conflict and run 'hg evolve --continue'"
- " or use 'hg update -C .' to abort\n"))
- raise
-
-def _solvebumped(ui, repo, bumped, dryrun=False, confirm=False,
- progresscb=None):
- """Stabilize a bumped changeset"""
- repo = repo.unfiltered()
- bumped = repo[bumped.rev()]
- # For now we deny bumped merge
- if len(bumped.parents()) > 1:
- msg = _('skipping %s : we do not handle merge yet\n') % bumped
- ui.write_err(msg)
- return 2
- prec = repo.set('last(allprecursors(%d) and public())', bumped).next()
- # For now we deny target merge
- if len(prec.parents()) > 1:
- msg = _('skipping: %s: public version is a merge, '
- 'this is not handled yet\n') % prec
- ui.write_err(msg)
- return 2
-
- displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
- if not ui.quiet or confirm:
- repo.ui.write(_('recreate:'))
- displayer.show(bumped)
- repo.ui.write(_('atop:'))
- displayer.show(prec)
- if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
- raise error.Abort(_('evolve aborted by user'))
- if dryrun:
- todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1())
- repo.ui.write(todo)
- repo.ui.write(('hg update %s;\n' % prec))
- repo.ui.write(('hg revert --all --rev %s;\n' % bumped))
- repo.ui.write(('hg commit --msg "bumped update to %s"'))
- return 0
- if progresscb: progresscb()
- newid = tmpctx = None
- tmpctx = bumped
- # Basic check for common parent. Far too complicated and fragile
- tr = repo.currenttransaction()
- assert tr is not None
- bmupdate = _bookmarksupdater(repo, bumped.node(), tr)
- if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)):
- # Need to rebase the changeset at the right place
- repo.ui.status(
- _('rebasing to destination parent: %s\n') % prec.p1())
- try:
- tmpid = relocate(repo, bumped, prec.p1())
- if tmpid is not None:
- tmpctx = repo[tmpid]
- obsolete.createmarkers(repo, [(bumped, (tmpctx,))])
- except MergeFailure:
- repo.opener.write('graftstate', bumped.hex() + '\n')
- repo.ui.write_err(_('evolution failed!\n'))
- repo.ui.write_err(
- _("fix conflict and run 'hg evolve --continue'\n"))
- raise
- # Create the new commit context
- repo.ui.status(_('computing new diff\n'))
- files = set()
- copied = copies.pathcopies(prec, bumped)
- precmanifest = prec.manifest().copy()
- # 3.3.2 needs a list.
- # future 3.4 don't detect the size change during iteration
- # this is fishy
- for key, val in list(bumped.manifest().iteritems()):
- precvalue = precmanifest.get(key, None)
- if precvalue is not None:
- del precmanifest[key]
- if precvalue != val:
- files.add(key)
- files.update(precmanifest) # add missing files
- # commit it
- if files: # something to commit!
- def filectxfn(repo, ctx, path):
- if path in bumped:
- fctx = bumped[path]
- flags = fctx.flags()
- mctx = memfilectx(repo, fctx.path(), fctx.data(),
- islink='l' in flags,
- isexec='x' in flags,
- copied=copied.get(path))
- return mctx
- return None
- text = 'bumped update to %s:\n\n' % prec
- text += bumped.description()
-
- new = context.memctx(repo,
- parents=[prec.node(), node.nullid],
- text=text,
- files=files,
- filectxfn=filectxfn,
- user=bumped.user(),
- date=bumped.date(),
- extra=bumped.extra())
-
- newid = repo.commitctx(new)
- if newid is None:
- obsolete.createmarkers(repo, [(tmpctx, ())])
- newid = prec.node()
- else:
- phases.retractboundary(repo, tr, bumped.phase(), [newid])
- obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))],
- flag=obsolete.bumpedfix)
- bmupdate(newid)
- repo.ui.status(_('committed as %s\n') % node.short(newid))
- # reroute the working copy parent to the new changeset
- repo.dirstate.beginparentchange()
- repo.dirstate.setparents(newid, node.nullid)
- repo.dirstate.endparentchange()
-
-def _solvedivergent(ui, repo, divergent, dryrun=False, confirm=False,
- progresscb=None):
- repo = repo.unfiltered()
- divergent = repo[divergent.rev()]
- base, others = divergentdata(divergent)
- if len(others) > 1:
- othersstr = "[%s]" % (','.join([str(i) for i in others]))
- msg = _("skipping %d:divergent with a changeset that got splitted"
- " into multiple ones:\n"
- "|[%s]\n"
- "| This is not handled by automatic evolution yet\n"
- "| You have to fallback to manual handling with commands "
- "such as:\n"
- "| - hg touch -D\n"
- "| - hg prune\n"
- "| \n"
- "| You should contact your local evolution Guru for help.\n"
- ) % (divergent, othersstr)
- ui.write_err(msg)
- return 2
- other = others[0]
- if len(other.parents()) > 1:
- msg = _("skipping %s: divergent changeset can't be "
- "a merge (yet)\n") % divergent
- ui.write_err(msg)
- hint = _("You have to fallback to solving this by hand...\n"
- "| This probably means redoing the merge and using \n"
- "| `hg prune` to kill older version.\n")
- ui.write_err(hint)
- return 2
- if other.p1() not in divergent.parents():
- msg = _("skipping %s: have a different parent than %s "
- "(not handled yet)\n") % (divergent, other)
- hint = _("| %(d)s, %(o)s are not based on the same changeset.\n"
- "| With the current state of its implementation, \n"
- "| evolve does not work in that case.\n"
- "| rebase one of them next to the other and run \n"
- "| this command again.\n"
- "| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n"
- "| - or: hg rebase --dest 'p1(%(o)s)' -r %(d)s\n"
- ) % {'d': divergent, 'o': other}
- ui.write_err(msg)
- ui.write_err(hint)
- return 2
-
- displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
- if not ui.quiet or confirm:
- ui.write(_('merge:'))
- displayer.show(divergent)
- ui.write(_('with: '))
- displayer.show(other)
- ui.write(_('base: '))
- displayer.show(base)
- if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y':
- raise error.Abort(_('evolve aborted by user'))
- if dryrun:
- ui.write(('hg update -c %s &&\n' % divergent))
- ui.write(('hg merge %s &&\n' % other))
- ui.write(('hg commit -m "auto merge resolving conflict between '
- '%s and %s"&&\n' % (divergent, other)))
- ui.write(('hg up -C %s &&\n' % base))
- ui.write(('hg revert --all --rev tip &&\n'))
- ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n'
- % divergent))
- return
- if divergent not in repo[None].parents():
- repo.ui.status(_('updating to "local" conflict\n'))
- hg.update(repo, divergent.rev())
- repo.ui.note(_('merging divergent changeset\n'))
- if progresscb: progresscb()
- try:
- stats = merge.update(repo,
- other.node(),
- branchmerge=True,
- force=False,
- ancestor=base.node(),
- mergeancestor=True)
- except TypeError:
- # Mercurial < 43c00ca887d1 (3.7)
- stats = merge.update(repo,
- other.node(),
- branchmerge=True,
- force=False,
- partial=None,
- ancestor=base.node(),
- mergeancestor=True)
-
- hg._showstats(repo, stats)
- if stats[3]:
- repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
- "or 'hg update -C .' to abort\n"))
- if stats[3] > 0:
- raise error.Abort('merge conflict between several amendments '
- '(this is not automated yet)',
- hint="""/!\ You can try:
-/!\ * manual merge + resolve => new cset X
-/!\ * hg up to the parent of the amended changeset (which are named W and Z)
-/!\ * hg revert --all -r X
-/!\ * hg ci -m "same message as the amended changeset" => new cset Y
-/!\ * hg prune -n Y W Z
-""")
- if progresscb: progresscb()
- emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit')
- tr = repo.currenttransaction()
- assert tr is not None
- try:
- repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve')
- repo.dirstate.beginparentchange()
- repo.dirstate.setparents(divergent.node(), node.nullid)
- repo.dirstate.endparentchange()
- oldlen = len(repo)
- amend(ui, repo, message='', logfile='')
- if oldlen == len(repo):
- new = divergent
- # no changes
- else:
- new = repo['.']
- obsolete.createmarkers(repo, [(other, (new,))])
- phases.retractboundary(repo, tr, other.phase(), [new.node()])
- finally:
- repo.ui.restoreconfig(emtpycommitallowed)
-
-def divergentdata(ctx):
- """return base, other part of a conflict
-
- This only return the first one.
-
- XXX this woobly function won't survive XXX
- """
- repo = ctx._repo.unfiltered()
- for base in repo.set('reverse(allprecursors(%d))', ctx):
- newer = obsolete.successorssets(ctx._repo, base.node())
- # drop filter and solution including the original ctx
- newer = [n for n in newer if n and ctx.node() not in n]
- if newer:
- return base, tuple(ctx._repo[o] for o in newer[0])
- raise error.Abort("base of divergent changeset %s not found" % ctx,
- hint='this case is not yet handled')
-
-
-
-shorttemplate = '[{rev}] {desc|firstline}\n'
-
-@command('^previous',
- [('B', 'move-bookmark', False,
- _('move active bookmark after update')),
- ('', 'merge', False, _('bring uncommitted change along')),
- ('', 'no-topic', False, _('ignore topic and move topologically')),
- ('n', 'dry-run', False,
- _('do not perform actions, just print what would be done'))],
- '[OPTION]...')
-def cmdprevious(ui, repo, **opts):
- """update to parent revision
-
- Displays the summary line of the destination for clarity."""
- wlock = None
- dryrunopt = opts['dry_run']
- if not dryrunopt:
- wlock = repo.wlock()
- try:
- wkctx = repo[None]
- wparents = wkctx.parents()
- if len(wparents) != 1:
- raise error.Abort('merge in progress')
- if not opts['merge']:
- try:
- cmdutil.bailifchanged(repo)
- except error.Abort as exc:
- exc.hint = _('do you want --merge?')
- raise
-
- parents = wparents[0].parents()
- topic = getattr(repo, 'currenttopic', '')
- if topic and not opts.get("no_topic", False):
- parents = [ctx for ctx in parents if ctx.topic() == topic]
- displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
- if not parents:
- ui.warn(_('no parent in topic "%s"\n') % topic)
- ui.warn(_('(do you want --no-topic)\n'))
- elif len(parents) == 1:
- p = parents[0]
- bm = bmactive(repo)
- shouldmove = opts.get('move_bookmark') and bm is not None
- if dryrunopt:
- ui.write(('hg update %s;\n' % p.rev()))
- if shouldmove:
- ui.write(('hg bookmark %s -r %s;\n' % (bm, p.rev())))
- else:
- ret = hg.update(repo, p.rev())
- if not ret:
- tr = lock = None
- try:
- lock = repo.lock()
- tr = repo.transaction('previous')
- if shouldmove:
- repo._bookmarks[bm] = p.node()
- repo._bookmarks.recordchange(tr)
- else:
- bmdeactivate(repo)
- tr.close()
- finally:
- lockmod.release(tr, lock)
-
- displayer.show(p)
- return 0
- else:
- for p in parents:
- displayer.show(p)
- ui.warn(_('multiple parents, explicitly update to one\n'))
- return 1
- finally:
- lockmod.release(wlock)
-
-@command('^next',
- [('B', 'move-bookmark', False,
- _('move active bookmark after update')),
- ('', 'merge', False, _('bring uncommitted change along')),
- ('', 'evolve', False, _('evolve the next changeset if necessary')),
- ('', 'no-topic', False, _('ignore topic and move topologically')),
- ('n', 'dry-run', False,
- _('do not perform actions, just print what would be done'))],
- '[OPTION]...')
-def cmdnext(ui, repo, **opts):
- """update to next child revision
-
- Use the ``--evolve`` flag to evolve unstable children on demand.
-
- Displays the summary line of the destination for clarity.
- """
- wlock = None
- dryrunopt = opts['dry_run']
- if not dryrunopt:
- wlock = repo.wlock()
- try:
- wkctx = repo[None]
- wparents = wkctx.parents()
- if len(wparents) != 1:
- raise error.Abort('merge in progress')
- if not opts['merge']:
- try:
- cmdutil.bailifchanged(repo)
- except error.Abort as exc:
- exc.hint = _('do you want --merge?')
- raise
-
- children = [ctx for ctx in wparents[0].children() if not ctx.obsolete()]
- topic = getattr(repo, 'currenttopic', '')
- filtered = []
- if topic and not opts.get("no_topic", False):
- filtered = [ctx for ctx in children if ctx.topic() != topic]
- # XXX N-square membership on children
- children = [ctx for ctx in children if ctx not in filtered]
- displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
- if len(children) == 1:
- c = children[0]
- bm = bmactive(repo)
- shouldmove = opts.get('move_bookmark') and bm is not None
- if dryrunopt:
- ui.write(('hg update %s;\n' % c.rev()))
- if shouldmove:
- ui.write(('hg bookmark %s -r %s;\n' % (bm, c.rev())))
- else:
- ret = hg.update(repo, c.rev())
- if not ret:
- lock = tr = None
- try:
- lock = repo.lock()
- tr = repo.transaction('next')
- if shouldmove:
- repo._bookmarks[bm] = c.node()
- repo._bookmarks.recordchange(tr)
- else:
- bmdeactivate(repo)
- tr.close()
- finally:
- lockmod.release(tr, lock)
- displayer.show(c)
- result = 0
- elif children:
- ui.warn(_("ambigious next changeset:\n"))
- for c in children:
- displayer.show(c)
- ui.warn(_('explicitly update to one of them\n'))
- result = 1
- else:
- aspchildren = _aspiringchildren(repo, [repo['.'].rev()])
- if topic:
- filtered.extend(repo[c] for c in children
- if repo[c].topic() != topic)
- # XXX N-square membership on children
- aspchildren = [ctx for ctx in aspchildren if ctx not in filtered]
- if not opts['evolve'] or not aspchildren:
- if filtered:
- ui.warn(_('no children on topic "%s"\n') % topic)
- ui.warn(_('do you want --no-topic\n'))
- else:
- ui.warn(_('no children\n'))
- if aspchildren:
- msg = _('(%i unstable changesets to be evolved here, '
- 'do you want --evolve?)\n')
- ui.warn(msg % len(aspchildren))
- result = 1
- elif 1 < len(aspchildren):
- ui.warn(_("ambigious next (unstable) changeset:\n"))
- for c in aspchildren:
- displayer.show(repo[c])
- ui.warn(_("(run 'hg evolve --rev REV' on one of them)\n"))
- return 1
- else:
- cmdutil.bailifchanged(repo)
- result = _solveone(ui, repo, repo[aspchildren[0]], dryrunopt,
- False, lambda:None, category='unstable')
- if not result:
- ui.status(_('working directory now at %s\n') % repo['.'])
- return result
- return 1
- return result
- finally:
- lockmod.release(wlock)
-
-def _reachablefrombookmark(repo, revs, bookmarks):
- """filter revisions and bookmarks reachable from the given bookmark
- yoinked from mq.py
- """
- repomarks = repo._bookmarks
- if not bookmarks.issubset(repomarks):
- raise error.Abort(_("bookmark '%s' not found") %
- ','.join(sorted(bookmarks - set(repomarks.keys()))))
-
- # If the requested bookmark is not the only one pointing to a
- # a revision we have to only delete the bookmark and not strip
- # anything. revsets cannot detect that case.
- nodetobookmarks = {}
- for mark, node in repomarks.iteritems():
- nodetobookmarks.setdefault(node, []).append(mark)
- for marks in nodetobookmarks.values():
- if bookmarks.issuperset(marks):
- if util.safehasattr(repair, 'stripbmrevset'):
- rsrevs = repair.stripbmrevset(repo, marks[0])
- else:
- rsrevs = repo.revs("ancestors(bookmark(%s)) - "
- "ancestors(head() and not bookmark(%s)) - "
- "ancestors(bookmark() and not bookmark(%s)) - "
- "obsolete()",
- marks[0], marks[0], marks[0])
- revs = set(revs)
- revs.update(set(rsrevs))
- revs = sorted(revs)
- return repomarks, revs
-
-def _deletebookmark(repo, repomarks, bookmarks):
- wlock = lock = tr = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- tr = repo.transaction('prune')
- for bookmark in bookmarks:
- del repomarks[bookmark]
- repomarks.recordchange(tr)
- tr.close()
- for bookmark in sorted(bookmarks):
- repo.ui.write(_("bookmark '%s' deleted\n") % bookmark)
- finally:
- lockmod.release(tr, lock, wlock)
-
-
-
-def _getmetadata(**opts):
- metadata = {}
- date = opts.get('date')
- user = opts.get('user')
- if date:
- metadata['date'] = '%i %i' % util.parsedate(date)
- if user:
- metadata['user'] = user
- return metadata
-
-
-@command('^prune|obsolete',
- [('n', 'new', [], _("successor changeset (DEPRECATED)")),
- ('s', 'succ', [], _("successor changeset")),
- ('r', 'rev', [], _("revisions to prune")),
- ('k', 'keep', None, _("does not modify working copy during prune")),
- ('', 'biject', False, _("do a 1-1 map between rev and successor ranges")),
- ('', 'fold', False,
- _("record a fold (multiple precursors, one successors)")),
- ('', 'split', False,
- _("record a split (on precursor, multiple successors)")),
- ('B', 'bookmark', [], _("remove revs only reachable from given"
- " bookmark"))] + metadataopts,
- _('[OPTION] [-r] REV...'))
- # -U --noupdate option to prevent wc update and or bookmarks update ?
-def cmdprune(ui, repo, *revs, **opts):
- """hide changesets by marking them obsolete
-
- Pruned changesets are obsolete with no successors. If they also have no
- descendants, they are hidden (invisible to all commands).
-
- Non-obsolete descendants of pruned changesets become "unstable". Use :hg:`evolve`
- to handle this situation.
-
- When you prune the parent of your working copy, Mercurial updates the working
- copy to a non-obsolete parent.
-
- You can use ``--succ`` to tell Mercurial that a newer version (successor) of the
- pruned changeset exists. Mercurial records successor revisions in obsolescence
- markers.
-
- You can use the ``--biject`` option to specify a 1-1 mapping (bijection) between
- revisions to pruned (precursor) and successor changesets. This option may be
- removed in a future release (with the functionality provided automatically).
-
- If you specify multiple revisions in ``--succ``, you are recording a "split" and
- must acknowledge it by passing ``--split``. Similarly, when you prune multiple
- changesets with a single successor, you must pass the ``--fold`` option.
- """
- revs = scmutil.revrange(repo, list(revs) + opts.get('rev'))
- succs = opts['new'] + opts['succ']
- bookmarks = set(opts.get('bookmark'))
- metadata = _getmetadata(**opts)
- biject = opts.get('biject')
- fold = opts.get('fold')
- split = opts.get('split')
-
- options = [o for o in ('biject', 'fold', 'split') if opts.get(o)]
- if 1 < len(options):
- raise error.Abort(_("can only specify one of %s") % ', '.join(options))
-
- if bookmarks:
- repomarks, revs = _reachablefrombookmark(repo, revs, bookmarks)
- if not revs:
- # no revisions to prune - delete bookmark immediately
- _deletebookmark(repo, repomarks, bookmarks)
-
- if not revs:
- raise error.Abort(_('nothing to prune'))
-
- wlock = lock = tr = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- tr = repo.transaction('prune')
- # defines pruned changesets
- precs = []
- revs.sort()
- for p in revs:
- cp = repo[p]
- if not cp.mutable():
- # note: createmarkers() would have raised something anyway
- raise error.Abort('cannot prune immutable changeset: %s' % cp,
- hint="see 'hg help phases' for details")
- precs.append(cp)
- if not precs:
- raise error.Abort('nothing to prune')
-
- if _disallowednewunstable(repo, revs):
- raise error.Abort(_("cannot prune in the middle of a stack"),
- hint = _("new unstable changesets are not allowed"))
-
- # defines successors changesets
- sucs = scmutil.revrange(repo, succs)
- sucs.sort()
- sucs = tuple(repo[n] for n in sucs)
- if not biject and len(sucs) > 1 and len(precs) > 1:
- msg = "Can't use multiple successors for multiple precursors"
- hint = _("use --biject to mark a series as a replacement"
- " for another")
- raise error.Abort(msg, hint=hint)
- elif biject and len(sucs) != len(precs):
- msg = "Can't use %d successors for %d precursors" \
- % (len(sucs), len(precs))
- raise error.Abort(msg)
- elif (len(precs) == 1 and len(sucs) > 1) and not split:
- msg = "please add --split if you want to do a split"
- raise error.Abort(msg)
- elif len(sucs) == 1 and len(precs) > 1 and not fold:
- msg = "please add --fold if you want to do a fold"
- raise error.Abort(msg)
- elif biject:
- relations = [(p, (s,)) for p, s in zip(precs, sucs)]
- else:
- relations = [(p, sucs) for p in precs]
-
- wdp = repo['.']
-
- if len(sucs) == 1 and len(precs) == 1 and wdp in precs:
- # '.' killed, so update to the successor
- newnode = sucs[0]
- else:
- # update to an unkilled parent
- newnode = wdp
-
- while newnode in precs or newnode.obsolete():
- newnode = newnode.parents()[0]
-
-
- if newnode.node() != wdp.node():
- if opts.get('keep', False):
- # This is largely the same as the implementation in
- # strip.stripcmd(). We might want to refactor this somewhere
- # common at some point.
-
- # only reset the dirstate for files that would actually change
- # between the working context and uctx
- descendantrevs = repo.revs("%d::." % newnode.rev())
- changedfiles = []
- for rev in descendantrevs:
- # blindly reset the files, regardless of what actually
- # changed
- changedfiles.extend(repo[rev].files())
-
- # reset files that only changed in the dirstate too
- dirstate = repo.dirstate
- dirchanges = [f for f in dirstate if dirstate[f] != 'n']
- changedfiles.extend(dirchanges)
- repo.dirstate.rebuild(newnode.node(), newnode.manifest(),
- changedfiles)
- writedirstate(dirstate, tr)
- else:
- bookactive = bmactive(repo)
- # Active bookmark that we don't want to delete (with -B option)
- # we deactivate and move it before the update and reactivate it
- # after
- movebookmark = bookactive and not bookmarks
- if movebookmark:
- bmdeactivate(repo)
- repo._bookmarks[bookactive] = newnode.node()
- repo._bookmarks.recordchange(tr)
- commands.update(ui, repo, newnode.rev())
- ui.status(_('working directory now at %s\n') % newnode)
- if movebookmark:
- bmactivate(repo, bookactive)
-
- # update bookmarks
- if bookmarks:
- _deletebookmark(repo, repomarks, bookmarks)
-
- # create markers
- obsolete.createmarkers(repo, relations, metadata=metadata)
-
- # informs that changeset have been pruned
- ui.status(_('%i changesets pruned\n') % len(precs))
-
- for ctx in repo.unfiltered().set('bookmark() and %ld', precs):
- # used to be:
- #
- # ldest = list(repo.set('max((::%d) - obsolete())', ctx))
- # if ldest:
- # c = ldest[0]
- #
- # but then revset took a lazy arrow in the knee and became much
- # slower. The new forms makes as much sense and a much faster.
- for dest in ctx.ancestors():
- if not dest.obsolete():
- updatebookmarks = _bookmarksupdater(repo, ctx.node(), tr)
- updatebookmarks(dest.node())
- break
-
- tr.close()
- finally:
- lockmod.release(tr, lock, wlock)
-
-@command('amend|refresh',
- [('A', 'addremove', None,
- _('mark new/missing files as added/removed before committing')),
- ('e', 'edit', False, _('invoke editor on commit messages')),
- ('', 'close-branch', None,
- _('mark a branch as closed, hiding it from the branch list')),
- ('s', 'secret', None, _('use the secret phase for committing')),
- ] + walkopts + commitopts + commitopts2 + commitopts3 + interactiveopt,
- _('[OPTION]... [FILE]...'))
-def amend(ui, repo, *pats, **opts):
- """combine a changeset with updates and replace it with a new one
-
- Commits a new changeset incorporating both the changes to the given files
- and all the changes from the current parent changeset into the repository.
-
- See :hg:`commit` for details about committing changes.
-
- If you don't specify -m, the parent's message will be reused.
-
- Behind the scenes, Mercurial first commits the update as a regular child
- of the current parent. Then it creates a new commit on the parent's parents
- with the updated contents. Then it changes the working copy parent to this
- new combined changeset. Finally, the old changeset and its update are hidden
- from :hg:`log` (unless you use --hidden with log).
-
- Returns 0 on success, 1 if nothing changed.
- """
- opts = opts.copy()
- edit = opts.pop('edit', False)
- log = opts.get('logfile')
- opts['amend'] = True
- if not (edit or opts['message'] or log):
- opts['message'] = repo['.'].description()
- _resolveoptions(ui, opts)
- _alias, commitcmd = cmdutil.findcmd('commit', commands.table)
- return commitcmd[0](ui, repo, *pats, **opts)
-
-
-def _touchedbetween(repo, source, dest, match=None):
- touched = set()
- for files in repo.status(source, dest, match=match)[:3]:
- touched.update(files)
- return touched
-
-def _commitfiltered(repo, ctx, match, target=None):
- """Recommit ctx with changed files not in match. Return the new
- node identifier, or None if nothing changed.
- """
- base = ctx.p1()
- if target is None:
- target = base
- # ctx
- initialfiles = _touchedbetween(repo, base, ctx)
- if base == target:
- affected = set(f for f in initialfiles if match(f))
- newcontent = set()
- else:
- affected = _touchedbetween(repo, target, ctx, match=match)
- newcontent = _touchedbetween(repo, target, base, match=match)
- # The commit touchs all existing files
- # + all file that needs a new content
- # - the file affected bny uncommit with the same content than base.
- files = (initialfiles - affected) | newcontent
- if not newcontent and files == initialfiles:
- return None
-
- # Filter copies
- copied = copies.pathcopies(target, ctx)
- copied = dict((dst, src) for dst, src in copied.iteritems()
- if dst in files)
- def filectxfn(repo, memctx, path, contentctx=ctx, redirect=newcontent):
- if path in redirect:
- return filectxfn(repo, memctx, path, contentctx=target, redirect=())
- if path not in contentctx:
- return None
- fctx = contentctx[path]
- flags = fctx.flags()
- mctx = memfilectx(repo, fctx.path(), fctx.data(),
- islink='l' in flags,
- isexec='x' in flags,
- copied=copied.get(path))
- return mctx
-
- new = context.memctx(repo,
- parents=[base.node(), node.nullid],
- text=ctx.description(),
- files=files,
- filectxfn=filectxfn,
- user=ctx.user(),
- date=ctx.date(),
- extra=ctx.extra())
- # commitctx always create a new revision, no need to check
- newid = repo.commitctx(new)
- return newid
-
-def _uncommitdirstate(repo, oldctx, match):
- """Fix the dirstate after switching the working directory from
- oldctx to a copy of oldctx not containing changed files matched by
- match.
- """
- ctx = repo['.']
- ds = repo.dirstate
- copies = dict(ds.copies())
- m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
- for f in m:
- if ds[f] == 'r':
- # modified + removed -> removed
- continue
- ds.normallookup(f)
-
- for f in a:
- if ds[f] == 'r':
- # added + removed -> unknown
- ds.drop(f)
- elif ds[f] != 'a':
- ds.add(f)
-
- for f in r:
- if ds[f] == 'a':
- # removed + added -> normal
- ds.normallookup(f)
- elif ds[f] != 'r':
- ds.remove(f)
-
- # Merge old parent and old working dir copies
- oldcopies = {}
- for f in (m + a):
- src = oldctx[f].renamed()
- if src:
- oldcopies[f] = src[0]
- oldcopies.update(copies)
- copies = dict((dst, oldcopies.get(src, src))
- for dst, src in oldcopies.iteritems())
- # Adjust the dirstate copies
- for dst, src in copies.iteritems():
- if (src not in ctx or dst in ctx or ds[dst] != 'a'):
- src = None
- ds.copy(src, dst)
-
-@command('^uncommit',
- [('a', 'all', None, _('uncommit all changes when no arguments given')),
- ('r', 'rev', '', _('revert commit content to REV instead')),
- ] + commands.walkopts,
- _('[OPTION]... [NAME]'))
-def uncommit(ui, repo, *pats, **opts):
- """move changes from parent revision to working directory
-
- Changes to selected files in the checked out revision appear again as
- uncommitted changed in the working directory. A new revision
- without the selected changes is created, becomes the checked out
- revision, and obsoletes the previous one.
-
- The --include option specifies patterns to uncommit.
- The --exclude option specifies patterns to keep in the commit.
-
- The --rev argument let you change the commit file to a content of another
- revision. It still does not change the content of your file in the working
- directory.
-
- Return 0 if changed files are uncommitted.
- """
-
- wlock = lock = tr = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- wctx = repo[None]
- if len(wctx.parents()) <= 0:
- raise error.Abort(_("cannot uncommit null changeset"))
- if len(wctx.parents()) > 1:
- raise error.Abort(_("cannot uncommit while merging"))
- old = repo['.']
- if old.phase() == phases.public:
- raise error.Abort(_("cannot rewrite immutable changeset"))
- if len(old.parents()) > 1:
- raise error.Abort(_("cannot uncommit merge changeset"))
- oldphase = old.phase()
-
-
- rev = None
- if opts.get('rev'):
- rev = scmutil.revsingle(repo, opts.get('rev'))
- ctx = repo[None]
- if ctx.p1() == rev or ctx.p2() == rev:
- raise error.Abort(_("cannot uncommit to parent changeset"))
-
- onahead = old.rev() in repo.changelog.headrevs()
- disallowunstable = not obsolete.isenabled(repo,
- obsolete.allowunstableopt)
- if disallowunstable and not onahead:
- raise error.Abort(_("cannot uncommit in the middle of a stack"))
-
- # Recommit the filtered changeset
- tr = repo.transaction('uncommit')
- updatebookmarks = _bookmarksupdater(repo, old.node(), tr)
- newid = None
- includeorexclude = opts.get('include') or opts.get('exclude')
- if (pats or includeorexclude or opts.get('all')):
- match = scmutil.match(old, pats, opts)
- newid = _commitfiltered(repo, old, match, target=rev)
- if newid is None:
- raise error.Abort(_('nothing to uncommit'),
- hint=_("use --all to uncommit all files"))
- # Move local changes on filtered changeset
- obsolete.createmarkers(repo, [(old, (repo[newid],))])
- phases.retractboundary(repo, tr, oldphase, [newid])
- repo.dirstate.beginparentchange()
- repo.dirstate.setparents(newid, node.nullid)
- _uncommitdirstate(repo, old, match)
- repo.dirstate.endparentchange()
- updatebookmarks(newid)
- if not repo[newid].files():
- ui.warn(_("new changeset is empty\n"))
- ui.status(_("(use 'hg prune .' to remove it)\n"))
- tr.close()
- finally:
- lockmod.release(tr, lock, wlock)
-
-@eh.wrapcommand('commit')
-def commitwrapper(orig, ui, repo, *arg, **kwargs):
- tr = None
- if kwargs.get('amend', False):
- wlock = lock = None
- else:
- wlock = repo.wlock()
- lock = repo.lock()
- try:
- obsoleted = kwargs.get('obsolete', [])
- if obsoleted:
- obsoleted = repo.set('%lr', obsoleted)
- result = orig(ui, repo, *arg, **kwargs)
- if not result: # commit succeeded
- new = repo['-1']
- oldbookmarks = []
- markers = []
- for old in obsoleted:
- oldbookmarks.extend(repo.nodebookmarks(old.node()))
- markers.append((old, (new,)))
- if markers:
- obsolete.createmarkers(repo, markers)
- for book in oldbookmarks:
- repo._bookmarks[book] = new.node()
- if oldbookmarks:
- if not wlock:
- wlock = repo.wlock()
- if not lock:
- lock = repo.lock()
- tr = repo.transaction('commit')
- repo._bookmarks.recordchange(tr)
- tr.close()
- return result
- finally:
- lockmod.release(tr, lock, wlock)
-
-@command('^split',
- [('r', 'rev', [], _("revision to split")),
- ] + commitopts + commitopts2,
- _('hg split [OPTION]... [-r] REV'))
-def cmdsplit(ui, repo, *revs, **opts):
- """split a changeset into smaller changesets
-
- By default, split the current revision by prompting for all its hunks to be
- redistributed into new changesets.
-
- Use --rev to split a given changeset instead.
- """
- tr = wlock = lock = None
- newcommits = []
-
- revarg = (list(revs) + opts.get('rev')) or ['.']
- if len(revarg) != 1:
- msg = _("more than one revset is given")
- hnt = _("use either `hg split <rs>` or `hg split --rev <rs>`, not both")
- raise error.Abort(msg, hint=hnt)
-
- rev = scmutil.revsingle(repo, revarg[0])
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- cmdutil.bailifchanged(repo)
- tr = repo.transaction('split')
- ctx = repo[rev]
- r = ctx.rev()
- disallowunstable = not obsolete.isenabled(repo,
- obsolete.allowunstableopt)
- if disallowunstable:
- # XXX We should check head revs
- if repo.revs("(%d::) - %d", rev, rev):
- raise error.Abort(_("cannot split commit: %s not a head") % ctx)
-
- if len(ctx.parents()) > 1:
- raise error.Abort(_("cannot split merge commits"))
- prev = ctx.p1()
- bmupdate = _bookmarksupdater(repo, ctx.node(), tr)
- bookactive = bmactive(repo)
- if bookactive is not None:
- repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo))
- bmdeactivate(repo)
- hg.update(repo, prev)
-
- commands.revert(ui, repo, rev=r, all=True)
- def haschanges():
- modified, added, removed, deleted = repo.status()[:4]
- return modified or added or removed or deleted
- msg = ("HG: This is the original pre-split commit message. "
- "Edit it as appropriate.\n\n")
- msg += ctx.description()
- opts['message'] = msg
- opts['edit'] = True
- while haschanges():
- pats = ()
- cmdutil.dorecord(ui, repo, commands.commit, 'commit', False,
- cmdutil.recordfilter, *pats, **opts)
- # TODO: Does no seem like the best way to do this
- # We should make dorecord return the newly created commit
- newcommits.append(repo['.'])
- if haschanges():
- if ui.prompt('Done splitting? [yN]', default='n') == 'y':
- commands.commit(ui, repo, **opts)
- newcommits.append(repo['.'])
- break
- else:
- ui.status(_("no more change to split\n"))
-
- if newcommits:
- tip = repo[newcommits[-1]]
- bmupdate(tip.node())
- if bookactive is not None:
- bmactivate(repo, bookactive)
- obsolete.createmarkers(repo, [(repo[r], newcommits)])
- tr.close()
- finally:
- lockmod.release(tr, lock, wlock)
-
-
-@eh.wrapcommand('strip', extension='strip', opts=[
- ('', 'bundle', None, _("delete the commit entirely and move it to a "
- "backup bundle")),
- ])
-def stripwrapper(orig, ui, repo, *revs, **kwargs):
- if (not ui.configbool('experimental', 'prunestrip') or
- kwargs.get('bundle', False)):
- return orig(ui, repo, *revs, **kwargs)
-
- if kwargs.get('force'):
- ui.warn(_("warning: --force has no effect during strip with evolve "
- "enabled\n"))
- if kwargs.get('no_backup', False):
- ui.warn(_("warning: --no-backup has no effect during strips with "
- "evolve enabled\n"))
-
- revs = list(revs) + kwargs.pop('rev', [])
- revs = set(scmutil.revrange(repo, revs))
- revs = repo.revs("(%ld)::", revs)
- kwargs['rev'] = []
- kwargs['new'] = []
- kwargs['succ'] = []
- kwargs['biject'] = False
- return cmdprune(ui, repo, *revs, **kwargs)
-
-@command('^touch',
- [('r', 'rev', [], 'revision to update'),
- ('D', 'duplicate', False,
- 'do not mark the new revision as successor of the old one'),
- ('A', 'allowdivergence', False,
- 'mark the new revision as successor of the old one potentially creating '
- 'divergence')],
- # allow to choose the seed ?
- _('[-r] revs'))
-def touch(ui, repo, *revs, **opts):
- """create successors that are identical to their predecessors except
- for the changeset ID
-
- This is used to "resurrect" changesets
- """
- duplicate = opts['duplicate']
- allowdivergence = opts['allowdivergence']
- revs = list(revs)
- revs.extend(opts['rev'])
- if not revs:
- revs = ['.']
- revs = scmutil.revrange(repo, revs)
- if not revs:
- ui.write_err('no revision to touch\n')
- return 1
- if not duplicate and repo.revs('public() and %ld', revs):
- raise error.Abort("can't touch public revision")
- displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
- wlock = lock = tr = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- tr = repo.transaction('touch')
- revs.sort() # ensure parent are run first
- newmapping = {}
- for r in revs:
- ctx = repo[r]
- extra = ctx.extra().copy()
- extra['__touch-noise__'] = random.randint(0, 0xffffffff)
- # search for touched parent
- p1 = ctx.p1().node()
- p2 = ctx.p2().node()
- p1 = newmapping.get(p1, p1)
- p2 = newmapping.get(p2, p2)
-
- if not (duplicate or allowdivergence):
- # The user hasn't yet decided what to do with the revived
- # cset, let's ask
- sset = obsolete.successorssets(repo, ctx.node())
- nodivergencerisk = len(sset) == 0 or (
- len(sset) == 1 and
- len(sset[0]) == 1 and
- repo[sset[0][0]].rev() == ctx.rev()
- )
- if nodivergencerisk:
- duplicate = False
- else:
- displayer.show(ctx)
- index = ui.promptchoice(
- _("reviving this changeset will create divergence"
- " unless you make a duplicate.\n(a)llow divergence or"
- " (d)uplicate the changeset? $$ &Allowdivergence $$ "
- "&Duplicate"), 0)
- choice = ['allowdivergence', 'duplicate'][index]
- if choice == 'allowdivergence':
- duplicate = False
- else:
- duplicate = True
-
- new, unusedvariable = rewrite(repo, ctx, [], ctx,
- [p1, p2],
- commitopts={'extra': extra})
- # store touched version to help potential children
- newmapping[ctx.node()] = new
-
- if not duplicate:
- obsolete.createmarkers(repo, [(ctx, (repo[new],))])
- phases.retractboundary(repo, tr, ctx.phase(), [new])
- if ctx in repo[None].parents():
- repo.dirstate.beginparentchange()
- repo.dirstate.setparents(new, node.nullid)
- repo.dirstate.endparentchange()
- tr.close()
- finally:
- lockmod.release(tr, lock, wlock)
-
-@command('^fold|squash',
- [('r', 'rev', [], _("revision to fold")),
- ('', 'exact', None, _("only fold specified revisions")),
- ('', 'from', None, _("fold revisions linearly to working copy parent"))
- ] + commitopts + commitopts2,
- _('hg fold [OPTION]... [-r] REV'))
-def fold(ui, repo, *revs, **opts):
- """fold multiple revisions into a single one
-
- With --from, folds all the revisions linearly between the given revisions
- and the parent of the working directory.
-
- With --exact, folds only the specified revisions while ignoring the
- parent of the working directory. In this case, the given revisions must
- form a linear unbroken chain.
-
- .. container:: verbose
-
- Some examples:
-
- - Fold the current revision with its parent::
-
- hg fold --from .^
-
- - Fold all draft revisions with working directory parent::
-
- hg fold --from 'draft()'
-
- See :hg:`help phases` for more about draft revisions and
- :hg:`help revsets` for more about the `draft()` keyword
-
- - Fold revisions between 3 and 6 with the working directory parent::
-
- hg fold --from 3::6
-
- - Fold revisions 3 and 4:
-
- hg fold "3 + 4" --exact
-
- - Only fold revisions linearly between foo and @::
-
- hg fold foo::@ --exact
- """
- revs = list(revs)
- revs.extend(opts['rev'])
- if not revs:
- raise error.Abort(_('no revisions specified'))
-
- revs = scmutil.revrange(repo, revs)
-
- if opts['from'] and opts['exact']:
- raise error.Abort(_('cannot use both --from and --exact'))
- elif opts['from']:
- # Try to extend given revision starting from the working directory
- extrevs = repo.revs('(%ld::.) or (.::%ld)', revs, revs)
- discardedrevs = [r for r in revs if r not in extrevs]
- if discardedrevs:
- raise error.Abort(_("cannot fold non-linear revisions"),
- hint=_("given revisions are unrelated to parent "
- "of working directory"))
- revs = extrevs
- elif opts['exact']:
- # Nothing to do; "revs" is already set correctly
- pass
- else:
- raise error.Abort(_('must specify either --from or --exact'))
-
- if not revs:
- raise error.Abort(_('specified revisions evaluate to an empty set'),
- hint=_('use different revision arguments'))
- elif len(revs) == 1:
- ui.write_err(_('single revision specified, nothing to fold\n'))
- return 1
-
- wlock = lock = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
-
- root, head = _foldcheck(repo, revs)
-
- tr = repo.transaction('fold')
- try:
- commitopts = opts.copy()
- allctx = [repo[r] for r in revs]
- targetphase = max(c.phase() for c in allctx)
-
- if commitopts.get('message') or commitopts.get('logfile'):
- commitopts['edit'] = False
- else:
- msgs = ["HG: This is a fold of %d changesets." % len(allctx)]
- msgs += ["HG: Commit message of changeset %s.\n\n%s\n" %
- (c.rev(), c.description()) for c in allctx]
- commitopts['message'] = "\n".join(msgs)
- commitopts['edit'] = True
-
- newid, unusedvariable = rewrite(repo, root, allctx, head,
- [root.p1().node(),
- root.p2().node()],
- commitopts=commitopts)
- phases.retractboundary(repo, tr, targetphase, [newid])
- obsolete.createmarkers(repo, [(ctx, (repo[newid],))
- for ctx in allctx])
- tr.close()
- finally:
- tr.release()
- ui.status('%i changesets folded\n' % len(revs))
- if repo['.'].rev() in revs:
- hg.update(repo, newid)
- finally:
- lockmod.release(lock, wlock)
-
-@command('^metaedit',
- [('r', 'rev', [], _("revision to edit")),
- ('', 'fold', None, _("also fold specified revisions into one")),
- ] + commitopts + commitopts2,
- _('hg metaedit [OPTION]... [-r] [REV]'))
-def metaedit(ui, repo, *revs, **opts):
- """edit commit information
-
- Edits the commit information for the specified revisions. By default, edits
- commit information for the working directory parent.
-
- With --fold, also folds multiple revisions into one if necessary. In this
- case, the given revisions must form a linear unbroken chain.
-
- .. container:: verbose
-
- Some examples:
-
- - Edit the commit message for the working directory parent::
-
- hg metaedit
-
- - Change the username for the working directory parent::
-
- hg metaedit --user 'New User <new-email@example.com>'
-
- - Combine all draft revisions that are ancestors of foo but not of @ into
- one::
-
- hg metaedit --fold 'draft() and only(foo,@)'
-
- See :hg:`help phases` for more about draft revisions, and
- :hg:`help revsets` for more about the `draft()` and `only()` keywords.
- """
- revs = list(revs)
- revs.extend(opts['rev'])
- if not revs:
- if opts['fold']:
- raise error.Abort(_('revisions must be specified with --fold'))
- revs = ['.']
-
- wlock = lock = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
-
- revs = scmutil.revrange(repo, revs)
- if not opts['fold'] and len(revs) > 1:
- # TODO: handle multiple revisions. This is somewhat tricky because
- # if we want to edit a series of commits:
- #
- # a ---- b ---- c
- #
- # we need to rewrite a first, then directly rewrite b on top of the
- # new a, then rewrite c on top of the new b. So we need to handle
- # revisions in topological order.
- raise error.Abort(_('editing multiple revisions without --fold is '
- 'not currently supported'))
-
- if opts['fold']:
- root, head = _foldcheck(repo, revs)
- else:
- if repo.revs("%ld and public()", revs):
- raise error.Abort(_('cannot edit commit information for public '
- 'revisions'))
- newunstable = _disallowednewunstable(repo, revs)
- if newunstable:
- raise error.Abort(
- _('cannot edit commit information in the middle of a '\
- 'stack'), hint=_('%s will become unstable and new unstable'\
- ' changes are not allowed') % repo[newunstable.first()])
- root = head = repo[revs.first()]
-
- wctx = repo[None]
- p1 = wctx.p1()
- tr = repo.transaction('metaedit')
- newp1 = None
- try:
- commitopts = opts.copy()
- allctx = [repo[r] for r in revs]
- targetphase = max(c.phase() for c in allctx)
-
- if commitopts.get('message') or commitopts.get('logfile'):
- commitopts['edit'] = False
- else:
- if opts['fold']:
- msgs = ["HG: This is a fold of %d changesets." % len(allctx)]
- msgs += ["HG: Commit message of changeset %s.\n\n%s\n" %
- (c.rev(), c.description()) for c in allctx]
- else:
- msgs = [head.description()]
- commitopts['message'] = "\n".join(msgs)
- commitopts['edit'] = True
-
- # TODO: if the author and message are the same, don't create a new
- # hash. Right now we create a new hash because the date can be
- # different.
- newid, created = rewrite(repo, root, allctx, head,
- [root.p1().node(), root.p2().node()],
- commitopts=commitopts)
- if created:
- if p1.rev() in revs:
- newp1 = newid
- phases.retractboundary(repo, tr, targetphase, [newid])
- obsolete.createmarkers(repo, [(ctx, (repo[newid],))
- for ctx in allctx])
- else:
- ui.status(_("nothing changed\n"))
- tr.close()
- finally:
- tr.release()
-
- if opts['fold']:
- ui.status('%i changesets folded\n' % len(revs))
- if newp1 is not None:
- hg.update(repo, newp1)
- finally:
- lockmod.release(lock, wlock)
-
-def _foldcheck(repo, revs):
- roots = repo.revs('roots(%ld)', revs)
- if len(roots) > 1:
- raise error.Abort(_("cannot fold non-linear revisions "
- "(multiple roots given)"))
- root = repo[roots.first()]
- if root.phase() <= phases.public:
- raise error.Abort(_("cannot fold public revisions"))
- heads = repo.revs('heads(%ld)', revs)
- if len(heads) > 1:
- raise error.Abort(_("cannot fold non-linear revisions "
- "(multiple heads given)"))
- head = repo[heads.first()]
- if _disallowednewunstable(repo, revs):
- raise error.Abort(_("cannot fold chain not ending with a head "\
- "or with branching"), hint = _("new unstable"\
- " changesets are not allowed"))
- return root, head
-
-def _disallowednewunstable(repo, revs):
- allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
- if allowunstable:
- return revset.baseset()
- return repo.revs("(%ld::) - %ld", revs, revs)
-
-@eh.wrapcommand('graft')
-def graftwrapper(orig, ui, repo, *revs, **kwargs):
- kwargs = dict(kwargs)
- revs = list(revs) + kwargs.get('rev', [])
- kwargs['rev'] = []
- obsoleted = kwargs.setdefault('obsolete', [])
-
- wlock = lock = None
- try:
- wlock = repo.wlock()
- lock = repo.lock()
- if kwargs.get('old_obsolete'):
- if kwargs.get('continue'):
- obsoleted.extend(repo.opener.read('graftstate').splitlines())
- else:
- obsoleted.extend(revs)
- # convert obsolete target into revs to avoid alias joke
- obsoleted[:] = [str(i) for i in repo.revs('%lr', obsoleted)]
- if obsoleted and len(revs) > 1:
-
- raise error.Abort(_('cannot graft multiple revisions while '
- 'obsoleting (for now).'))
-
- return commitwrapper(orig, ui, repo,*revs, **kwargs)
- finally:
- lockmod.release(lock, wlock)
-
-@eh.extsetup
-def oldevolveextsetup(ui):
- for cmd in ['prune', 'uncommit', 'touch', 'fold']:
- try:
- entry = extensions.wrapcommand(cmdtable, cmd,
- warnobserrors)
- except error.UnknownCommand:
- # Commands may be disabled
- continue
-
- entry = cmdutil.findcmd('commit', commands.table)[1]
- entry[1].append(('o', 'obsolete', [],
- _("make commit obsolete this revision (DEPRECATED)")))
- entry = cmdutil.findcmd('graft', commands.table)[1]
- entry[1].append(('o', 'obsolete', [],
- _("make graft obsoletes this revision (DEPRECATED)")))
- entry[1].append(('O', 'old-obsolete', False,
- _("make graft obsoletes its source (DEPRECATED)")))
-
-#####################################################################
-### Obsolescence marker exchange experimenation ###
-#####################################################################
-
-def obsexcmsg(ui, message, important=False):
- verbose = ui.configbool('experimental', 'verbose-obsolescence-exchange',
- False)
- if verbose:
- message = 'OBSEXC: ' + message
- if important or verbose:
- ui.status(message)
-
-def obsexcprg(ui, *args, **kwargs):
- topic = 'obsmarkers exchange'
- if ui.configbool('experimental', 'verbose-obsolescence-exchange', False):
- topic = 'OBSEXC'
- ui.progress(topic, *args, **kwargs)
-
-@eh.wrapfunction(exchange, '_pushdiscoveryobsmarkers')
-def _pushdiscoveryobsmarkers(orig, pushop):
- if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
- and pushop.repo.obsstore
- and 'obsolete' in pushop.remote.listkeys('namespaces')):
- repo = pushop.repo
- obsexcmsg(repo.ui, "computing relevant nodes\n")
- revs = list(repo.revs('::%ln', pushop.futureheads))
- unfi = repo.unfiltered()
- cl = unfi.changelog
- if not pushop.remote.capable('_evoext_obshash_0'):
- # do not trust core yet
- # return orig(pushop)
- nodes = [cl.node(r) for r in revs]
- if nodes:
- obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n"
- % len(nodes))
- pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
- else:
- obsexcmsg(repo.ui, "markers already in sync\n")
- pushop.outobsmarkers = []
- pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
- return
-
- common = []
- obsexcmsg(repo.ui, "looking for common markers in %i nodes\n"
- % len(revs))
- commonrevs = list(unfi.revs('::%ln', pushop.outgoing.commonheads))
- common = findcommonobsmarkers(pushop.ui, unfi, pushop.remote,
- commonrevs)
-
- revs = list(unfi.revs('%ld - (::%ln)', revs, common))
- nodes = [cl.node(r) for r in revs]
- if nodes:
- obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n"
- % len(nodes))
- pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
- else:
- obsexcmsg(repo.ui, "markers already in sync\n")
- pushop.outobsmarkers = []
-
-@eh.wrapfunction(wireproto, 'capabilities')
-def discocapabilities(orig, repo, proto):
- """wrapper to advertise new capability"""
- caps = orig(repo, proto)
- if obsolete.isenabled(repo, obsolete.exchangeopt):
- caps += ' _evoext_obshash_0'
- return caps
-
-@eh.extsetup
-def _installobsmarkersdiscovery(ui):
- hgweb_mod.perms['evoext_obshash'] = 'pull'
- hgweb_mod.perms['evoext_obshash1'] = 'pull'
- # wrap command content
- oldcap, args = wireproto.commands['capabilities']
- def newcap(repo, proto):
- return discocapabilities(oldcap, repo, proto)
- wireproto.commands['capabilities'] = (newcap, args)
- wireproto.commands['evoext_obshash'] = (srv_obshash, 'nodes')
- wireproto.commands['evoext_obshash1'] = (srv_obshash1, 'nodes')
- if getattr(exchange, '_pushdiscoveryobsmarkers', None) is None:
- ui.warn(_('evolve: your mercurial version is too old\n'
- 'evolve: (running in degraded mode, push will '
- 'includes all markers)\n'))
- else:
- olddisco = exchange.pushdiscoverymapping['obsmarker']
- def newdisco(pushop):
- _pushdiscoveryobsmarkers(olddisco, pushop)
- exchange.pushdiscoverymapping['obsmarker'] = newdisco
-
-### Set discovery START
-
-from mercurial import dagutil
-from mercurial import setdiscovery
-
-def _obshash(repo, nodes, version=0):
- if version == 0:
- hashs = _obsrelsethashtreefm0(repo)
- elif version ==1:
- hashs = _obsrelsethashtreefm1(repo)
- else:
- assert False
- nm = repo.changelog.nodemap
- revs = [nm.get(n) for n in nodes]
- return [r is None and nullid or hashs[r][1] for r in revs]
-
-def srv_obshash(repo, proto, nodes):
- return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes)))
-
-def srv_obshash1(repo, proto, nodes):
- return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes),
- version=1))
-
-@eh.addattr(localrepo.localpeer, 'evoext_obshash')
-def local_obshash(peer, nodes):
- return _obshash(peer._repo, nodes)
-
-@eh.addattr(localrepo.localpeer, 'evoext_obshash1')
-def local_obshash1(peer, nodes):
- return _obshash(peer._repo, nodes, version=1)
-
-@eh.addattr(wireproto.wirepeer, 'evoext_obshash')
-def peer_obshash(self, nodes):
- d = self._call("evoext_obshash", nodes=wireproto.encodelist(nodes))
- try:
- return wireproto.decodelist(d)
- except ValueError:
- self._abort(error.ResponseError(_("unexpected response:"), d))
-
-@eh.addattr(wireproto.wirepeer, 'evoext_obshash1')
-def peer_obshash1(self, nodes):
- d = self._call("evoext_obshash1", nodes=wireproto.encodelist(nodes))
- try:
- return wireproto.decodelist(d)
- except ValueError:
- self._abort(error.ResponseError(_("unexpected response:"), d))
-
-def findcommonobsmarkers(ui, local, remote, probeset,
- initialsamplesize=100,
- fullsamplesize=200):
- # from discovery
- roundtrips = 0
- cl = local.changelog
- dag = dagutil.revlogdag(cl)
- missing = set()
- common = set()
- undecided = set(probeset)
- totalnb = len(undecided)
- ui.progress(_("comparing with other"), 0, total=totalnb)
- _takefullsample = setdiscovery._takefullsample
- if remote.capable('_evoext_obshash_1'):
- getremotehash = remote.evoext_obshash1
- localhash = _obsrelsethashtreefm1(local)
- else:
- getremotehash = remote.evoext_obshash
- localhash = _obsrelsethashtreefm0(local)
-
- while undecided:
-
- ui.note(_("sampling from both directions\n"))
- if len(undecided) < fullsamplesize:
- sample = set(undecided)
- else:
- sample = _takefullsample(dag, undecided, size=fullsamplesize)
-
- roundtrips += 1
- ui.progress(_("comparing with other"), totalnb - len(undecided),
- total=totalnb)
- ui.debug("query %i; still undecided: %i, sample size is: %i\n"
- % (roundtrips, len(undecided), len(sample)))
- # indices between sample and externalized version must match
- sample = list(sample)
- remotehash = getremotehash(dag.externalizeall(sample))
-
- yesno = [localhash[ix][1] == remotehash[si]
- for si, ix in enumerate(sample)]
-
- commoninsample = set(n for i, n in enumerate(sample) if yesno[i])
- common.update(dag.ancestorset(commoninsample, common))
-
- missinginsample = [n for i, n in enumerate(sample) if not yesno[i]]
- missing.update(dag.descendantset(missinginsample, missing))
-
- undecided.difference_update(missing)
- undecided.difference_update(common)
-
-
- ui.progress(_("comparing with other"), None)
- result = dag.headsetofconnecteds(common)
- ui.debug("%d total queries\n" % roundtrips)
-
- if not result:
- return set([nullid])
- return dag.externalizeall(result)
-
-
-_pushkeyescape = getattr(obsolete, '_pushkeyescape', None)
-
-class pushobsmarkerStringIO(StringIO):
- """hacky string io for progress"""
-
- @util.propertycache
- def length(self):
- return len(self.getvalue())
-
- def read(self, size=None):
- obsexcprg(self.ui, self.tell(), unit=_("bytes"), total=self.length)
- return StringIO.read(self, size)
-
- def __iter__(self):
- d = self.read(4096)
- while d:
- yield d
- d = self.read(4096)
-
-@eh.wrapfunction(exchange, '_pushobsolete')
-def _pushobsolete(orig, pushop):
- """utility function to push obsolete markers to a remote"""
- stepsdone = getattr(pushop, 'stepsdone', None)
- if stepsdone is not None:
- if 'obsmarkers' in stepsdone:
- return
- stepsdone.add('obsmarkers')
- if util.safehasattr(pushop, 'cgresult'):
- cgresult = pushop.cgresult
- else:
- cgresult = pushop.ret
- if cgresult == 0:
- return
- pushop.ui.debug('try to push obsolete markers to remote\n')
- repo = pushop.repo
- remote = pushop.remote
- if (obsolete.isenabled(repo, obsolete.exchangeopt) and repo.obsstore and
- 'obsolete' in remote.listkeys('namespaces')):
- markers = pushop.outobsmarkers
- if not markers:
- obsexcmsg(repo.ui, "no marker to push\n")
- elif remote.capable('_evoext_pushobsmarkers_0'):
- obsdata = pushobsmarkerStringIO()
- for chunk in obsolete.encodemarkers(markers, True):
- obsdata.write(chunk)
- obsdata.seek(0)
- obsdata.ui = repo.ui
- obsexcmsg(repo.ui, "pushing %i obsolescence markers (%i bytes)\n"
- % (len(markers), len(obsdata.getvalue())),
- True)
- remote.evoext_pushobsmarkers_0(obsdata)
- obsexcprg(repo.ui, None)
- else:
- rslts = []
- remotedata = _pushkeyescape(markers).items()
- totalbytes = sum(len(d) for k, d in remotedata)
- sentbytes = 0
- obsexcmsg(repo.ui, "pushing %i obsolescence markers in %i "
- "pushkey payload (%i bytes)\n"
- % (len(markers), len(remotedata), totalbytes),
- True)
- for key, data in remotedata:
- obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"),
- total=totalbytes)
- rslts.append(remote.pushkey('obsolete', key, '', data))
- sentbytes += len(data)
- obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"),
- total=totalbytes)
- obsexcprg(repo.ui, None)
- if [r for r in rslts if not r]:
- msg = _('failed to push some obsolete markers!\n')
- repo.ui.warn(msg)
- obsexcmsg(repo.ui, "DONE\n")
-
-
-@eh.addattr(wireproto.wirepeer, 'evoext_pushobsmarkers_0')
-def client_pushobsmarkers(self, obsfile):
- """wireprotocol peer method"""
- self.requirecap('_evoext_pushobsmarkers_0',
- _('push obsolete markers faster'))
- ret, output = self._callpush('evoext_pushobsmarkers_0', obsfile)
- for l in output.splitlines(True):
- self.ui.status(_('remote: '), l)
- return ret
-
-@eh.addattr(httppeer.httppeer, 'evoext_pushobsmarkers_0')
-def httpclient_pushobsmarkers(self, obsfile):
- """httpprotocol peer method
- (Cannot simply use _callpush as http is doing some special handling)"""
- self.requirecap('_evoext_pushobsmarkers_0',
- _('push obsolete markers faster'))
- try:
- r = self._call('evoext_pushobsmarkers_0', data=obsfile)
- vals = r.split('\n', 1)
- if len(vals) < 2:
- raise error.ResponseError(_("unexpected response:"), r)
-
- for l in vals[1].splitlines(True):
- if l.strip():
- self.ui.status(_('remote: '), l)
- return vals[0]
- except socket.error as err:
- if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
- raise error.Abort(_('push failed: %s') % err.args[1])
- raise error.Abort(err.args[1])
-
-@eh.wrapfunction(localrepo.localrepository, '_restrictcapabilities')
-def local_pushobsmarker_capabilities(orig, repo, caps):
- caps = orig(repo, caps)
- caps.add('_evoext_pushobsmarkers_0')
- return caps
-
-def _pushobsmarkers(repo, data):
- tr = lock = None
- try:
- lock = repo.lock()
- tr = repo.transaction('pushkey: obsolete markers')
- new = repo.obsstore.mergemarkers(tr, data)
- if new is not None:
- obsexcmsg(repo.ui, "%i obsolescence markers added\n" % new, True)
- tr.close()
- finally:
- lockmod.release(tr, lock)
- repo.hook('evolve_pushobsmarkers')
-
-@eh.addattr(localrepo.localpeer, 'evoext_pushobsmarkers_0')
-def local_pushobsmarkers(peer, obsfile):
- data = obsfile.read()
- _pushobsmarkers(peer._repo, data)
-
-def srv_pushobsmarkers(repo, proto):
- """wireprotocol command"""
- fp = StringIO()
- proto.redirect()
- proto.getfile(fp)
- data = fp.getvalue()
- fp.close()
- _pushobsmarkers(repo, data)
- return wireproto.pushres(0)
-
-def _buildpullobsmarkersboundaries(pullop):
- """small funtion returning the argument for pull markers call
- may to contains 'heads' and 'common'. skip the key for None.
-
- Its a separed functio to play around with strategy for that."""
- repo = pullop.repo
- remote = pullop.remote
- unfi = repo.unfiltered()
- revs = unfi.revs('::(%ln - null)', pullop.common)
- common = [nullid]
- if remote.capable('_evoext_obshash_0'):
- obsexcmsg(repo.ui, "looking for common markers in %i nodes\n"
- % len(revs))
- common = findcommonobsmarkers(repo.ui, repo, remote, revs)
- return {'heads': pullop.pulledsubset, 'common': common}
-
-@eh.uisetup
-def addgetbundleargs(self):
- gboptsmap['evo_obscommon'] = 'nodes'
-
-@eh.wrapfunction(exchange, '_pullbundle2extraprepare')
-def _addobscommontob2pull(orig, pullop, kwargs):
- ret = orig(pullop, kwargs)
- if ('obsmarkers' in kwargs and
- pullop.remote.capable('_evoext_getbundle_obscommon')):
- boundaries = _buildpullobsmarkersboundaries(pullop)
- common = boundaries['common']
- if common != [nullid]:
- kwargs['evo_obscommon'] = common
- return ret
-
-@eh.wrapfunction(exchange, '_getbundleobsmarkerpart')
-def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs):
- if 'evo_obscommon' not in kwargs:
- return orig(bundler, repo, source, **kwargs)
-
- heads = kwargs.get('heads')
- if kwargs.get('obsmarkers', False):
- if heads is None:
- heads = repo.heads()
- obscommon = kwargs.get('evo_obscommon', ())
- assert obscommon
- obsset = repo.unfiltered().set('::%ln - ::%ln', heads, obscommon)
- subset = [c.node() for c in obsset]
- markers = repo.obsstore.relevantmarkers(subset)
- exchange.buildobsmarkerspart(bundler, markers)
-
-@eh.uisetup
-def installgetbundlepartgen(ui):
- origfunc = exchange.getbundle2partsmapping['obsmarkers']
- def newfunc(*args, **kwargs):
- return _getbundleobsmarkerpart(origfunc, *args, **kwargs)
- exchange.getbundle2partsmapping['obsmarkers'] = newfunc
-
-@eh.wrapfunction(exchange, '_pullobsolete')
-def _pullobsolete(orig, pullop):
- if not obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
- return None
- if 'obsmarkers' not in getattr(pullop, 'todosteps', ['obsmarkers']):
- return None
- if 'obsmarkers' in getattr(pullop, 'stepsdone', []):
- return None
- wirepull = pullop.remote.capable('_evoext_pullobsmarkers_0')
- if not wirepull:
- return orig(pullop)
- if 'obsolete' not in pullop.remote.listkeys('namespaces'):
- return None # remote opted out of obsolescence marker exchange
- tr = None
- ui = pullop.repo.ui
- boundaries = _buildpullobsmarkersboundaries(pullop)
- if not set(boundaries['heads']) - set(boundaries['common']):
- obsexcmsg(ui, "nothing to pull\n")
- return None
-
- obsexcmsg(ui, "pull obsolescence markers\n", True)
- new = 0
-
- if wirepull:
- obsdata = pullop.remote.evoext_pullobsmarkers_0(**boundaries)
- obsdata = obsdata.read()
- if len(obsdata) > 5:
- obsexcmsg(ui, "merging obsolescence markers (%i bytes)\n"
- % len(obsdata))
- tr = pullop.gettransaction()
- old = len(pullop.repo.obsstore._all)
- pullop.repo.obsstore.mergemarkers(tr, obsdata)
- new = len(pullop.repo.obsstore._all) - old
- obsexcmsg(ui, "%i obsolescence markers added\n" % new, True)
- else:
- obsexcmsg(ui, "no unknown remote markers\n")
- obsexcmsg(ui, "DONE\n")
- if new:
- pullop.repo.invalidatevolatilesets()
- return tr
-
-def _getobsmarkersstream(repo, heads=None, common=None):
- revset = ''
- args = []
- repo = repo.unfiltered()
- if heads is None:
- revset = 'all()'
- elif heads:
- revset += "(::%ln)"
- args.append(heads)
- else:
- assert False, 'pulling no heads?'
- if common:
- revset += ' - (::%ln)'
- args.append(common)
- nodes = [c.node() for c in repo.set(revset, *args)]
- markers = repo.obsstore.relevantmarkers(nodes)
- obsdata = StringIO()
- for chunk in obsolete.encodemarkers(markers, True):
- obsdata.write(chunk)
- obsdata.seek(0)
- return obsdata
-
-@eh.addattr(wireproto.wirepeer, 'evoext_pullobsmarkers_0')
-def client_pullobsmarkers(self, heads=None, common=None):
- self.requirecap('_evoext_pullobsmarkers_0', _('look up remote obsmarkers'))
- opts = {}
- if heads is not None:
- opts['heads'] = wireproto.encodelist(heads)
- if common is not None:
- opts['common'] = wireproto.encodelist(common)
- if util.safehasattr(self, '_callcompressable'):
- f = self._callcompressable("evoext_pullobsmarkers_0", **opts)
- else:
- f = self._callstream("evoext_pullobsmarkers_0", **opts)
- f = self._decompress(f)
- length = int(f.read(20))
- chunk = 4096
- current = 0
- data = StringIO()
- ui = self.ui
- obsexcprg(ui, current, unit=_("bytes"), total=length)
- while current < length:
- readsize = min(length - current, chunk)
- data.write(f.read(readsize))
- current += readsize
- obsexcprg(ui, current, unit=_("bytes"), total=length)
- obsexcprg(ui, None)
- data.seek(0)
- return data
-
-@eh.addattr(localrepo.localpeer, 'evoext_pullobsmarkers_0')
-def local_pullobsmarkers(self, heads=None, common=None):
- return _getobsmarkersstream(self._repo, heads=heads, common=common)
-
-# The wireproto.streamres API changed, handling chunking and compression
-# directly. Handle either case.
-if util.safehasattr(wireproto.abstractserverproto, 'groupchunks'):
- # We need to handle chunking and compression directly
- def streamres(d, proto):
- return wireproto.streamres(proto.groupchunks(d))
-else:
- # Leave chunking and compression to streamres
- def streamres(d, proto):
- return wireproto.streamres(reader=d, v1compressible=True)
-
-def srv_pullobsmarkers(repo, proto, others):
- opts = wireproto.options('', ['heads', 'common'], others)
- for k, v in opts.iteritems():
- if k in ('heads', 'common'):
- opts[k] = wireproto.decodelist(v)
- obsdata = _getobsmarkersstream(repo, **opts)
- finaldata = StringIO()
- obsdata = obsdata.getvalue()
- finaldata.write('%20i' % len(obsdata))
- finaldata.write(obsdata)
- finaldata.seek(0)
- return streamres(finaldata, proto)
-
-def _obsrelsethashtreefm0(repo):
- return _obsrelsethashtree(repo, obsolete._fm0encodeonemarker)
-
-def _obsrelsethashtreefm1(repo):
- return _obsrelsethashtree(repo, obsolete._fm1encodeonemarker)
-
-def _obsrelsethashtree(repo, encodeonemarker):
- cache = []
- unfi = repo.unfiltered()
- markercache = {}
- repo.ui.progress(_("preparing locally"), 0, total=len(unfi))
- for i in unfi:
- ctx = unfi[i]
- entry = 0
- sha = hashlib.sha1()
- # add data from p1
- for p in ctx.parents():
- p = p.rev()
- if p < 0:
- p = nullid
- else:
- p = cache[p][1]
- if p != nullid:
- entry += 1
- sha.update(p)
- tmarkers = repo.obsstore.relevantmarkers([ctx.node()])
- if tmarkers:
- bmarkers = []
- for m in tmarkers:
- if not m in markercache:
- markercache[m] = encodeonemarker(m)
- bmarkers.append(markercache[m])
- bmarkers.sort()
- for m in bmarkers:
- entry += 1
- sha.update(m)
- if entry:
- cache.append((ctx.node(), sha.digest()))
- else:
- cache.append((ctx.node(), nullid))
- repo.ui.progress(_("preparing locally"), i, total=len(unfi))
- repo.ui.progress(_("preparing locally"), None)
- return cache
-
-@command('debugobsrelsethashtree',
- [('', 'v0', None, 'hash on marker format "0"'),
- ('', 'v1', None, 'hash on marker format "1" (default)')] , _(''))
-def debugobsrelsethashtree(ui, repo, v0=False, v1=False):
- """display Obsolete markers, Relevant Set, Hash Tree
- changeset-node obsrelsethashtree-node
-
- It computed form the "orsht" of its parent and markers
- relevant to the changeset itself."""
- if v0 and v1:
- raise error.Abort('cannot only specify one format')
- elif v0:
- treefunc = _obsrelsethashtreefm0
- else:
- treefunc = _obsrelsethashtreefm1
-
- for chg, obs in treefunc(repo):
- ui.status('%s %s\n' % (node.hex(chg), node.hex(obs)))
-
-_bestformat = max(obsolete.formats.keys())
-
-
-@eh.wrapfunction(obsolete, '_checkinvalidmarkers')
-def _checkinvalidmarkers(orig, markers):
- """search for marker with invalid data and raise error if needed
-
- Exist as a separated function to allow the evolve extension for a more
- subtle handling.
- """
- if 'debugobsconvert' in sys.argv:
- return
- for mark in markers:
- if node.nullid in mark[1]:
- raise error.Abort(_('bad obsolescence marker detected: '
- 'invalid successors nullid'),
- hint=_('You should run `hg debugobsconvert`'))
-
-@command(
- 'debugobsconvert',
- [('', 'new-format', _bestformat, _('Destination format for markers.'))],
- '')
-def debugobsconvert(ui, repo, new_format):
- origmarkers = repo.obsstore._all # settle version
- if new_format == repo.obsstore._version:
- msg = _('New format is the same as the old format, not upgrading!')
- raise error.Abort(msg)
- f = repo.svfs('obsstore', 'wb', atomictemp=True)
- known = set()
- markers = []
- for m in origmarkers:
- # filter out invalid markers
- if nullid in m[1]:
- m = list(m)
- m[1] = tuple(s for s in m[1] if s != nullid)
- m = tuple(m)
- if m in known:
- continue
- known.add(m)
- markers.append(m)
- ui.write(_('Old store is version %d, will rewrite in version %d\n') % (
- repo.obsstore._version, new_format))
- map(f.write, obsolete.encodemarkers(markers, True, new_format))
- f.close()
- ui.write(_('Done!\n'))
-
-
-@eh.wrapfunction(wireproto, 'capabilities')
-def capabilities(orig, repo, proto):
- """wrapper to advertise new capability"""
- caps = orig(repo, proto)
- if obsolete.isenabled(repo, obsolete.exchangeopt):
- caps += ' _evoext_pushobsmarkers_0'
- caps += ' _evoext_pullobsmarkers_0'
- caps += ' _evoext_obshash_0'
- caps += ' _evoext_obshash_1'
- caps += ' _evoext_getbundle_obscommon'
- return caps
-
-
-@eh.extsetup
-def _installwireprotocol(ui):
- localrepo.moderncaps.add('_evoext_pullobsmarkers_0')
- hgweb_mod.perms['evoext_pushobsmarkers_0'] = 'push'
- hgweb_mod.perms['evoext_pullobsmarkers_0'] = 'pull'
- wireproto.commands['evoext_pushobsmarkers_0'] = (srv_pushobsmarkers, '')
- wireproto.commands['evoext_pullobsmarkers_0'] = (srv_pullobsmarkers, '*')
- # wrap command content
- oldcap, args = wireproto.commands['capabilities']
- def newcap(repo, proto):
- return capabilities(oldcap, repo, proto)
- wireproto.commands['capabilities'] = (newcap, args)
-
-# Mercurial >= 3.6 passes ui
-def _helploader(ui=None):
- return help.gettext(evolutionhelptext)
-
-@eh.uisetup
-def _setuphelp(ui):
- for entry in help.helptable:
- if entry[0] == "evolution":
- break
- else:
- help.helptable.append((["evolution"], _("Safely Rewriting History"),
- _helploader))
- help.helptable.sort()
-
-def _relocatecommit(repo, orig, commitmsg):
- if commitmsg is None:
- commitmsg = orig.description()
- extra = dict(orig.extra())
- if 'branch' in extra:
- del extra['branch']
- extra['rebase_source'] = orig.hex()
-
- backup = repo.ui.backupconfig('phases', 'new-commit')
- try:
- targetphase = max(orig.phase(), phases.draft)
- repo.ui.setconfig('phases', 'new-commit', targetphase, 'evolve')
- # Commit might fail if unresolved files exist
- nodenew = repo.commit(text=commitmsg, user=orig.user(),
- date=orig.date(), extra=extra)
- finally:
- repo.ui.restoreconfig(backup)
- return nodenew
-
-def _finalizerelocate(repo, orig, dest, nodenew, tr):
- destbookmarks = repo.nodebookmarks(dest.node())
- nodesrc = orig.node()
- destphase = repo[nodesrc].phase()
- oldbookmarks = repo.nodebookmarks(nodesrc)
- if nodenew is not None:
- phases.retractboundary(repo, tr, destphase, [nodenew])
- obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))])
- for book in oldbookmarks:
- repo._bookmarks[book] = nodenew
- else:
- obsolete.createmarkers(repo, [(repo[nodesrc], ())])
- # Behave like rebase, move bookmarks to dest
- for book in oldbookmarks:
- repo._bookmarks[book] = dest.node()
- for book in destbookmarks: # restore bookmark that rebase move
- repo._bookmarks[book] = dest.node()
- if oldbookmarks or destbookmarks:
- repo._bookmarks.recordchange(tr)
-
-evolvestateversion = 0
-
-@eh.uisetup
-def setupevolveunfinished(ui):
- data = ('evolvestate', True, False, _('evolve in progress'),
- _("use 'hg evolve --continue' or 'hg update -C .' to abort"))
- cmdutil.unfinishedstates.append(data)
-
-@eh.wrapfunction(hg, 'clean')
-def clean(orig, repo, *args, **kwargs):
- ret = orig(repo, *args, **kwargs)
- util.unlinkpath(repo.join('evolvestate'), ignoremissing=True)
- return ret
-
-def _evolvestatewrite(repo, state):
- # [version]
- # [type][length][content]
- #
- # `version` is a 4 bytes integer (handled at higher level)
- # `type` is a single character, `length` is a 4 byte integer, and
- # `content` is an arbitrary byte sequence of length `length`.
- f = repo.vfs('evolvestate', 'w')
- try:
- f.write(_pack('>I', evolvestateversion))
- current = state['current']
- key = 'C' # as in 'current'
- format = '>sI%is' % len(current)
- f.write(_pack(format, key, len(current), current))
- finally:
- f.close()
-
-def _evolvestateread(repo):
- try:
- f = repo.vfs('evolvestate')
- except IOError as err:
- if err.errno != errno.ENOENT:
- raise
- return None
- try:
- versionblob = f.read(4)
- if len(versionblob) < 4:
- repo.ui.debug('ignoring corrupted evolvestte (file contains %i bits)'
- % len(versionblob))
- return None
- version = _unpack('>I', versionblob)[0]
- if version != evolvestateversion:
- raise error.Abort(_('unknown evolvestate version %i')
- % version, hint=_('upgrade your evolve'))
- records = []
- data = f.read()
- off = 0
- end = len(data)
- while off < end:
- rtype = data[off]
- off += 1
- length = _unpack('>I', data[off:(off + 4)])[0]
- off += 4
- record = data[off:(off + length)]
- off += length
- if rtype == 't':
- rtype, record = record[0], record[1:]
- records.append((rtype, record))
- state = {}
- for rtype, rdata in records:
- if rtype == 'C':
- state['current'] = rdata
- elif rtype.lower():
- repo.ui.debug('ignore evolve state record type %s' % rtype)
- else:
- raise error.Abort(_('unknown evolvestate field type %r')
- % rtype, hint=_('upgrade your evolve'))
- return state
- finally:
- f.close()
-
-def _evolvestatedelete(repo):
- util.unlinkpath(repo.join('evolvestate'), ignoremissing=True)
-
-def _evolvemerge(repo, orig, dest, pctx, keepbranch):
- """Used by the evolve function to merge dest on top of pctx.
- return the same tuple as merge.graft"""
- if repo['.'].rev() != dest.rev():
- #assert False
- try:
- merge.update(repo,
- dest,
- branchmerge=False,
- force=True)
- except TypeError:
- # Mercurial < 43c00ca887d1 (3.7)
- merge.update(repo,
- dest,
- branchmerge=False,
- force=True,
- partial=False)
- if bmactive(repo):
- repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo))
- bmdeactivate(repo)
- if keepbranch:
- repo.dirstate.setbranch(orig.branch())
- if util.safehasattr(repo, 'currenttopic'):
- # uurrgs
- # there no other topic setter yet
- if not orig.topic() and repo.vfs.exists('topic'):
- repo.vfs.unlink('topic')
- else:
- with repo.vfs.open('topic', 'w') as f:
- f.write(orig.topic())
-
- try:
- r = merge.graft(repo, orig, pctx, ['local', 'graft'], True)
- except TypeError:
- # not using recent enough mercurial
- if len(orig.parents()) == 2:
- raise error.Abort(
- _("no support for evolving merge changesets yet"),
- hint=_("Redo the merge and use `hg prune <old> --succ "
- "<new>` to obsolete the old one"))
-
- r = merge.graft(repo, orig, pctx, ['local', 'graft'])
- return r
--- a/hgext/inhibit.py Tue Feb 28 17:22:21 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,310 +0,0 @@
-"""reduce the changesets evolution feature scope for early and noob friendly ui
-
-the full scale changeset evolution have some massive bleeding edge and it is
-very easy for people not very intimate with the concept to end up in intricate
-situation. in order to get some of the benefit sooner, this extension is
-disabling some of the less polished aspect of evolution. it should gradually
-get thinner and thinner as changeset evolution will get more polished. this
-extension is only recommended for large scale organisations. individual user
-should probably stick on using evolution in its current state, understand its
-concept and provide feedback
-
-This extension provides the ability to "inhibit" obsolescence markers. obsolete
-revision can be cheaply brought back to life that way.
-However as the inhibitor are not fitting in an append only model, this is
-incompatible with sharing mutable history.
-"""
-from mercurial import localrepo
-from mercurial import obsolete
-from mercurial import extensions
-from mercurial import cmdutil
-from mercurial import error
-from mercurial import scmutil
-from mercurial import commands
-from mercurial import lock as lockmod
-from mercurial import bookmarks
-from mercurial import util
-from mercurial.i18n import _
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
-def _inhibitenabled(repo):
- return util.safehasattr(repo, '_obsinhibit')
-
-def reposetup(ui, repo):
-
- class obsinhibitedrepo(repo.__class__):
-
- @localrepo.storecache('obsinhibit')
- def _obsinhibit(self):
- # XXX we should make sure it is invalidated by transaction failure
- obsinhibit = set()
- raw = self.svfs.tryread('obsinhibit')
- for i in xrange(0, len(raw), 20):
- obsinhibit.add(raw[i:i + 20])
- return obsinhibit
-
- def commit(self, *args, **kwargs):
- newnode = super(obsinhibitedrepo, self).commit(*args, **kwargs)
- if newnode is not None:
- _inhibitmarkers(repo, [newnode])
- return newnode
-
- repo.__class__ = obsinhibitedrepo
-
-def _update(orig, ui, repo, *args, **kwargs):
- """
- When moving to a commit we want to inhibit any obsolete commit affecting
- the changeset we are updating to. In other words we don't want any visible
- commit to be obsolete.
- """
- wlock = None
- try:
- # Evolve is running a hook on lock release to display a warning message
- # if the workind dir's parent is obsolete.
- # We take the lock here to make sure that we inhibit the parent before
- # that hook get a chance to run.
- wlock = repo.wlock()
- res = orig(ui, repo, *args, **kwargs)
- newhead = repo['.'].node()
- _inhibitmarkers(repo, [newhead])
- return res
- finally:
- lockmod.release(wlock)
-
-def _bookmarkchanged(orig, bkmstoreinst, *args, **kwargs):
- """ Add inhibition markers to every obsolete bookmarks """
- repo = bkmstoreinst._repo
- bkmstorenodes = [repo[v].node() for v in bkmstoreinst.values()]
- _inhibitmarkers(repo, bkmstorenodes)
- return orig(bkmstoreinst, *args, **kwargs)
-
-def _bookmark(orig, ui, repo, *bookmarks, **opts):
- """ Add a -D option to the bookmark command, map it to prune -B """
- haspruneopt = opts.get('prune', False)
- if not haspruneopt:
- return orig(ui, repo, *bookmarks, **opts)
- elif opts.get('rename'):
- raise error.Abort('Cannot use both -m and -D')
- elif len(bookmarks) == 0:
- hint = _('make sure to put a space between -D and your bookmark name')
- raise error.Abort(_('Error, please check your command'), hint=hint)
-
- # Call prune -B
- evolve = extensions.find('evolve')
- optsdict = {
- 'new': [],
- 'succ': [],
- 'rev': [],
- 'bookmark': bookmarks,
- 'keep': None,
- 'biject': False,
- }
- evolve.cmdprune(ui, repo, **optsdict)
-
-# obsolescence inhibitor
-########################
-
-def _schedulewrite(tr, obsinhibit):
- """Make sure on disk content will be updated on transaction commit"""
- def writer(fp):
- """Serialize the inhibited list to disk.
- """
- raw = ''.join(obsinhibit)
- fp.write(raw)
- tr.addfilegenerator('obsinhibit', ('obsinhibit',), writer)
- tr.hookargs['obs_inbihited'] = '1'
-
-def _filterpublic(repo, nodes):
- """filter out inhibitor on public changeset
-
- Public changesets are already immune to obsolescence"""
- getrev = repo.changelog.nodemap.get
- getphase = repo._phasecache.phase
- return (n for n in nodes
- if getrev(n) is not None and getphase(repo, getrev(n)))
-
-def _inhibitmarkers(repo, nodes):
- """add marker inhibitor for all obsolete revision under <nodes>
-
- Content of <nodes> and all mutable ancestors are considered. Marker for
- obsolete revision only are created.
- """
- if not _inhibitenabled(repo):
- return
-
- # we add (non public()) as a lower boundary to
- # - use the C code in 3.6 (no ancestors in C as this is written)
- # - restrict the search space. Otherwise, the ancestors can spend a lot of
- # time iterating if you have a check very low in the repo. We do not need
- # to iterate over tens of thousand of public revisions with higher
- # revision number
- #
- # In addition, the revset logic could be made significantly smarter here.
- newinhibit = repo.revs('(not public())::%ln and obsolete()', nodes)
- if newinhibit:
- node = repo.changelog.node
- lock = tr = None
- try:
- lock = repo.lock()
- tr = repo.transaction('obsinhibit')
- repo._obsinhibit.update(node(r) for r in newinhibit)
- _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
- repo.invalidatevolatilesets()
- tr.close()
- finally:
- lockmod.release(tr, lock)
-
-def _deinhibitmarkers(repo, nodes):
- """lift obsolescence inhibition on a set of nodes
-
- This will be triggered when inhibited nodes received new obsolescence
- markers. Otherwise the new obsolescence markers would also be inhibited.
- """
- if not _inhibitenabled(repo):
- return
-
- deinhibited = repo._obsinhibit & set(nodes)
- if deinhibited:
- tr = repo.transaction('obsinhibit')
- try:
- repo._obsinhibit -= deinhibited
- _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
- repo.invalidatevolatilesets()
- tr.close()
- finally:
- tr.release()
-
-def _createmarkers(orig, repo, relations, flag=0, date=None, metadata=None):
- """wrap markers create to make sure we de-inhibit target nodes"""
- # wrapping transactio to unify the one in each function
- lock = tr = None
- try:
- lock = repo.lock()
- tr = repo.transaction('add-obsolescence-marker')
- orig(repo, relations, flag, date, metadata)
- precs = (r[0].node() for r in relations)
- _deinhibitmarkers(repo, precs)
- tr.close()
- finally:
- lockmod.release(tr, lock)
-
-def _filterobsoleterevswrap(orig, repo, rebasesetrevs, *args, **kwargs):
- repo._notinhibited = rebasesetrevs
- try:
- repo.invalidatevolatilesets()
- r = orig(repo, rebasesetrevs, *args, **kwargs)
- finally:
- del repo._notinhibited
- repo.invalidatevolatilesets()
- return r
-
-def transactioncallback(orig, repo, desc, *args, **kwargs):
- """ Wrap localrepo.transaction to inhibit new obsolete changes """
- def inhibitposttransaction(transaction):
- # At the end of the transaction we catch all the new visible and
- # obsolete commit to inhibit them
- visibleobsolete = repo.revs('obsolete() - hidden()')
- ignoreset = set(getattr(repo, '_rebaseset', []))
- ignoreset |= set(getattr(repo, '_obsoletenotrebased', []))
- visibleobsolete = list(r for r in visibleobsolete if r not in ignoreset)
- if visibleobsolete:
- _inhibitmarkers(repo, [repo[r].node() for r in visibleobsolete])
- transaction = orig(repo, desc, *args, **kwargs)
- if desc != 'strip' and _inhibitenabled(repo):
- transaction.addpostclose('inhibitposttransaction',
- inhibitposttransaction)
- return transaction
-
-
-# We wrap these two functions to address the following scenario:
-# - Assuming that we have markers between commits in the rebase set and
-# destination and that these markers are inhibited
-# - At the end of the rebase the nodes are still visible because rebase operate
-# without inhibition and skip these nodes
-# We keep track in repo._obsoletenotrebased of the obsolete commits skipped by
-# the rebase and lift the inhibition in the end of the rebase.
-
-def _computeobsoletenotrebased(orig, repo, *args, **kwargs):
- r = orig(repo, *args, **kwargs)
- repo._obsoletenotrebased = r.keys()
- return r
-
-def _clearrebased(orig, ui, repo, *args, **kwargs):
- r = orig(ui, repo, *args, **kwargs)
- tonode = repo.changelog.node
- if util.safehasattr(repo, '_obsoletenotrebased'):
- _deinhibitmarkers(repo, [tonode(k) for k in repo._obsoletenotrebased])
- return r
-
-
-def extsetup(ui):
- # lets wrap the computation of the obsolete set
- # We apply inhibition there
- obsfunc = obsolete.cachefuncs['obsolete']
- def _computeobsoleteset(repo):
- """remove any inhibited nodes from the obsolete set
-
- This will trickle down to other part of mercurial (hidden, log, etc)"""
- obs = obsfunc(repo)
- if _inhibitenabled(repo):
- getrev = repo.changelog.nodemap.get
- blacklist = getattr(repo, '_notinhibited', set())
- for n in repo._obsinhibit:
- if getrev(n) not in blacklist:
- obs.discard(getrev(n))
- return obs
- try:
- extensions.find('directaccess')
- except KeyError:
- errormsg = _('cannot use inhibit without the direct access extension\n')
- hint = _("(please enable it or inhibit won\'t work)\n")
- ui.warn(errormsg)
- ui.warn(hint)
- return
-
- # Wrapping this to inhibit obsolete revs resulting from a transaction
- extensions.wrapfunction(localrepo.localrepository,
- 'transaction', transactioncallback)
-
- obsolete.cachefuncs['obsolete'] = _computeobsoleteset
- # wrap create marker to make it able to lift the inhibition
- extensions.wrapfunction(obsolete, 'createmarkers', _createmarkers)
- # drop divergence computation since it is incompatible with "light revive"
- obsolete.cachefuncs['divergent'] = lambda repo: set()
- # drop bumped computation since it is incompatible with "light revive"
- obsolete.cachefuncs['bumped'] = lambda repo: set()
- # wrap update to make sure that no obsolete commit is visible after an
- # update
- extensions.wrapcommand(commands.table, 'update', _update)
- try:
- rebase = extensions.find('rebase')
- if rebase:
- if util.safehasattr(rebase, '_filterobsoleterevs'):
- extensions.wrapfunction(rebase,
- '_filterobsoleterevs',
- _filterobsoleterevswrap)
- extensions.wrapfunction(rebase, 'clearrebased', _clearrebased)
- if util.safehasattr(rebase, '_computeobsoletenotrebased'):
- extensions.wrapfunction(rebase,
- '_computeobsoletenotrebased',
- _computeobsoletenotrebased)
-
- except KeyError:
- pass
- # There are two ways to save bookmark changes during a transation, we
- # wrap both to add inhibition markers.
- extensions.wrapfunction(bookmarks.bmstore, 'recordchange', _bookmarkchanged)
- if getattr(bookmarks.bmstore, 'write', None) is not None:# mercurial < 3.9
- extensions.wrapfunction(bookmarks.bmstore, 'write', _bookmarkchanged)
- # Add bookmark -D option
- entry = extensions.wrapcommand(commands.table, 'bookmark', _bookmark)
- entry[1].append(('D','prune',None,
- _('delete the bookmark and prune the commits underneath')))
-
-@command('debugobsinhibit', [], '')
-def cmddebugobsinhibit(ui, repo, *revs):
- """inhibit obsolescence markers effect on a set of revs"""
- nodes = (repo[r].node() for r in scmutil.revrange(repo, revs))
- _inhibitmarkers(repo, nodes)
--- a/hgext/obsolete.py Tue Feb 28 17:22:21 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-# 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.
-"""Deprecated extension that formerly introduced "Changeset Obsolescence".
-
-This concept is now partially in Mercurial core (starting with Mercurial 2.3).
-The remaining logic has been grouped with the evolve extension.
-
-Some code remains in this extensions to detect and convert prehistoric format
-of obsolete marker than early user may have create. Keep it enabled if you
-were such user.
-"""
-
-from mercurial import error
-
-try:
- from mercurial import obsolete
-except ImportError:
- raise error.Abort('Obsolete extension requires Mercurial 2.3 (or later)')
-
-import sys
-import json
-
-from mercurial import cmdutil
-from mercurial.i18n import _
-from mercurial.node import bin, nullid
-from mercurial import util
-
-
-#####################################################################
-### Older format management ###
-#####################################################################
-
-# Code related to detection and management of older legacy format never
-# handled by core
-
-
-def reposetup(ui, repo):
- """Detect that a repo still contains some old obsolete format
- """
- if not repo.local():
- return
- evolveopts = ui.configlist('experimental', 'evolution')
- if not evolveopts:
- evolveopts = 'all'
- ui.setconfig('experimental', 'evolution', evolveopts)
- for arg in sys.argv:
- if 'debugc' in arg:
- break
- else:
- data = repo.opener.tryread('obsolete-relations')
- if not data:
- data = repo.svfs.tryread('obsoletemarkers')
- if data:
- raise error.Abort('old format of obsolete marker detected!\n'
- 'run `hg debugconvertobsolete` once.')
-
-def _obsdeserialize(flike):
- """read a file like object serialized with _obsserialize
-
- this deserialize into a {subject -> objects} mapping
-
- this was the very first format ever."""
- rels = {}
- for line in flike:
- subhex, objhex = line.split()
- subnode = bin(subhex)
- if subnode == nullid:
- subnode = None
- rels.setdefault(subnode, set()).add(bin(objhex))
- return rels
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-@command('debugconvertobsolete', [], '')
-def cmddebugconvertobsolete(ui, repo):
- """import markers from an .hg/obsolete-relations file"""
- cnt = 0
- err = 0
- l = repo.lock()
- some = False
- try:
- unlink = []
- tr = repo.transaction('convert-obsolete')
- try:
- repo._importoldobsolete = True
- store = repo.obsstore
- ### very first format
- try:
- f = repo.opener('obsolete-relations')
- try:
- some = True
- for line in f:
- subhex, objhex = line.split()
- suc = bin(subhex)
- prec = bin(objhex)
- sucs = (suc==nullid) and [] or [suc]
- meta = {
- 'date': '%i %i' % util.makedate(),
- 'user': ui.username(),
- }
- try:
- store.create(tr, prec, sucs, 0, metadata=meta)
- cnt += 1
- except ValueError:
- repo.ui.write_err("invalid old marker line: %s"
- % (line))
- err += 1
- finally:
- f.close()
- unlink.append(repo.join('obsolete-relations'))
- except IOError:
- pass
- ### second (json) format
- data = repo.svfs.tryread('obsoletemarkers')
- if data:
- some = True
- for oldmark in json.loads(data):
- del oldmark['id'] # dropped for now
- del oldmark['reason'] # unused until then
- oldobject = str(oldmark.pop('object'))
- oldsubjects = [str(s) for s in oldmark.pop('subjects', [])]
- LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError)
- if len(oldobject) != 40:
- try:
- oldobject = repo[oldobject].node()
- except LOOKUP_ERRORS:
- pass
- if any(len(s) != 40 for s in oldsubjects):
- try:
- oldsubjects = [repo[s].node() for s in oldsubjects]
- except LOOKUP_ERRORS:
- pass
-
- oldmark['date'] = '%i %i' % tuple(oldmark['date'])
- meta = dict((k.encode('utf-8'), v.encode('utf-8'))
- for k, v in oldmark.iteritems())
- try:
- succs = [bin(n) for n in oldsubjects]
- succs = [n for n in succs if n != nullid]
- store.create(tr, bin(oldobject), succs,
- 0, metadata=meta)
- cnt += 1
- except ValueError:
- repo.ui.write_err("invalid marker %s -> %s\n"
- % (oldobject, oldsubjects))
- err += 1
- unlink.append(repo.sjoin('obsoletemarkers'))
- tr.close()
- for path in unlink:
- util.unlink(path)
- finally:
- tr.release()
- finally:
- del repo._importoldobsolete
- l.release()
- if not some:
- ui.warn(_('nothing to do\n'))
- ui.status('%i obsolete marker converted\n' % cnt)
- if err:
- ui.write_err('%i conversion failed. check you graph!\n' % err)
--- a/hgext/pushexperiment.py Tue Feb 28 17:22:21 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-"""Small extension altering some push behavior
-
-- Add a new wire protocol command to exchange obsolescence markers. Sending the
- raw file as a binary instead of using pushkey hack.
-- Add a "push done" notification
-- Push obsolescence marker before anything else (This works around the lack
-of global transaction)
-
-"""
-
-import errno
-from StringIO import StringIO
-
-from mercurial.i18n import _
-from mercurial import error
-from mercurial import extensions
-from mercurial import wireproto
-from mercurial import obsolete
-from mercurial import localrepo
-
-
-def client_pushobsmarkers(self, obsfile):
- """wireprotocol peer method"""
- self.requirecap('_push_experiment_pushobsmarkers_0',
- _('push obsolete markers faster'))
- ret, output = self._callpush('push_experiment_pushobsmarkers_0', obsfile)
- for l in output.splitlines(True):
- self.ui.status(_('remote: '), l)
- return ret
-
-
-def srv_pushobsmarkers(repo, proto):
- """wireprotocol command"""
- fp = StringIO()
- proto.redirect()
- proto.getfile(fp)
- data = fp.getvalue()
- fp.close()
- lock = repo.lock()
- try:
- tr = repo.transaction('pushkey: obsolete markers')
- try:
- repo.obsstore.mergemarkers(tr, data)
- tr.close()
- finally:
- tr.release()
- finally:
- lock.release()
- return wireproto.pushres(0)
-
-
-def syncpush(orig, repo, remote):
- """wraper for obsolete.syncpush to use the fast way if possible"""
- if not (obsolete.isenabled(repo, obsolete.exchangeopt) and
- repo.obsstore):
- return
- if remote.capable('_push_experiment_pushobsmarkers_0'):
- return # already pushed before changeset
- remote.push_experiment_pushobsmarkers_0(obsfp)
- return
- return orig(repo, remote)
-
-
-def client_notifypushend(self):
- """wire peer command to notify a push is done"""
- self.requirecap('_push_experiment_notifypushend_0',
- _('hook once push is all done'))
- return self._call('push_experiment_notifypushend_0')
-
-
-def srv_notifypushend(repo, proto):
- """wire protocol command to notify a push is done"""
- proto.redirect()
- repo.hook('notifypushend')
- return wireproto.pushres(0)
-
-
-def augmented_push(orig, repo, remote, *args, **kwargs):
- """push wrapped that call the wire protocol command"""
- if not remote.canpush():
- raise error.Abort(_("destination does not support push"))
- if (obsolete.isenabled(repo, obsolete.exchangeopt) and repo.obsstore
- and remote.capable('_push_experiment_pushobsmarkers_0')):
- # push marker early to limit damage of pushing too early.
- try:
- obsfp = repo.svfs('obsstore')
- except IOError as e:
- if e.errno != errno.ENOENT:
- raise
- else:
- remote.push_experiment_pushobsmarkers_0(obsfp)
- ret = orig(repo, remote, *args, **kwargs)
- if remote.capable('_push_experiment_notifypushend_0'):
- remote.push_experiment_notifypushend_0()
- return ret
-
-
-def capabilities(orig, repo, proto):
- """wrapper to advertise new capability"""
- caps = orig(repo, proto)
- if obsolete.isenabled(repo, obsolete.exchangeopt):
- caps += ' _push_experiment_pushobsmarkers_0'
- caps += ' _push_experiment_notifypushend_0'
- return caps
-
-
-def extsetup(ui):
- wireproto.wirepeer.push_experiment_pushobsmarkers_0 = client_pushobsmarkers
- wireproto.wirepeer.push_experiment_notifypushend_0 = client_notifypushend
- wireproto.commands['push_experiment_pushobsmarkers_0'] = \
- (srv_pushobsmarkers, '')
- wireproto.commands['push_experiment_notifypushend_0'] = \
- (srv_notifypushend, '')
- extensions.wrapfunction(wireproto, 'capabilities', capabilities)
- extensions.wrapfunction(obsolete, 'syncpush', syncpush)
- extensions.wrapfunction(localrepo.localrepository, 'push', augmented_push)
-
-
--- a/hgext/simple4server.py Tue Feb 28 17:22:21 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,321 +0,0 @@
-'''enable experimental obsolescence feature of Mercurial
-
-OBSOLESCENCE IS AN EXPERIMENTAL FEATURE MAKE SURE YOU UNDERSTOOD THE INVOLVED
-CONCEPT BEFORE USING IT.
-
-/!\ THIS EXTENSION IS INTENDED FOR SERVER SIDE ONLY USAGE /!\
-
-For client side usages it is recommended to use the evolve extension for
-improved user interface.'''
-
-testedwith = '3.3 3.4-rc'
-buglink = 'https://bz.mercurial-scm.org/'
-
-import mercurial.obsolete
-
-import hashlib
-import struct
-from mercurial import error
-from mercurial import util
-from mercurial import wireproto
-from mercurial import extensions
-from mercurial import obsolete
-from cStringIO import StringIO
-from mercurial import node
-from mercurial.hgweb import hgweb_mod
-from mercurial import bundle2
-from mercurial import localrepo
-from mercurial import exchange
-from mercurial import node
-_pack = struct.pack
-
-gboptslist = gboptsmap = None
-try:
- from mercurial import obsolete
- from mercurial import wireproto
- gboptslist = getattr(wireproto, 'gboptslist', None)
- gboptsmap = getattr(wireproto, 'gboptsmap', None)
-except (ImportError, AttributeError):
- raise error.Abort('Your Mercurial is too old for this version of Evolve\n'
- 'requires version 3.0.1 or above')
-
-# Start of simple4server specific content
-
-from mercurial import pushkey
-
-# specific content also include the wrapping int extsetup
-def _nslist(orig, repo):
- rep = orig(repo)
- if not repo.ui.configbool('__temporary__', 'advertiseobsolete', True):
- rep.pop('obsolete')
- return rep
-
-# End of simple4server specific content
-
-
-
-# from evolve extension: 1a23c7c52a43
-def srv_pushobsmarkers(repo, proto):
- """That receives a stream of markers and apply then to the repo"""
- fp = StringIO()
- proto.redirect()
- proto.getfile(fp)
- data = fp.getvalue()
- fp.close()
- lock = repo.lock()
- try:
- tr = repo.transaction('pushkey: obsolete markers')
- try:
- repo.obsstore.mergemarkers(tr, data)
- tr.close()
- finally:
- tr.release()
- finally:
- lock.release()
- repo.hook('evolve_pushobsmarkers')
- return wireproto.pushres(0)
-
-# from evolve extension: 1a23c7c52a43
-def _getobsmarkersstream(repo, heads=None, common=None):
- """Get a binary stream for all markers relevant to `::<heads> - ::<common>`
- """
- revset = ''
- args = []
- repo = repo.unfiltered()
- if heads is None:
- revset = 'all()'
- elif heads:
- revset += "(::%ln)"
- args.append(heads)
- else:
- assert False, 'pulling no heads?'
- if common:
- revset += ' - (::%ln)'
- args.append(common)
- nodes = [c.node() for c in repo.set(revset, *args)]
- markers = repo.obsstore.relevantmarkers(nodes)
- obsdata = StringIO()
- for chunk in obsolete.encodemarkers(markers, True):
- obsdata.write(chunk)
- obsdata.seek(0)
- return obsdata
-
-if not util.safehasattr(obsolete.obsstore, 'relevantmarkers'):
- # from evolve extension: 1a23c7c52a43
- class pruneobsstore(obsolete.obsstore):
- """And extended obsstore class that read parent information from v1
- format
-
- Evolve extension adds parent information in prune marker.
- We use it to make markers relevant to pushed changeset."""
-
- def __init__(self, *args, **kwargs):
- self.prunedchildren = {}
- return super(pruneobsstore, self).__init__(*args, **kwargs)
-
- def _load(self, markers):
- markers = self._prunedetectingmarkers(markers)
- return super(pruneobsstore, self)._load(markers)
-
-
- def _prunedetectingmarkers(self, markers):
- for m in markers:
- if not m[1]: # no successors
- meta = obsolete.decodemeta(m[3])
- if 'p1' in meta:
- p1 = node.bin(meta['p1'])
- self.prunedchildren.setdefault(p1, set()).add(m)
- if 'p2' in meta:
- p2 = node.bin(meta['p2'])
- self.prunedchildren.setdefault(p2, set()).add(m)
- yield m
-
- # from evolve extension: 1a23c7c52a43
- def relevantmarkers(self, nodes):
- """return a set of all obsolescence marker relevant to a set of node.
-
- "relevant" to a set of node mean:
-
- - marker that use this changeset as successors
- - prune marker of direct children on this changeset.
- - recursive application of the two rules on precursors of these markers
-
- It is a set so you cannot rely on order"""
- seennodes = set(nodes)
- seenmarkers = set()
- pendingnodes = set(nodes)
- precursorsmarkers = self.precursors
- prunedchildren = self.prunedchildren
- while pendingnodes:
- direct = set()
- for current in pendingnodes:
- direct.update(precursorsmarkers.get(current, ()))
- direct.update(prunedchildren.get(current, ()))
- direct -= seenmarkers
- pendingnodes = set([m[0] for m in direct])
- seenmarkers |= direct
- pendingnodes -= seennodes
- seennodes |= pendingnodes
- return seenmarkers
-
-# The wireproto.streamres API changed, handling chunking and compression
-# directly. Handle either case.
-if util.safehasattr(wireproto.abstractserverproto, 'groupchunks'):
- # We need to handle chunking and compression directly
- def streamres(d, proto):
- return wireproto.streamres(proto.groupchunks(d))
-else:
- # Leave chunking and compression to streamres
- def streamres(d, proto):
- return wireproto.streamres(reader=d, v1compressible=True)
-
-# from evolve extension: cf35f38d6a10
-def srv_pullobsmarkers(repo, proto, others):
- """serves a binary stream of markers.
-
- Serves relevant to changeset between heads and common. The stream is prefix
- by a -string- representation of an integer. This integer is the size of the
- stream."""
- opts = wireproto.options('', ['heads', 'common'], others)
- for k, v in opts.iteritems():
- if k in ('heads', 'common'):
- opts[k] = wireproto.decodelist(v)
- obsdata = _getobsmarkersstream(repo, **opts)
- finaldata = StringIO()
- obsdata = obsdata.getvalue()
- finaldata.write('%20i' % len(obsdata))
- finaldata.write(obsdata)
- finaldata.seek(0)
- return streamres(finaldata, proto)
-
-
-# from evolve extension: 3249814dabd1
-def _obsrelsethashtreefm0(repo):
- return _obsrelsethashtree(repo, obsolete._fm0encodeonemarker)
-
-# from evolve extension: 3249814dabd1
-def _obsrelsethashtreefm1(repo):
- return _obsrelsethashtree(repo, obsolete._fm1encodeonemarker)
-
-# from evolve extension: 3249814dabd1
-def _obsrelsethashtree(repo, encodeonemarker):
- cache = []
- unfi = repo.unfiltered()
- markercache = {}
- for i in unfi:
- ctx = unfi[i]
- entry = 0
- sha = hashlib.sha1()
- # add data from p1
- for p in ctx.parents():
- p = p.rev()
- if p < 0:
- p = node.nullid
- else:
- p = cache[p][1]
- if p != node.nullid:
- entry += 1
- sha.update(p)
- tmarkers = repo.obsstore.relevantmarkers([ctx.node()])
- if tmarkers:
- bmarkers = []
- for m in tmarkers:
- if not m in markercache:
- markercache[m] = encodeonemarker(m)
- bmarkers.append(markercache[m])
- bmarkers.sort()
- for m in bmarkers:
- entry += 1
- sha.update(m)
- if entry:
- cache.append((ctx.node(), sha.digest()))
- else:
- cache.append((ctx.node(), node.nullid))
- return cache
-
-# from evolve extension: 3249814dabd1
-def _obshash(repo, nodes, version=0):
- if version == 0:
- hashs = _obsrelsethashtreefm0(repo)
- elif version ==1:
- hashs = _obsrelsethashtreefm1(repo)
- else:
- assert False
- nm = repo.changelog.nodemap
- revs = [nm.get(n) for n in nodes]
- return [r is None and node.nullid or hashs[r][1] for r in revs]
-
-# from evolve extension: 3249814dabd1
-def srv_obshash(repo, proto, nodes):
- return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes)))
-
-# from evolve extension: 3249814dabd1
-def srv_obshash1(repo, proto, nodes):
- return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes),
- version=1))
-
-# from evolve extension: 3249814dabd1
-def capabilities(orig, repo, proto):
- """wrapper to advertise new capability"""
- caps = orig(repo, proto)
- advertise = repo.ui.configbool('__temporary__', 'advertiseobsolete', True)
- if obsolete.isenabled(repo, obsolete.exchangeopt) and advertise:
- caps += ' _evoext_pushobsmarkers_0'
- caps += ' _evoext_pullobsmarkers_0'
- caps += ' _evoext_obshash_0'
- caps += ' _evoext_obshash_1'
- caps += ' _evoext_getbundle_obscommon'
- return caps
-
-def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs):
- if 'evo_obscommon' not in kwargs:
- return orig(bundler, repo, source, **kwargs)
-
- heads = kwargs.get('heads')
- if 'evo_obscommon' not in kwargs:
- return orig(bundler, repo, source, **kwargs)
-
- if kwargs.get('obsmarkers', False):
- if heads is None:
- heads = repo.heads()
- obscommon = kwargs.get('evo_obscommon', ())
- obsset = repo.set('::%ln - ::%ln', heads, obscommon)
- subset = [c.node() for c in obsset]
- markers = repo.obsstore.relevantmarkers(subset)
- exchange.buildobsmarkerspart(bundler, markers)
-
-# from evolve extension: 10867a8e27c6
-# heavily modified
-def extsetup(ui):
- localrepo.moderncaps.add('_evoext_b2x_obsmarkers_0')
- gboptsmap['evo_obscommon'] = 'nodes'
- if not util.safehasattr(obsolete.obsstore, 'relevantmarkers'):
- obsolete.obsstore = pruneobsstore
- obsolete.obsstore.relevantmarkers = relevantmarkers
- hgweb_mod.perms['evoext_pushobsmarkers_0'] = 'push'
- hgweb_mod.perms['evoext_pullobsmarkers_0'] = 'pull'
- hgweb_mod.perms['evoext_obshash'] = 'pull'
- wireproto.commands['evoext_pushobsmarkers_0'] = (srv_pushobsmarkers, '')
- wireproto.commands['evoext_pullobsmarkers_0'] = (srv_pullobsmarkers, '*')
- # wrap module content
- origfunc = exchange.getbundle2partsmapping['obsmarkers']
- def newfunc(*args, **kwargs):
- return _getbundleobsmarkerpart(origfunc, *args, **kwargs)
- exchange.getbundle2partsmapping['obsmarkers'] = newfunc
- extensions.wrapfunction(wireproto, 'capabilities', capabilities)
- # wrap command content
- oldcap, args = wireproto.commands['capabilities']
- def newcap(repo, proto):
- return capabilities(oldcap, repo, proto)
- wireproto.commands['capabilities'] = (newcap, args)
- wireproto.commands['evoext_obshash'] = (srv_obshash, 'nodes')
- wireproto.commands['evoext_obshash1'] = (srv_obshash1, 'nodes')
- # specific simple4server content
- extensions.wrapfunction(pushkey, '_nslist', _nslist)
- pushkey._namespaces['namespaces'] = (lambda *x: False, pushkey._nslist)
-
-def reposetup(ui, repo):
- evolveopts = ui.configlist('experimental', 'evolution')
- if not evolveopts:
- evolveopts = 'all'
- ui.setconfig('experimental', 'evolution', evolveopts)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/__init__.py Tue Feb 28 17:27:44 2017 +0100
@@ -0,0 +1,4 @@
+# name space package to host third party extensions
+from __future__ import absolute_import
+import pkgutil
+__path__ = pkgutil.extend_path(__path__, __name__)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/__init__.py Tue Feb 28 17:27:44 2017 +0100
@@ -0,0 +1,4187 @@
+# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
+# Logilab SA <contact@logilab.fr>
+# Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+# Patrick Mezard <patrick@mezard.eu>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+'''extends Mercurial feature related to Changeset Evolution
+
+This extension provides several commands to mutate history and deal with
+resulting issues.
+
+It also:
+
+ - enables the "Changeset Obsolescence" feature of Mercurial,
+ - alters core commands and extensions that rewrite history to use
+ this feature,
+ - improves some aspect of the early implementation in Mercurial core
+'''
+
+__version__ = '5.6.0'
+testedwith = '3.4.3 3.5.2 3.6.2 3.7.3 3.8.1 3.9 4.0 4.1'
+buglink = 'https://bz.mercurial-scm.org/'
+
+
+evolutionhelptext = """
+Obsolescence markers make it possible to mark changesets that have been
+deleted or superset in a new version of the changeset.
+
+Unlike the previous way of handling such changes, by stripping the old
+changesets from the repository, obsolescence markers can be propagated
+between repositories. This allows for a safe and simple way of exchanging
+mutable history and altering it after the fact. Changeset phases are
+respected, such that only draft and secret changesets can be altered (see
+:hg:`help phases` for details).
+
+Obsolescence is tracked using "obsolete markers", a piece of metadata
+tracking which changesets have been made obsolete, potential successors for
+a given changeset, the moment the changeset was marked as obsolete, and the
+user who performed the rewriting operation. The markers are stored
+separately from standard changeset data can be exchanged without any of the
+precursor changesets, preventing unnecessary exchange of obsolescence data.
+
+The complete set of obsolescence markers describes a history of changeset
+modifications that is orthogonal to the repository history of file
+modifications. This changeset history allows for detection and automatic
+resolution of edge cases arising from multiple users rewriting the same part
+of history concurrently.
+
+Current feature status
+======================
+
+This feature is still in development. If you see this help, you have enabled an
+extension that turned this feature on.
+
+Obsolescence markers will be exchanged between repositories that explicitly
+assert support for the obsolescence feature (this can currently only be done
+via an extension).""".strip()
+
+
+import sys, os
+import random
+try:
+ import StringIO as io
+ StringIO = io.StringIO
+except ImportError:
+ import io
+ StringIO = io.StringIO
+import re
+import collections
+import socket
+import errno
+import hashlib
+import struct
+sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
+
+import mercurial
+from mercurial import util
+from mercurial import repair
+
+try:
+ from mercurial import obsolete
+ if not obsolete._enabled:
+ obsolete._enabled = True
+ from mercurial import wireproto
+ gboptslist = getattr(wireproto, 'gboptslist', None)
+ gboptsmap = getattr(wireproto, 'gboptsmap', None)
+except (ImportError, AttributeError):
+ gboptslist = gboptsmap = None
+
+# Flags for enabling optional parts of evolve
+commandopt = 'allnewcommands'
+
+from mercurial import bookmarks as bookmarksmod
+from mercurial import cmdutil
+from mercurial import commands
+from mercurial import context
+from mercurial import copies
+from mercurial import error
+from mercurial import exchange
+from mercurial import extensions
+from mercurial import help
+from mercurial import httppeer
+from mercurial import hg
+from mercurial import lock as lockmod
+from mercurial import merge
+from mercurial import node
+from mercurial import phases
+from mercurial import patch
+from mercurial import revset
+from mercurial import scmutil
+from mercurial import templatekw
+from mercurial.i18n import _
+from mercurial.commands import walkopts, commitopts, commitopts2, mergetoolopts
+from mercurial.node import nullid
+from mercurial import wireproto
+from mercurial import localrepo
+from mercurial.hgweb import hgweb_mod
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+_pack = struct.pack
+_unpack = struct.unpack
+
+if gboptsmap is not None:
+ memfilectx = context.memfilectx
+elif gboptslist is not None:
+ oldmemfilectx = context.memfilectx
+ def memfilectx(repo, *args, **kwargs):
+ return oldmemfilectx(*args, **kwargs)
+else:
+ raise ImportError('evolve needs version %s or above' %
+ min(testedwith.split()))
+
+aliases, entry = cmdutil.findcmd('commit', commands.table)
+hasinteractivemode = any(['interactive' in e for e in entry[1]])
+if hasinteractivemode:
+ interactiveopt = [['i', 'interactive', None, _('use interactive mode')]]
+else:
+ interactiveopt = []
+# This extension contains the following code
+#
+# - Extension Helper code
+# - Obsolescence cache
+# - ...
+# - Older format compat
+
+
+#####################################################################
+### Extension helper ###
+#####################################################################
+
+class exthelper(object):
+ """Helper for modular extension setup
+
+ A single helper should be instantiated for each extension. Helper
+ methods are then used as decorators for various purpose.
+
+ All decorators return the original function and may be chained.
+ """
+
+ def __init__(self):
+ self._uicallables = []
+ self._extcallables = []
+ self._repocallables = []
+ self._revsetsymbols = []
+ self._templatekws = []
+ self._commandwrappers = []
+ self._extcommandwrappers = []
+ self._functionwrappers = []
+ self._duckpunchers = []
+
+ def final_uisetup(self, ui):
+ """Method to be used as the extension uisetup
+
+ The following operations belong here:
+
+ - Changes to ui.__class__ . The ui object that will be used to run the
+ command has not yet been created. Changes made here will affect ui
+ objects created after this, and in particular the ui that will be
+ passed to runcommand
+ - Command wraps (extensions.wrapcommand)
+ - Changes that need to be visible to other extensions: because
+ initialization occurs in phases (all extensions run uisetup, then all
+ run extsetup), a change made here will be visible to other extensions
+ during extsetup
+ - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch
+ module members
+ - Setup of pre-* and post-* hooks
+ - pushkey setup
+ """
+ for cont, funcname, func in self._duckpunchers:
+ setattr(cont, funcname, func)
+ for command, wrapper, opts in self._commandwrappers:
+ entry = extensions.wrapcommand(commands.table, command, wrapper)
+ if opts:
+ for short, long, val, msg in opts:
+ entry[1].append((short, long, val, msg))
+ for cont, funcname, wrapper in self._functionwrappers:
+ extensions.wrapfunction(cont, funcname, wrapper)
+ for c in self._uicallables:
+ c(ui)
+
+ def final_extsetup(self, ui):
+ """Method to be used as a the extension extsetup
+
+ The following operations belong here:
+
+ - Changes depending on the status of other extensions. (if
+ extensions.find('mq'))
+ - Add a global option to all commands
+ - Register revset functions
+ """
+ knownexts = {}
+ for name, symbol in self._revsetsymbols:
+ revset.symbols[name] = symbol
+ for name, kw in self._templatekws:
+ templatekw.keywords[name] = kw
+ for ext, command, wrapper, opts in self._extcommandwrappers:
+ if ext not in knownexts:
+ try:
+ e = extensions.find(ext)
+ except KeyError:
+ # Extension isn't enabled, so don't bother trying to wrap
+ # it.
+ continue
+ knownexts[ext] = e.cmdtable
+ entry = extensions.wrapcommand(knownexts[ext], command, wrapper)
+ if opts:
+ for short, long, val, msg in opts:
+ entry[1].append((short, long, val, msg))
+
+ for c in self._extcallables:
+ c(ui)
+
+ def final_reposetup(self, ui, repo):
+ """Method to be used as the extension reposetup
+
+ The following operations belong here:
+
+ - All hooks but pre-* and post-*
+ - Modify configuration variables
+ - Changes to repo.__class__, repo.dirstate.__class__
+ """
+ for c in self._repocallables:
+ c(ui, repo)
+
+ def uisetup(self, call):
+ """Decorated function will be executed during uisetup
+
+ example::
+
+ @eh.uisetup
+ def setupbabar(ui):
+ print 'this is uisetup!'
+ """
+ self._uicallables.append(call)
+ return call
+
+ def extsetup(self, call):
+ """Decorated function will be executed during extsetup
+
+ example::
+
+ @eh.extsetup
+ def setupcelestine(ui):
+ print 'this is extsetup!'
+ """
+ self._extcallables.append(call)
+ return call
+
+ def reposetup(self, call):
+ """Decorated function will be executed during reposetup
+
+ example::
+
+ @eh.reposetup
+ def setupzephir(ui, repo):
+ print 'this is reposetup!'
+ """
+ self._repocallables.append(call)
+ return call
+
+ def revset(self, symbolname):
+ """Decorated function is a revset symbol
+
+ The name of the symbol must be given as the decorator argument.
+ The symbol is added during `extsetup`.
+
+ example::
+
+ @eh.revset('hidden')
+ def revsetbabar(repo, subset, x):
+ args = revset.getargs(x, 0, 0, 'babar accept no argument')
+ return [r for r in subset if 'babar' in repo[r].description()]
+ """
+ def dec(symbol):
+ self._revsetsymbols.append((symbolname, symbol))
+ return symbol
+ return dec
+
+
+ def templatekw(self, keywordname):
+ """Decorated function is a template keyword
+
+ The name of the keyword must be given as the decorator argument.
+ The symbol is added during `extsetup`.
+
+ example::
+
+ @eh.templatekw('babar')
+ def kwbabar(ctx):
+ return 'babar'
+ """
+ def dec(keyword):
+ self._templatekws.append((keywordname, keyword))
+ return keyword
+ return dec
+
+ def wrapcommand(self, command, extension=None, opts=[]):
+ """Decorated function is a command wrapper
+
+ The name of the command must be given as the decorator argument.
+ The wrapping is installed during `uisetup`.
+
+ If the second option `extension` argument is provided, the wrapping
+ will be applied in the extension commandtable. This argument must be a
+ string that will be searched using `extension.find` if not found and
+ Abort error is raised. If the wrapping applies to an extension, it is
+ installed during `extsetup`.
+
+ example::
+
+ @eh.wrapcommand('summary')
+ def wrapsummary(orig, ui, repo, *args, **kwargs):
+ ui.note('Barry!')
+ return orig(ui, repo, *args, **kwargs)
+
+ The `opts` argument allows specifying additional arguments for the
+ command.
+
+ """
+ def dec(wrapper):
+ if extension is None:
+ self._commandwrappers.append((command, wrapper, opts))
+ else:
+ self._extcommandwrappers.append((extension, command, wrapper,
+ opts))
+ return wrapper
+ return dec
+
+ def wrapfunction(self, container, funcname):
+ """Decorated function is a function wrapper
+
+ This function takes two arguments, the container and the name of the
+ function to wrap. The wrapping is performed during `uisetup`.
+ (there is no extension support)
+
+ example::
+
+ @eh.function(discovery, 'checkheads')
+ def wrapfunction(orig, *args, **kwargs):
+ ui.note('His head smashed in and his heart cut out')
+ return orig(*args, **kwargs)
+ """
+ def dec(wrapper):
+ self._functionwrappers.append((container, funcname, wrapper))
+ return wrapper
+ return dec
+
+ def addattr(self, container, funcname):
+ """Decorated function is to be added to the container
+
+ This function takes two arguments, the container and the name of the
+ function to wrap. The wrapping is performed during `uisetup`.
+
+ example::
+
+ @eh.function(context.changectx, 'babar')
+ def babar(ctx):
+ return 'babar' in ctx.description
+ """
+ def dec(func):
+ self._duckpunchers.append((container, funcname, func))
+ return func
+ return dec
+
+eh = exthelper()
+uisetup = eh.final_uisetup
+extsetup = eh.final_extsetup
+reposetup = eh.final_reposetup
+
+#####################################################################
+### Option configuration ###
+#####################################################################
+
+@eh.reposetup # must be the first of its kin.
+def _configureoptions(ui, repo):
+ # If no capabilities are specified, enable everything.
+ # This is so existing evolve users don't need to change their config.
+ evolveopts = ui.configlist('experimental', 'evolution')
+ if not evolveopts:
+ evolveopts = ['all']
+ ui.setconfig('experimental', 'evolution', evolveopts, 'evolve')
+
+@eh.uisetup
+def _configurecmdoptions(ui):
+ # Unregister evolve commands if the command capability is not specified.
+ #
+ # This must be in the same function as the option configuration above to
+ # guarantee it happens after the above configuration, but before the
+ # extsetup functions.
+ evolvecommands = ui.configlist('experimental', 'evolutioncommands')
+ evolveopts = ui.configlist('experimental', 'evolution')
+ if evolveopts and (commandopt not in evolveopts and
+ 'all' not in evolveopts):
+ # We build whitelist containing the commands we want to enable
+ whitelist = set()
+ for cmd in evolvecommands:
+ matchingevolvecommands = [e for e in cmdtable.keys() if cmd in e]
+ if not matchingevolvecommands:
+ raise error.Abort(_('unknown command: %s') % cmd)
+ elif len(matchingevolvecommands) > 1:
+ msg = _('ambiguous command specification: "%s" matches %r')
+ raise error.Abort(msg % (cmd, matchingevolvecommands))
+ else:
+ whitelist.add(matchingevolvecommands[0])
+ for disabledcmd in set(cmdtable) - whitelist:
+ del cmdtable[disabledcmd]
+
+#####################################################################
+### experimental behavior ###
+#####################################################################
+
+commitopts3 = [
+ ('D', 'current-date', None,
+ _('record the current date as commit date')),
+ ('U', 'current-user', None,
+ _('record the current user as committer')),
+]
+
+def _resolveoptions(ui, opts):
+ """modify commit options dict to handle related options
+
+ For now, all it does is figure out the commit date: respect -D unless
+ -d was supplied.
+ """
+ # N.B. this is extremely similar to setupheaderopts() in mq.py
+ if not opts.get('date') and opts.get('current_date'):
+ opts['date'] = '%d %d' % util.makedate()
+ if not opts.get('user') and opts.get('current_user'):
+ opts['user'] = ui.username()
+
+getrevs = obsolete.getrevs
+
+#####################################################################
+### Additional Utilities ###
+#####################################################################
+
+# This section contains a lot of small utility function and method
+
+# - Function to create markers
+# - useful alias pstatus and pdiff (should probably go in evolve)
+# - "troubles" method on changectx
+# - function to travel through the obsolescence graph
+# - function to find useful changeset to stabilize
+
+
+### Useful alias
+
+@eh.uisetup
+def _installalias(ui):
+ if ui.config('alias', 'pstatus', None) is None:
+ ui.setconfig('alias', 'pstatus', 'status --rev .^', 'evolve')
+ if ui.config('alias', 'pdiff', None) is None:
+ ui.setconfig('alias', 'pdiff', 'diff --rev .^', 'evolve')
+ if ui.config('alias', 'olog', None) is None:
+ ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden",
+ 'evolve')
+ if ui.config('alias', 'odiff', None) is None:
+ ui.setconfig('alias', 'odiff',
+ "diff --hidden --rev 'limit(precursors(.),1)' --rev .",
+ 'evolve')
+ if ui.config('alias', 'grab', None) is None:
+ if os.name == 'nt':
+ ui.setconfig('alias', 'grab',
+ "! " + util.hgexecutable() + " rebase --dest . --rev $@ && "
+ + util.hgexecutable() + " up tip",
+ 'evolve')
+ else:
+ ui.setconfig('alias', 'grab',
+ "! $HG rebase --dest . --rev $@ && $HG up tip",
+ 'evolve')
+
+
+### Troubled revset symbol
+
+@eh.revset('troubled')
+def revsettroubled(repo, subset, x):
+ """``troubled()``
+ Changesets with troubles.
+ """
+ revset.getargs(x, 0, 0, 'troubled takes no arguments')
+ troubled = set()
+ troubled.update(getrevs(repo, 'unstable'))
+ troubled.update(getrevs(repo, 'bumped'))
+ troubled.update(getrevs(repo, 'divergent'))
+ troubled = revset.baseset(troubled)
+ troubled.sort() # set is non-ordered, enforce order
+ return subset & troubled
+
+### Obsolescence graph
+
+# XXX SOME MAJOR CLEAN UP TO DO HERE XXX
+
+def _precursors(repo, s):
+ """Precursor of a changeset"""
+ cs = set()
+ nm = repo.changelog.nodemap
+ markerbysubj = repo.obsstore.precursors
+ node = repo.changelog.node
+ for r in s:
+ for p in markerbysubj.get(node(r), ()):
+ pr = nm.get(p[0])
+ if pr is not None:
+ cs.add(pr)
+ cs -= repo.changelog.filteredrevs # nodemap has no filtering
+ return cs
+
+def _allprecursors(repo, s): # XXX we need a better naming
+ """transitive precursors of a subset"""
+ node = repo.changelog.node
+ toproceed = [node(r) for r in s]
+ seen = set()
+ allsubjects = repo.obsstore.precursors
+ while toproceed:
+ nc = toproceed.pop()
+ for mark in allsubjects.get(nc, ()):
+ np = mark[0]
+ if np not in seen:
+ seen.add(np)
+ toproceed.append(np)
+ nm = repo.changelog.nodemap
+ cs = set()
+ for p in seen:
+ pr = nm.get(p)
+ if pr is not None:
+ cs.add(pr)
+ cs -= repo.changelog.filteredrevs # nodemap has no filtering
+ return cs
+
+def _successors(repo, s):
+ """Successors of a changeset"""
+ cs = set()
+ node = repo.changelog.node
+ nm = repo.changelog.nodemap
+ markerbyobj = repo.obsstore.successors
+ for r in s:
+ for p in markerbyobj.get(node(r), ()):
+ for sub in p[1]:
+ sr = nm.get(sub)
+ if sr is not None:
+ cs.add(sr)
+ cs -= repo.changelog.filteredrevs # nodemap has no filtering
+ return cs
+
+def _allsuccessors(repo, s, haltonflags=0): # XXX we need a better naming
+ """transitive successors of a subset
+
+ haltonflags allows to provide flags which prevent the evaluation of a
+ marker. """
+ node = repo.changelog.node
+ toproceed = [node(r) for r in s]
+ seen = set()
+ allobjects = repo.obsstore.successors
+ while toproceed:
+ nc = toproceed.pop()
+ for mark in allobjects.get(nc, ()):
+ if mark[2] & haltonflags:
+ continue
+ for sub in mark[1]:
+ if sub == nullid:
+ continue # should not be here!
+ if sub not in seen:
+ seen.add(sub)
+ toproceed.append(sub)
+ nm = repo.changelog.nodemap
+ cs = set()
+ for s in seen:
+ sr = nm.get(s)
+ if sr is not None:
+ cs.add(sr)
+ cs -= repo.changelog.filteredrevs # nodemap has no filtering
+ return cs
+
+
+
+
+#####################################################################
+### Extending revset and template ###
+#####################################################################
+
+# this section add several useful revset symbol not yet in core.
+# they are subject to changes
+
+
+### XXX I'm not sure this revset is useful
+@eh.revset('suspended')
+def revsetsuspended(repo, subset, x):
+ """``suspended()``
+ Obsolete changesets with non-obsolete descendants.
+ """
+ revset.getargs(x, 0, 0, 'suspended takes no arguments')
+ suspended = revset.baseset(getrevs(repo, 'suspended'))
+ suspended.sort()
+ return subset & suspended
+
+
+@eh.revset('precursors')
+def revsetprecursors(repo, subset, x):
+ """``precursors(set)``
+ Immediate precursors of changesets in set.
+ """
+ s = revset.getset(repo, revset.fullreposet(repo), x)
+ s = revset.baseset(_precursors(repo, s))
+ s.sort()
+ return subset & s
+
+
+@eh.revset('allprecursors')
+def revsetallprecursors(repo, subset, x):
+ """``allprecursors(set)``
+ Transitive precursors of changesets in set.
+ """
+ s = revset.getset(repo, revset.fullreposet(repo), x)
+ s = revset.baseset(_allprecursors(repo, s))
+ s.sort()
+ return subset & s
+
+
+@eh.revset('successors')
+def revsetsuccessors(repo, subset, x):
+ """``successors(set)``
+ Immediate successors of changesets in set.
+ """
+ s = revset.getset(repo, revset.fullreposet(repo), x)
+ s = revset.baseset(_successors(repo, s))
+ s.sort()
+ return subset & s
+
+@eh.revset('allsuccessors')
+def revsetallsuccessors(repo, subset, x):
+ """``allsuccessors(set)``
+ Transitive successors of changesets in set.
+ """
+ s = revset.getset(repo, revset.fullreposet(repo), x)
+ s = revset.baseset(_allsuccessors(repo, s))
+ s.sort()
+ return subset & s
+
+### template keywords
+# XXX it does not handle troubles well :-/
+
+@eh.templatekw('obsolete')
+def obsoletekw(repo, ctx, templ, **args):
+ """:obsolete: String. Whether the changeset is ``obsolete``.
+ """
+ if ctx.obsolete():
+ return 'obsolete'
+ return ''
+
+@eh.templatekw('troubles')
+def showtroubles(repo, ctx, **args):
+ """:troubles: List of strings. Evolution troubles affecting the changeset
+ (zero or more of "unstable", "divergent" or "bumped")."""
+ return templatekw.showlist('trouble', ctx.troubles(), plural='troubles',
+ **args)
+
+#####################################################################
+### Various trouble warning ###
+#####################################################################
+
+# This section take care of issue warning to the user when troubles appear
+
+
+def _warnobsoletewc(ui, repo):
+ if repo['.'].obsolete():
+ ui.warn(_('working directory parent is obsolete!\n'))
+ if (not ui.quiet) and obsolete.isenabled(repo, commandopt):
+ ui.warn(_("(use 'hg evolve' to update to its successor)\n"))
+
+@eh.wrapcommand("update")
+@eh.wrapcommand("pull")
+def wrapmayobsoletewc(origfn, ui, repo, *args, **opts):
+ """Warn that the working directory parent is an obsolete changeset"""
+ def warnobsolete():
+ _warnobsoletewc(ui, repo)
+ wlock = None
+ try:
+ wlock = repo.wlock()
+ repo._afterlock(warnobsolete)
+ res = origfn(ui, repo, *args, **opts)
+ finally:
+ lockmod.release(wlock)
+ return res
+
+@eh.wrapcommand("parents")
+def wrapparents(origfn, ui, repo, *args, **opts):
+ res = origfn(ui, repo, *args, **opts)
+ _warnobsoletewc(ui, repo)
+ return res
+
+# XXX this could wrap transaction code
+# XXX (but this is a bit a layer violation)
+@eh.wrapcommand("commit")
+@eh.wrapcommand("import")
+@eh.wrapcommand("push")
+@eh.wrapcommand("pull")
+@eh.wrapcommand("graft")
+@eh.wrapcommand("phase")
+@eh.wrapcommand("unbundle")
+def warnobserrors(orig, ui, repo, *args, **kwargs):
+ """display warning is the command resulted in more instable changeset"""
+ # part of the troubled stuff may be filtered (stash ?)
+ # This needs a better implementation but will probably wait for core.
+ filtered = repo.changelog.filteredrevs
+ priorunstables = len(set(getrevs(repo, 'unstable')) - filtered)
+ priorbumpeds = len(set(getrevs(repo, 'bumped')) - filtered)
+ priordivergents = len(set(getrevs(repo, 'divergent')) - filtered)
+ ret = orig(ui, repo, *args, **kwargs)
+ # workaround phase stupidity
+ #phases._filterunknown(ui, repo.changelog, repo._phasecache.phaseroots)
+ filtered = repo.changelog.filteredrevs
+ newunstables = \
+ len(set(getrevs(repo, 'unstable')) - filtered) - priorunstables
+ newbumpeds = \
+ len(set(getrevs(repo, 'bumped')) - filtered) - priorbumpeds
+ newdivergents = \
+ len(set(getrevs(repo, 'divergent')) - filtered) - priordivergents
+ if newunstables > 0:
+ ui.warn(_('%i new unstable changesets\n') % newunstables)
+ if newbumpeds > 0:
+ ui.warn(_('%i new bumped changesets\n') % newbumpeds)
+ if newdivergents > 0:
+ ui.warn(_('%i new divergent changesets\n') % newdivergents)
+ return ret
+
+@eh.wrapfunction(mercurial.exchange, 'push')
+def push(orig, repo, *args, **opts):
+ """Add a hint for "hg evolve" when troubles make push fails
+ """
+ try:
+ return orig(repo, *args, **opts)
+ except error.Abort as ex:
+ hint = _("use 'hg evolve' to get a stable history "
+ "or --force to ignore warnings")
+ if (len(ex.args) >= 1
+ and ex.args[0].startswith('push includes ')
+ and ex.hint is None):
+ ex.hint = hint
+ raise
+
+def summaryhook(ui, repo):
+ def write(fmt, count):
+ s = fmt % count
+ if count:
+ ui.write(s)
+ else:
+ ui.note(s)
+
+ # util.versiontuple was introduced in 3.6.2
+ if not util.safehasattr(util, 'versiontuple'):
+ nbunstable = len(getrevs(repo, 'unstable'))
+ nbbumped = len(getrevs(repo, 'bumped'))
+ nbdivergent = len(getrevs(repo, 'divergent'))
+ write('unstable: %i changesets\n', nbunstable)
+ write('bumped: %i changesets\n', nbbumped)
+ write('divergent: %i changesets\n', nbdivergent)
+ else:
+ # In 3.6.2, summary in core gained this feature, no need to display it
+ pass
+ state = _evolvestateread(repo)
+ if state is not None:
+ # i18n: column positioning for "hg summary"
+ ui.write(_('evolve: (evolve --continue)\n'))
+
+@eh.extsetup
+def obssummarysetup(ui):
+ cmdutil.summaryhooks.add('evolve', summaryhook)
+
+
+#####################################################################
+### Core Other extension compat ###
+#####################################################################
+
+
+@eh.extsetup
+def _rebasewrapping(ui):
+ # warning about more obsolete
+ try:
+ rebase = extensions.find('rebase')
+ if rebase:
+ extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors)
+ except KeyError:
+ pass # rebase not found
+ try:
+ histedit = extensions.find('histedit')
+ if histedit:
+ extensions.wrapcommand(histedit.cmdtable, 'histedit', warnobserrors)
+ except KeyError:
+ pass # histedit not found
+
+#####################################################################
+### Old Evolve extension content ###
+#####################################################################
+
+# XXX need clean up and proper sorting in other section
+
+### util function
+#############################
+
+### changeset rewriting logic
+#############################
+
+def rewrite(repo, old, updates, head, newbases, commitopts):
+ """Return (nodeid, created) where nodeid is the identifier of the
+ changeset generated by the rewrite process, and created is True if
+ nodeid was actually created. If created is False, nodeid
+ references a changeset existing before the rewrite call.
+ """
+ wlock = lock = tr = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ tr = repo.transaction('rewrite')
+ if len(old.parents()) > 1: #XXX remove this unnecessary limitation.
+ raise error.Abort(_('cannot amend merge changesets'))
+ base = old.p1()
+ updatebookmarks = _bookmarksupdater(repo, old.node(), tr)
+
+ # commit a new version of the old changeset, including the update
+ # collect all files which might be affected
+ files = set(old.files())
+ for u in updates:
+ files.update(u.files())
+
+ # Recompute copies (avoid recording a -> b -> a)
+ copied = copies.pathcopies(base, head)
+
+
+ # prune files which were reverted by the updates
+ def samefile(f):
+ if f in head.manifest():
+ a = head.filectx(f)
+ if f in base.manifest():
+ b = base.filectx(f)
+ return (a.data() == b.data()
+ and a.flags() == b.flags())
+ else:
+ return False
+ else:
+ return f not in base.manifest()
+ files = [f for f in files if not samefile(f)]
+ # commit version of these files as defined by head
+ headmf = head.manifest()
+ def filectxfn(repo, ctx, path):
+ if path in headmf:
+ fctx = head[path]
+ flags = fctx.flags()
+ mctx = memfilectx(repo, fctx.path(), fctx.data(),
+ islink='l' in flags,
+ isexec='x' in flags,
+ copied=copied.get(path))
+ return mctx
+ return None
+
+ message = cmdutil.logmessage(repo.ui, commitopts)
+ if not message:
+ message = old.description()
+
+ user = commitopts.get('user') or old.user()
+ date = commitopts.get('date') or None # old.date()
+ extra = dict(commitopts.get('extra', old.extra()))
+ extra['branch'] = head.branch()
+
+ new = context.memctx(repo,
+ parents=newbases,
+ text=message,
+ files=files,
+ filectxfn=filectxfn,
+ user=user,
+ date=date,
+ extra=extra)
+
+ if commitopts.get('edit'):
+ new._text = cmdutil.commitforceeditor(repo, new, [])
+ revcount = len(repo)
+ newid = repo.commitctx(new)
+ new = repo[newid]
+ created = len(repo) != revcount
+ updatebookmarks(newid)
+
+ tr.close()
+ return newid, created
+ finally:
+ lockmod.release(tr, lock, wlock)
+
+class MergeFailure(error.Abort):
+ pass
+
+def relocate(repo, orig, dest, pctx=None, keepbranch=False):
+ """rewrite <rev> on dest"""
+ if orig.rev() == dest.rev():
+ raise error.Abort(_('tried to relocate a node on top of itself'),
+ hint=_("This shouldn't happen. If you still "
+ "need to move changesets, please do so "
+ "manually with nothing to rebase - working "
+ "directory parent is also destination"))
+
+ if pctx is None:
+ if len(orig.parents()) == 2:
+ raise error.Abort(_("tried to relocate a merge commit without "
+ "specifying which parent should be moved"),
+ hint=_("Specify the parent by passing in pctx"))
+ pctx = orig.p1()
+
+ destbookmarks = repo.nodebookmarks(dest.node())
+ nodesrc = orig.node()
+ destphase = repo[nodesrc].phase()
+ commitmsg = orig.description()
+
+ cache = {}
+ sha1s = re.findall(sha1re, commitmsg)
+ unfi = repo.unfiltered()
+ for sha1 in sha1s:
+ ctx = None
+ try:
+ ctx = unfi[sha1]
+ except error.RepoLookupError:
+ continue
+
+ if not ctx.obsolete():
+ continue
+
+ successors = obsolete.successorssets(repo, ctx.node(), cache)
+
+ # We can't make any assumptions about how to update the hash if the
+ # cset in question was split or diverged.
+ if len(successors) == 1 and len(successors[0]) == 1:
+ newsha1 = node.hex(successors[0][0])
+ commitmsg = commitmsg.replace(sha1, newsha1[:len(sha1)])
+ else:
+ repo.ui.note(_('The stale commit message reference to %s could '
+ 'not be updated\n') % sha1)
+
+ tr = repo.currenttransaction()
+ assert tr is not None
+ try:
+ r = _evolvemerge(repo, orig, dest, pctx, keepbranch)
+ if r[-1]: #some conflict
+ raise error.Abort(
+ 'unresolved merge conflicts (see hg help resolve)')
+ nodenew = _relocatecommit(repo, orig, commitmsg)
+ except error.Abort as exc:
+ repo.dirstate.beginparentchange()
+ repo.setparents(repo['.'].node(), nullid)
+ writedirstate(repo.dirstate, tr)
+ # fix up dirstate for copies and renames
+ copies.duplicatecopies(repo, dest.rev(), orig.p1().rev())
+ repo.dirstate.endparentchange()
+ class LocalMergeFailure(MergeFailure, exc.__class__):
+ pass
+ exc.__class__ = LocalMergeFailure
+ tr.close() # to keep changes in this transaction (e.g. dirstate)
+ raise
+ oldbookmarks = repo.nodebookmarks(nodesrc)
+ _finalizerelocate(repo, orig, dest, nodenew, tr)
+ return nodenew
+
+def _bookmarksupdater(repo, oldid, tr):
+ """Return a callable update(newid) updating the current bookmark
+ and bookmarks bound to oldid to newid.
+ """
+ def updatebookmarks(newid):
+ dirty = False
+ oldbookmarks = repo.nodebookmarks(oldid)
+ if oldbookmarks:
+ for b in oldbookmarks:
+ repo._bookmarks[b] = newid
+ dirty = True
+ if dirty:
+ repo._bookmarks.recordchange(tr)
+ return updatebookmarks
+
+### bookmarks api compatibility layer ###
+def bmdeactivate(repo):
+ try:
+ return bookmarksmod.deactivate(repo)
+ except AttributeError:
+ return bookmarksmod.unsetcurrent(repo)
+def bmactivate(repo, book):
+ try:
+ return bookmarksmod.activate(repo, book)
+ except AttributeError:
+ return bookmarksmod.setcurrent(repo, book)
+
+def bmactive(repo):
+ try:
+ return repo._activebookmark
+ except AttributeError:
+ return repo._bookmarkcurrent
+
+### dirstate compatibility layer < hg 3.6
+
+def writedirstate(dirstate, tr):
+ if dirstate.write.func_code.co_argcount != 1: # mercurial 3.6 and above
+ return dirstate.write(tr)
+ return dirstate.write()
+
+
+
+### new command
+#############################
+metadataopts = [
+ ('d', 'date', '',
+ _('record the specified date in metadata'), _('DATE')),
+ ('u', 'user', '',
+ _('record the specified user in metadata'), _('USER')),
+]
+
+@eh.uisetup
+def _installimportobsolete(ui):
+ entry = cmdutil.findcmd('import', commands.table)[1]
+ entry[1].append(('', 'obsolete', False,
+ _('mark the old node as obsoleted by '
+ 'the created commit')))
+
+@eh.wrapfunction(mercurial.cmdutil, 'tryimportone')
+def tryimportone(orig, ui, repo, hunk, parents, opts, *args, **kwargs):
+ extracted = patch.extract(ui, hunk)
+ if util.safehasattr(extracted, 'get'):
+ # mercurial 3.6 return a dictionary there
+ expected = extracted.get('nodeid')
+ else:
+ expected = extracted[5]
+ if expected is not None:
+ expected = node.bin(expected)
+ oldextract = patch.extract
+ try:
+ patch.extract = lambda ui, hunk: extracted
+ ret = orig(ui, repo, hunk, parents, opts, *args, **kwargs)
+ finally:
+ patch.extract = oldextract
+ created = ret[1]
+ if (opts['obsolete'] and None not in (created, expected)
+ and created != expected):
+ tr = repo.transaction('import-obs')
+ try:
+ metadata = {'user': ui.username()}
+ repo.obsstore.create(tr, expected, (created,),
+ metadata=metadata)
+ tr.close()
+ finally:
+ tr.release()
+ return ret
+
+
+def _deprecatealias(oldalias, newalias):
+ '''Deprecates an alias for a command in favour of another
+
+ Creates a new entry in the command table for the old alias. It creates a
+ wrapper that has its synopsis set to show that is has been deprecated.
+ The documentation will be replace with a pointer to the new alias.
+ If a user invokes the command a deprecation warning will be printed and
+ the command of the *new* alias will be invoked.
+
+ This function is loosely based on the extensions.wrapcommand function.
+ '''
+ try:
+ aliases, entry = cmdutil.findcmd(newalias, cmdtable)
+ except error.UnknownCommand:
+ # Commands may be disabled
+ return
+ for alias, e in cmdtable.items():
+ if e is entry:
+ break
+
+ synopsis = '(DEPRECATED)'
+ if len(entry) > 2:
+ fn, opts, _syn = entry
+ else:
+ fn, opts, = entry
+ deprecationwarning = _('%s have been deprecated in favor of %s\n') % (
+ oldalias, newalias)
+ def newfn(*args, **kwargs):
+ ui = args[0]
+ ui.warn(deprecationwarning)
+ util.checksignature(fn)(*args, **kwargs)
+ newfn.__doc__ = deprecationwarning
+ cmdwrapper = command(oldalias, opts, synopsis)
+ cmdwrapper(newfn)
+
+@eh.extsetup
+def deprecatealiases(ui):
+ _deprecatealias('gup', 'next')
+ _deprecatealias('gdown', 'previous')
+
+@command('debugrecordpruneparents', [], '')
+def cmddebugrecordpruneparents(ui, repo):
+ """add parent data to prune markers when possible
+
+ This command searches the repo for prune markers without parent information.
+ If the pruned node is locally known, it creates a new marker with parent
+ data.
+ """
+ pgop = 'reading markers'
+
+ # lock from the beginning to prevent race
+ wlock = lock = tr = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ tr = repo.transaction('recordpruneparents')
+ unfi = repo.unfiltered()
+ nm = unfi.changelog.nodemap
+ store = repo.obsstore
+ pgtotal = len(store._all)
+ for idx, mark in enumerate(list(store._all)):
+ if not mark[1]:
+ rev = nm.get(mark[0])
+ if rev is not None:
+ ctx = unfi[rev]
+ parents = tuple(p.node() for p in ctx.parents())
+ before = len(store._all)
+ store.create(tr, mark[0], mark[1], mark[2], mark[3],
+ parents=parents)
+ if len(store._all) - before:
+ ui.write(_('created new markers for %i\n') % rev)
+ ui.progress(pgop, idx, total=pgtotal)
+ tr.close()
+ ui.progress(pgop, None)
+ finally:
+ lockmod.release(tr, lock, wlock)
+
+@command('debugobsstorestat', [], '')
+def cmddebugobsstorestat(ui, repo):
+ """print statistics about obsolescence markers in the repo"""
+ def _updateclustermap(nodes, mark, clustersmap):
+ c = (set(nodes), set([mark]))
+ toproceed = set(nodes)
+ while toproceed:
+ n = toproceed.pop()
+ other = clustersmap.get(n)
+ if (other is not None
+ and other is not c):
+ other[0].update(c[0])
+ other[1].update(c[1])
+ for on in c[0]:
+ if on in toproceed:
+ continue
+ clustersmap[on] = other
+ c = other
+ clustersmap[n] = c
+
+ store = repo.obsstore
+ unfi = repo.unfiltered()
+ nm = unfi.changelog.nodemap
+ ui.write(_('markers total: %9i\n') % len(store._all))
+ sucscount = [0, 0 , 0, 0]
+ known = 0
+ parentsdata = 0
+ metakeys = {}
+ # node -> cluster mapping
+ # a cluster is a (set(nodes), set(markers)) tuple
+ clustersmap = {}
+ # same data using parent information
+ pclustersmap = {}
+ for mark in store:
+ if mark[0] in nm:
+ known += 1
+ nbsucs = len(mark[1])
+ sucscount[min(nbsucs, 3)] += 1
+ meta = mark[3]
+ for key, value in meta:
+ metakeys.setdefault(key, 0)
+ metakeys[key] += 1
+ meta = dict(meta)
+ parents = [meta.get('p1'), meta.get('p2')]
+ parents = [node.bin(p) for p in parents if p is not None]
+ if parents:
+ parentsdata += 1
+ # cluster handling
+ nodes = set(mark[1])
+ nodes.add(mark[0])
+ _updateclustermap(nodes, mark, clustersmap)
+ # same with parent data
+ nodes.update(parents)
+ _updateclustermap(nodes, mark, pclustersmap)
+
+ # freezing the result
+ for c in clustersmap.values():
+ fc = (frozenset(c[0]), frozenset(c[1]))
+ for n in fc[0]:
+ clustersmap[n] = fc
+ # same with parent data
+ for c in pclustersmap.values():
+ fc = (frozenset(c[0]), frozenset(c[1]))
+ for n in fc[0]:
+ pclustersmap[n] = fc
+ ui.write((' for known precursors: %9i\n' % known))
+ ui.write((' with parents data: %9i\n' % parentsdata))
+ # successors data
+ ui.write(('markers with no successors: %9i\n' % sucscount[0]))
+ ui.write((' 1 successors: %9i\n' % sucscount[1]))
+ ui.write((' 2 successors: %9i\n' % sucscount[2]))
+ ui.write((' more than 2 successors: %9i\n' % sucscount[3]))
+ # meta data info
+ ui.write((' available keys:\n'))
+ for key in sorted(metakeys):
+ ui.write((' %15s: %9i\n' % (key, metakeys[key])))
+
+ allclusters = list(set(clustersmap.values()))
+ allclusters.sort(key=lambda x: len(x[1]))
+ ui.write(('disconnected clusters: %9i\n' % len(allclusters)))
+
+ ui.write(' any known node: %9i\n'
+ % len([c for c in allclusters
+ if [n for n in c[0] if nm.get(n) is not None]]))
+ if allclusters:
+ nbcluster = len(allclusters)
+ ui.write((' smallest length: %9i\n' % len(allclusters[0][1])))
+ ui.write((' longer length: %9i\n'
+ % len(allclusters[-1][1])))
+ median = len(allclusters[nbcluster//2][1])
+ ui.write((' median length: %9i\n' % median))
+ mean = sum(len(x[1]) for x in allclusters) // nbcluster
+ ui.write((' mean length: %9i\n' % mean))
+ allpclusters = list(set(pclustersmap.values()))
+ allpclusters.sort(key=lambda x: len(x[1]))
+ ui.write((' using parents data: %9i\n' % len(allpclusters)))
+ ui.write(' any known node: %9i\n'
+ % len([c for c in allclusters
+ if [n for n in c[0] if nm.get(n) is not None]]))
+ if allpclusters:
+ nbcluster = len(allpclusters)
+ ui.write((' smallest length: %9i\n'
+ % len(allpclusters[0][1])))
+ ui.write((' longer length: %9i\n'
+ % len(allpclusters[-1][1])))
+ median = len(allpclusters[nbcluster//2][1])
+ ui.write((' median length: %9i\n' % median))
+ mean = sum(len(x[1]) for x in allpclusters) // nbcluster
+ ui.write((' mean length: %9i\n' % mean))
+
+def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category):
+ """Resolve the troubles affecting one revision"""
+ wlock = lock = tr = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ tr = repo.transaction("evolve")
+ if 'unstable' == category:
+ result = _solveunstable(ui, repo, ctx, dryrun, confirm, progresscb)
+ elif 'bumped' == category:
+ result = _solvebumped(ui, repo, ctx, dryrun, confirm, progresscb)
+ elif 'divergent' == category:
+ result = _solvedivergent(ui, repo, ctx, dryrun, confirm,
+ progresscb)
+ else:
+ assert False, "unknown trouble category: %s" % (category)
+ tr.close()
+ return result
+ finally:
+ lockmod.release(tr, lock, wlock)
+
+def _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat):
+ """Used by the evolve function to display an error message when
+ no troubles can be resolved"""
+ troublecategories = ['bumped', 'divergent', 'unstable']
+ unselectedcategories = [c for c in troublecategories if c != targetcat]
+ msg = None
+ hint = None
+
+ troubled = {
+ "unstable": repo.revs("unstable()"),
+ "divergent": repo.revs("divergent()"),
+ "bumped": repo.revs("bumped()"),
+ "all": repo.revs("troubled()"),
+ }
+
+
+ hintmap = {
+ 'bumped': _("do you want to use --bumped"),
+ 'bumped+divergent': _("do you want to use --bumped or --divergent"),
+ 'bumped+unstable': _("do you want to use --bumped or --unstable"),
+ 'divergent': _("do you want to use --divergent"),
+ 'divergent+unstable': _("do you want to use --divergent"
+ " or --unstable"),
+ 'unstable': _("do you want to use --unstable"),
+ 'any+bumped': _("do you want to use --any (or --rev) and --bumped"),
+ 'any+bumped+divergent': _("do you want to use --any (or --rev) and"
+ " --bumped or --divergent"),
+ 'any+bumped+unstable': _("do you want to use --any (or --rev) and"
+ "--bumped or --unstable"),
+ 'any+divergent': _("do you want to use --any (or --rev) and"
+ " --divergent"),
+ 'any+divergent+unstable': _("do you want to use --any (or --rev)"
+ " and --divergent or --unstable"),
+ 'any+unstable': _("do you want to use --any (or --rev)"
+ "and --unstable"),
+ }
+
+ if revopt:
+ revs = scmutil.revrange(repo, revopt)
+ if not revs:
+ msg = _("set of specified revisions is empty")
+ else:
+ msg = _("no %s changesets in specified revisions") % targetcat
+ othertroubles = []
+ for cat in unselectedcategories:
+ if revs & troubled[cat]:
+ othertroubles.append(cat)
+ if othertroubles:
+ hint = hintmap['+'.join(othertroubles)]
+
+ elif anyopt:
+ msg = _("no %s changesets to evolve") % targetcat
+ othertroubles = []
+ for cat in unselectedcategories:
+ if troubled[cat]:
+ othertroubles.append(cat)
+ if othertroubles:
+ hint = hintmap['+'.join(othertroubles)]
+
+ else:
+ # evolve without any option = relative to the current wdir
+ if targetcat == 'unstable':
+ msg = _("nothing to evolve on current working copy parent")
+ else:
+ msg = _("current working copy parent is not %s") % targetcat
+
+ p1 = repo['.'].rev()
+ othertroubles = []
+ for cat in unselectedcategories:
+ if p1 in troubled[cat]:
+ othertroubles.append(cat)
+ if othertroubles:
+ hint = hintmap['+'.join(othertroubles)]
+ else:
+ l = len(troubled[targetcat])
+ if l:
+ hint = _("%d other %s in the repository, do you want --any "
+ "or --rev") % (l, targetcat)
+ else:
+ othertroubles = []
+ for cat in unselectedcategories:
+ if troubled[cat]:
+ othertroubles.append(cat)
+ if othertroubles:
+ hint = hintmap['any+'+('+'.join(othertroubles))]
+ else:
+ msg = _("no troubled changesets")
+
+ assert msg is not None
+ ui.write_err(msg+"\n")
+ if hint:
+ ui.write_err("("+hint+")\n")
+ return 2
+ else:
+ return 1
+
+def _cleanup(ui, repo, startnode, showprogress):
+ if showprogress:
+ ui.progress(_('evolve'), None)
+ if repo['.'] != startnode:
+ ui.status(_('working directory is now at %s\n') % repo['.'])
+
+class MultipleSuccessorsError(RuntimeError):
+ """Exception raised by _singlesuccessor when multiple successor sets exists
+
+ The object contains the list of successorssets in its 'successorssets'
+ attribute to call to easily recover.
+ """
+
+ def __init__(self, successorssets):
+ self.successorssets = successorssets
+
+def _singlesuccessor(repo, p):
+ """returns p (as rev) if not obsolete or its unique latest successors
+
+ fail if there are no such successor"""
+
+ if not p.obsolete():
+ return p.rev()
+ obs = repo[p]
+ ui = repo.ui
+ newer = obsolete.successorssets(repo, obs.node())
+ # search of a parent which is not killed
+ while not newer:
+ ui.debug("stabilize target %s is plain dead,"
+ " trying to stabilize on its parent\n" %
+ obs)
+ obs = obs.parents()[0]
+ newer = obsolete.successorssets(repo, obs.node())
+ if len(newer) > 1 or len(newer[0]) > 1:
+ raise MultipleSuccessorsError(newer)
+
+ return repo[newer[0][0]].rev()
+
+def builddependencies(repo, revs):
+ """returns dependency graphs giving an order to solve instability of revs
+ (see _orderrevs for more information on usage)"""
+
+ # For each troubled revision we keep track of what instability if any should
+ # be resolved in order to resolve it. Example:
+ # dependencies = {3: [6], 6:[]}
+ # Means that: 6 has no dependency, 3 depends on 6 to be solved
+ dependencies = {}
+ # rdependencies is the inverted dict of dependencies
+ rdependencies = collections.defaultdict(set)
+
+ for r in revs:
+ dependencies[r] = set()
+ for p in repo[r].parents():
+ try:
+ succ = _singlesuccessor(repo, p)
+ except MultipleSuccessorsError as exc:
+ dependencies[r] = exc.successorssets
+ continue
+ if succ in revs:
+ dependencies[r].add(succ)
+ rdependencies[succ].add(r)
+ return dependencies, rdependencies
+
+def _dedupedivergents(repo, revs):
+ """Dedupe the divergents revs in revs to get one from each group with the
+ lowest revision numbers
+ """
+ repo = repo.unfiltered()
+ res = set()
+ # To not reevaluate divergents of the same group once one is encountered
+ discarded = set()
+ for rev in revs:
+ if rev in discarded:
+ continue
+ divergent = repo[rev]
+ base, others = divergentdata(divergent)
+ othersrevs = [o.rev() for o in others]
+ res.add(min([divergent.rev()] + othersrevs))
+ discarded.update(othersrevs)
+ return res
+
+def _selectrevs(repo, allopt, revopt, anyopt, targetcat):
+ """select troubles in repo matching according to given options"""
+ revs = set()
+ if allopt or revopt:
+ revs = repo.revs(targetcat+'()')
+ if revopt:
+ revs = scmutil.revrange(repo, revopt) & revs
+ elif not anyopt:
+ topic = getattr(repo, 'currenttopic', '')
+ if topic:
+ revs = repo.revs('topic(%s)', topic) & revs
+ elif targetcat == 'unstable':
+ revs = _aspiringdescendant(repo,
+ repo.revs('(.::) - obsolete()::'))
+ revs = set(revs)
+ if targetcat == 'divergent':
+ # Pick one divergent per group of divergents
+ revs = _dedupedivergents(repo, revs)
+ elif anyopt:
+ revs = repo.revs('first(%s())' % (targetcat))
+ elif targetcat == 'unstable':
+ revs = set(_aspiringchildren(repo, repo.revs('(.::) - obsolete()::')))
+ if 1 < len(revs):
+ msg = "multiple evolve candidates"
+ hint = (_("select one of %s with --rev")
+ % ', '.join([str(repo[r]) for r in sorted(revs)]))
+ raise error.Abort(msg, hint=hint)
+ elif targetcat in repo['.'].troubles():
+ revs = set([repo['.'].rev()])
+ return revs
+
+
+def _orderrevs(repo, revs):
+ """Compute an ordering to solve instability for the given revs
+
+ revs is a list of unstable revisions.
+
+ Returns the same revisions ordered to solve their instability from the
+ bottom to the top of the stack that the stabilization process will produce
+ eventually.
+
+ This ensures the minimal number of stabilizations, as we can stabilize each
+ revision on its final stabilized destination.
+ """
+ # Step 1: Build the dependency graph
+ dependencies, rdependencies = builddependencies(repo, revs)
+ # Step 2: Build the ordering
+ # Remove the revisions with no dependency(A) and add them to the ordering.
+ # Removing these revisions leads to new revisions with no dependency (the
+ # one depending on A) that we can remove from the dependency graph and add
+ # to the ordering. We progress in a similar fashion until the ordering is
+ # built
+ solvablerevs = collections.deque([r for r in sorted(dependencies.keys())
+ if not dependencies[r]])
+ ordering = []
+ while solvablerevs:
+ rev = solvablerevs.popleft()
+ for dependent in rdependencies[rev]:
+ dependencies[dependent].remove(rev)
+ if not dependencies[dependent]:
+ solvablerevs.append(dependent)
+ del dependencies[rev]
+ ordering.append(rev)
+
+ ordering.extend(sorted(dependencies))
+ return ordering
+
+def divergentsets(repo, ctx):
+ """Compute sets of commits divergent with a given one"""
+ cache = {}
+ succsets = {}
+ base = {}
+ for n in obsolete.allprecursors(repo.obsstore, [ctx.node()]):
+ if n == ctx.node():
+ # a node can't be a base for divergence with itself
+ continue
+ nsuccsets = obsolete.successorssets(repo, n, cache)
+ for nsuccset in nsuccsets:
+ if ctx.node() in nsuccset:
+ # we are only interested in *other* successor sets
+ continue
+ if tuple(nsuccset) in base:
+ # we already know the latest base for this divergency
+ continue
+ base[tuple(nsuccset)] = n
+ divergence = []
+ for divset, b in base.iteritems():
+ divergence.append({
+ 'divergentnodes': divset,
+ 'commonprecursor': b
+ })
+
+ return divergence
+
+def _preparelistctxs(items, condition):
+ return [item.hex() for item in items if condition(item)]
+
+def _formatctx(fm, ctx):
+ fm.data(node=ctx.hex())
+ fm.data(desc=ctx.description())
+ fm.data(date=ctx.date())
+ fm.data(user=ctx.user())
+
+def listtroubles(ui, repo, troublecategories, **opts):
+ """Print all the troubles for the repo (or given revset)"""
+ troublecategories = troublecategories or ['divergent', 'unstable', 'bumped']
+ showunstable = 'unstable' in troublecategories
+ showbumped = 'bumped' in troublecategories
+ showdivergent = 'divergent' in troublecategories
+
+ revs = repo.revs('+'.join("%s()" % t for t in troublecategories))
+ if opts.get('rev'):
+ revs = revs & repo.revs(opts.get('rev'))
+
+ fm = ui.formatter('evolvelist', opts)
+ for rev in revs:
+ ctx = repo[rev]
+ unpars = _preparelistctxs(ctx.parents(), lambda p: p.unstable())
+ obspars = _preparelistctxs(ctx.parents(), lambda p: p.obsolete())
+ imprecs = _preparelistctxs(repo.set("allprecursors(%n)", ctx.node()),
+ lambda p: not p.mutable())
+ dsets = divergentsets(repo, ctx)
+
+ fm.startitem()
+ # plain formatter section
+ hashlen, desclen = 12, 60
+ desc = ctx.description()
+ if desc:
+ desc = desc.splitlines()[0]
+ desc = (desc[:desclen] + '...') if len(desc) > desclen else desc
+ fm.plain('%s: ' % ctx.hex()[:hashlen])
+ fm.plain('%s\n' % desc)
+ fm.data(node=ctx.hex(), rev=ctx.rev(), desc=desc, phase=ctx.phasestr())
+
+ for unpar in unpars if showunstable else []:
+ fm.plain(' unstable: %s (unstable parent)\n' % unpar[:hashlen])
+ for obspar in obspars if showunstable else []:
+ fm.plain(' unstable: %s (obsolete parent)\n' % obspar[:hashlen])
+ for imprec in imprecs if showbumped else []:
+ fm.plain(' bumped: %s (immutable precursor)\n' % imprec[:hashlen])
+
+ if dsets and showdivergent:
+ for dset in dsets:
+ fm.plain(' divergent: ')
+ first = True
+ for n in dset['divergentnodes']:
+ t = "%s (%s)" if first else " %s (%s)"
+ first = False
+ fm.plain(t % (node.hex(n)[:hashlen], repo[n].phasestr()))
+ comprec = node.hex(dset['commonprecursor'])[:hashlen]
+ fm.plain(" (precursor %s)\n" % comprec)
+ fm.plain("\n")
+
+ # templater-friendly section
+ _formatctx(fm, ctx)
+ troubles = []
+ for unpar in unpars:
+ troubles.append({'troubletype': 'unstable', 'sourcenode': unpar,
+ 'sourcetype': 'unstableparent'})
+ for obspar in obspars:
+ troubles.append({'troubletype': 'unstable', 'sourcenode': obspar,
+ 'sourcetype': 'obsoleteparent'})
+ for imprec in imprecs:
+ troubles.append({'troubletype': 'bumped', 'sourcenode': imprec,
+ 'sourcetype': 'immutableprecursor'})
+ for dset in dsets:
+ divnodes = [{'node': node.hex(n),
+ 'phase': repo[n].phasestr(),
+ } for n in dset['divergentnodes']]
+ troubles.append({'troubletype': 'divergent',
+ 'commonprecursor': node.hex(dset['commonprecursor']),
+ 'divergentnodes': divnodes})
+ fm.data(troubles=troubles)
+
+ fm.end()
+
+@command('^evolve|stabilize|solve',
+ [('n', 'dry-run', False,
+ _('do not perform actions, just print what would be done')),
+ ('', 'confirm', False,
+ _('ask for confirmation before performing the action')),
+ ('A', 'any', False,
+ _('also consider troubled changesets unrelated to current working '
+ 'directory')),
+ ('r', 'rev', [], _('solves troubles of these revisions')),
+ ('', 'bumped', False, _('solves only bumped changesets')),
+ ('', 'divergent', False, _('solves only divergent changesets')),
+ ('', 'unstable', False, _('solves only unstable changesets (default)')),
+ ('a', 'all', False, _('evolve all troubled changesets related to the '
+ 'current working directory and its descendants')),
+ ('c', 'continue', False, _('continue an interrupted evolution')),
+ ('l', 'list', False, 'provide details on troubled changesets in the repo'),
+ ] + mergetoolopts,
+ _('[OPTIONS]...'))
+def evolve(ui, repo, **opts):
+ """solve troubled changesets in your repository
+
+ Modifying history can lead to various types of troubled changesets:
+ unstable, bumped, or divergent. The evolve command resolves your troubles
+ by executing one of the following actions:
+
+ - update working copy to a successor
+ - rebase an unstable changeset
+ - extract the desired changes from a bumped changeset
+ - fuse divergent changesets back together
+
+ If you pass no arguments, evolve works in automatic mode: it will execute a
+ single action to reduce instability related to your working copy. There are
+ two cases for this action. First, if the parent of your working copy is
+ obsolete, evolve updates to the parent's successor. Second, if the working
+ copy parent is not obsolete but has obsolete predecessors, then evolve
+ determines if there is an unstable changeset that can be rebased onto the
+ working copy parent in order to reduce instability.
+ If so, evolve rebases that changeset. If not, evolve refuses to guess your
+ intention, and gives a hint about what you might want to do next.
+
+ Any time evolve creates a changeset, it updates the working copy to the new
+ changeset. (Currently, every successful evolve operation involves an update
+ as well; this may change in future.)
+
+ Automatic mode only handles common use cases. For example, it avoids taking
+ action in the case of ambiguity, and it ignores unstable changesets that
+ are not related to your working copy.
+ It also refuses to solve bumped or divergent changesets unless you explicity
+ request such behavior (see below).
+
+ Eliminating all instability around your working copy may require multiple
+ invocations of :hg:`evolve`. Alternately, use ``--all`` to recursively
+ select and evolve all unstable changesets that can be rebased onto the
+ working copy parent.
+ This is more powerful than successive invocations, since ``--all`` handles
+ ambiguous cases (e.g. unstable changesets with multiple children) by
+ evolving all branches.
+
+ When your repository cannot be handled by automatic mode, you might need to
+ use ``--rev`` to specify a changeset to evolve. For example, if you have
+ an unstable changeset that is not related to the working copy parent,
+ you could use ``--rev`` to evolve it. Or, if some changeset has multiple
+ unstable children, evolve in automatic mode refuses to guess which one to
+ evolve; you have to use ``--rev`` in that case.
+
+ Alternately, ``--any`` makes evolve search for the next evolvable changeset
+ regardless of whether it is related to the working copy parent.
+
+ You can supply multiple revisions to evolve multiple troubled changesets
+ in a single invocation. In revset terms, ``--any`` is equivalent to ``--rev
+ first(unstable())``. ``--rev`` and ``--all`` are mutually exclusive, as are
+ ``--rev`` and ``--any``.
+
+ ``hg evolve --any --all`` is useful for cleaning up instability across all
+ branches, letting evolve figure out the appropriate order and destination.
+
+ When you have troubled changesets that are not unstable, :hg:`evolve`
+ refuses to consider them unless you specify the category of trouble you
+ wish to resolve, with ``--bumped`` or ``--divergent``. These options are
+ currently mutually exclusive with each other and with ``--unstable``
+ (the default). You can combine ``--bumped`` or ``--divergent`` with
+ ``--rev``, ``--all``, or ``--any``.
+
+ You can also use the evolve command to list the troubles affecting your
+ repository by using the --list flag. You can choose to display only some
+ categories of troubles with the --unstable, --divergent or --bumped flags.
+ """
+
+ # Options
+ listopt = opts['list']
+ contopt = opts['continue']
+ anyopt = opts['any']
+ allopt = opts['all']
+ startnode = repo['.']
+ dryrunopt = opts['dry_run']
+ confirmopt = opts['confirm']
+ revopt = opts['rev']
+ troublecategories = ['bumped', 'divergent', 'unstable']
+ specifiedcategories = [t for t in troublecategories if opts[t]]
+ if listopt:
+ listtroubles(ui, repo, specifiedcategories, **opts)
+ return
+
+ targetcat = 'unstable'
+ if 1 < len(specifiedcategories):
+ msg = _('cannot specify more than one trouble category to solve (yet)')
+ raise error.Abort(msg)
+ elif len(specifiedcategories) == 1:
+ targetcat = specifiedcategories[0]
+ elif repo['.'].obsolete():
+ displayer = cmdutil.show_changeset(ui, repo,
+ {'template': shorttemplate})
+ # no args and parent is obsolete, update to successors
+ try:
+ ctx = repo[_singlesuccessor(repo, repo['.'])]
+ except MultipleSuccessorsError as exc:
+ repo.ui.write_err('parent is obsolete with multiple successors:\n')
+ for ln in exc.successorssets:
+ for n in ln:
+ displayer.show(repo[n])
+ return 2
+
+
+ ui.status(_('update:'))
+ if not ui.quiet:
+ displayer.show(ctx)
+
+ if dryrunopt:
+ return 0
+ res = hg.update(repo, ctx.rev())
+ if ctx != startnode:
+ ui.status(_('working directory is now at %s\n') % ctx)
+ return res
+
+ ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'evolve')
+ troubled = set(repo.revs('troubled()'))
+
+ # Progress handling
+ seen = 1
+ count = allopt and len(troubled) or 1
+ showprogress = allopt
+
+ def progresscb():
+ if revopt or allopt:
+ ui.progress(_('evolve'), seen, unit=_('changesets'), total=count)
+
+ # Continuation handling
+ if contopt:
+ if anyopt:
+ raise error.Abort('cannot specify both "--any" and "--continue"')
+ if allopt:
+ raise error.Abort('cannot specify both "--all" and "--continue"')
+ state = _evolvestateread(repo)
+ if state is None:
+ raise error.Abort('no evolve to continue')
+ orig = repo[state['current']]
+ # XXX This is a terrible terrible hack, please get rid of it.
+ lock = repo.wlock()
+ try:
+ repo.opener.write('graftstate', orig.hex() + '\n')
+ try:
+ graftcmd = commands.table['graft'][0]
+ ret = graftcmd(ui, repo, old_obsolete=True, **{'continue': True})
+ _evolvestatedelete(repo)
+ return ret
+ finally:
+ util.unlinkpath(repo.join('graftstate'), ignoremissing=True)
+ finally:
+ lock.release()
+ cmdutil.bailifchanged(repo)
+
+
+ if revopt and allopt:
+ raise error.Abort('cannot specify both "--rev" and "--all"')
+ if revopt and anyopt:
+ raise error.Abort('cannot specify both "--rev" and "--any"')
+
+ revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat)
+
+ if not revs:
+ return _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat)
+
+ # For the progress bar to show
+ count = len(revs)
+ # Order the revisions
+ if targetcat == 'unstable':
+ revs = _orderrevs(repo, revs)
+ for rev in revs:
+ progresscb()
+ _solveone(ui, repo, repo[rev], dryrunopt, confirmopt,
+ progresscb, targetcat)
+ seen += 1
+ progresscb()
+ _cleanup(ui, repo, startnode, showprogress)
+
+def _possibledestination(repo, rev):
+ """return all changesets that may be a new parent for REV"""
+ tonode = repo.changelog.node
+ parents = repo.changelog.parentrevs
+ torev = repo.changelog.rev
+ dest = set()
+ tovisit = list(parents(rev))
+ while tovisit:
+ r = tovisit.pop()
+ succsets = obsolete.successorssets(repo, tonode(r))
+ if not succsets:
+ tovisit.extend(parents(r))
+ else:
+ # We should probably pick only one destination from split
+ # (case where '1 < len(ss)'), This could be the currently tipmost
+ # but logic is less clear when result of the split are now on
+ # multiple branches.
+ for ss in succsets:
+ for n in ss:
+ dest.add(torev(n))
+ return dest
+
+def _aspiringchildren(repo, revs):
+ """Return a list of changectx which can be stabilized on top of pctx or
+ one of its descendants. Empty list if none can be found."""
+ target = set(revs)
+ result = []
+ for r in repo.revs('unstable() - %ld', revs):
+ dest = _possibledestination(repo, r)
+ if target & dest:
+ result.append(r)
+ return result
+
+def _aspiringdescendant(repo, revs):
+ """Return a list of changectx which can be stabilized on top of pctx or
+ one of its descendants recursively. Empty list if none can be found."""
+ target = set(revs)
+ result = set(target)
+ paths = collections.defaultdict(set)
+ for r in repo.revs('unstable() - %ld', revs):
+ for d in _possibledestination(repo, r):
+ paths[d].add(r)
+
+ result = set(target)
+ tovisit = list(revs)
+ while tovisit:
+ base = tovisit.pop()
+ for unstable in paths[base]:
+ if unstable not in result:
+ tovisit.append(unstable)
+ result.add(unstable)
+ return sorted(result - target)
+
+def _solveunstable(ui, repo, orig, dryrun=False, confirm=False,
+ progresscb=None):
+ """Stabilize an unstable changeset"""
+ pctx = orig.p1()
+ if len(orig.parents()) == 2:
+ if not pctx.obsolete():
+ pctx = orig.p2() # second parent is obsolete ?
+ elif orig.p2().obsolete():
+ hint = _("Redo the merge (%s) and use `hg prune <old> "
+ "--succ <new>` to obsolete the old one") % orig.hex()[:12]
+ ui.warn(_("warning: no support for evolving merge changesets "
+ "with two obsolete parents yet\n") +
+ _("(%s)\n") % hint)
+ return False
+
+ if not pctx.obsolete():
+ ui.warn(_("cannot solve instability of %s, skipping\n") % orig)
+ return False
+ obs = pctx
+ newer = obsolete.successorssets(repo, obs.node())
+ # search of a parent which is not killed
+ while not newer or newer == [()]:
+ ui.debug("stabilize target %s is plain dead,"
+ " trying to stabilize on its parent\n" %
+ obs)
+ obs = obs.parents()[0]
+ newer = obsolete.successorssets(repo, obs.node())
+ if len(newer) > 1:
+ msg = _("skipping %s: divergent rewriting. can't choose "
+ "destination\n") % obs
+ ui.write_err(msg)
+ return 2
+ targets = newer[0]
+ assert targets
+ if len(targets) > 1:
+ # split target, figure out which one to pick, are they all in line?
+ targetrevs = [repo[r].rev() for r in targets]
+ roots = repo.revs('roots(%ld)', targetrevs)
+ heads = repo.revs('heads(%ld)', targetrevs)
+ if len(roots) > 1 or len(heads) > 1:
+ msg = "cannot solve split accross two branches\n"
+ ui.write_err(msg)
+ return 2
+ target = repo[heads.first()]
+ else:
+ target = targets[0]
+ displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
+ target = repo[target]
+ if not ui.quiet or confirm:
+ repo.ui.write(_('move:'))
+ displayer.show(orig)
+ repo.ui.write(_('atop:'))
+ displayer.show(target)
+ if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
+ raise error.Abort(_('evolve aborted by user'))
+ if progresscb: progresscb()
+ todo = 'hg rebase -r %s -d %s\n' % (orig, target)
+ if dryrun:
+ repo.ui.write(todo)
+ else:
+ repo.ui.note(todo)
+ if progresscb: progresscb()
+ keepbranch = orig.p1().branch() != orig.branch()
+ try:
+ relocate(repo, orig, target, pctx, keepbranch)
+ except MergeFailure:
+ _evolvestatewrite(repo, {'current': orig.node()})
+ repo.ui.write_err(_('evolve failed!\n'))
+ repo.ui.write_err(
+ _("fix conflict and run 'hg evolve --continue'"
+ " or use 'hg update -C .' to abort\n"))
+ raise
+
+def _solvebumped(ui, repo, bumped, dryrun=False, confirm=False,
+ progresscb=None):
+ """Stabilize a bumped changeset"""
+ repo = repo.unfiltered()
+ bumped = repo[bumped.rev()]
+ # For now we deny bumped merge
+ if len(bumped.parents()) > 1:
+ msg = _('skipping %s : we do not handle merge yet\n') % bumped
+ ui.write_err(msg)
+ return 2
+ prec = repo.set('last(allprecursors(%d) and public())', bumped).next()
+ # For now we deny target merge
+ if len(prec.parents()) > 1:
+ msg = _('skipping: %s: public version is a merge, '
+ 'this is not handled yet\n') % prec
+ ui.write_err(msg)
+ return 2
+
+ displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
+ if not ui.quiet or confirm:
+ repo.ui.write(_('recreate:'))
+ displayer.show(bumped)
+ repo.ui.write(_('atop:'))
+ displayer.show(prec)
+ if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
+ raise error.Abort(_('evolve aborted by user'))
+ if dryrun:
+ todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1())
+ repo.ui.write(todo)
+ repo.ui.write(('hg update %s;\n' % prec))
+ repo.ui.write(('hg revert --all --rev %s;\n' % bumped))
+ repo.ui.write(('hg commit --msg "bumped update to %s"'))
+ return 0
+ if progresscb: progresscb()
+ newid = tmpctx = None
+ tmpctx = bumped
+ # Basic check for common parent. Far too complicated and fragile
+ tr = repo.currenttransaction()
+ assert tr is not None
+ bmupdate = _bookmarksupdater(repo, bumped.node(), tr)
+ if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)):
+ # Need to rebase the changeset at the right place
+ repo.ui.status(
+ _('rebasing to destination parent: %s\n') % prec.p1())
+ try:
+ tmpid = relocate(repo, bumped, prec.p1())
+ if tmpid is not None:
+ tmpctx = repo[tmpid]
+ obsolete.createmarkers(repo, [(bumped, (tmpctx,))])
+ except MergeFailure:
+ repo.opener.write('graftstate', bumped.hex() + '\n')
+ repo.ui.write_err(_('evolution failed!\n'))
+ repo.ui.write_err(
+ _("fix conflict and run 'hg evolve --continue'\n"))
+ raise
+ # Create the new commit context
+ repo.ui.status(_('computing new diff\n'))
+ files = set()
+ copied = copies.pathcopies(prec, bumped)
+ precmanifest = prec.manifest().copy()
+ # 3.3.2 needs a list.
+ # future 3.4 don't detect the size change during iteration
+ # this is fishy
+ for key, val in list(bumped.manifest().iteritems()):
+ precvalue = precmanifest.get(key, None)
+ if precvalue is not None:
+ del precmanifest[key]
+ if precvalue != val:
+ files.add(key)
+ files.update(precmanifest) # add missing files
+ # commit it
+ if files: # something to commit!
+ def filectxfn(repo, ctx, path):
+ if path in bumped:
+ fctx = bumped[path]
+ flags = fctx.flags()
+ mctx = memfilectx(repo, fctx.path(), fctx.data(),
+ islink='l' in flags,
+ isexec='x' in flags,
+ copied=copied.get(path))
+ return mctx
+ return None
+ text = 'bumped update to %s:\n\n' % prec
+ text += bumped.description()
+
+ new = context.memctx(repo,
+ parents=[prec.node(), node.nullid],
+ text=text,
+ files=files,
+ filectxfn=filectxfn,
+ user=bumped.user(),
+ date=bumped.date(),
+ extra=bumped.extra())
+
+ newid = repo.commitctx(new)
+ if newid is None:
+ obsolete.createmarkers(repo, [(tmpctx, ())])
+ newid = prec.node()
+ else:
+ phases.retractboundary(repo, tr, bumped.phase(), [newid])
+ obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))],
+ flag=obsolete.bumpedfix)
+ bmupdate(newid)
+ repo.ui.status(_('committed as %s\n') % node.short(newid))
+ # reroute the working copy parent to the new changeset
+ repo.dirstate.beginparentchange()
+ repo.dirstate.setparents(newid, node.nullid)
+ repo.dirstate.endparentchange()
+
+def _solvedivergent(ui, repo, divergent, dryrun=False, confirm=False,
+ progresscb=None):
+ repo = repo.unfiltered()
+ divergent = repo[divergent.rev()]
+ base, others = divergentdata(divergent)
+ if len(others) > 1:
+ othersstr = "[%s]" % (','.join([str(i) for i in others]))
+ msg = _("skipping %d:divergent with a changeset that got splitted"
+ " into multiple ones:\n"
+ "|[%s]\n"
+ "| This is not handled by automatic evolution yet\n"
+ "| You have to fallback to manual handling with commands "
+ "such as:\n"
+ "| - hg touch -D\n"
+ "| - hg prune\n"
+ "| \n"
+ "| You should contact your local evolution Guru for help.\n"
+ ) % (divergent, othersstr)
+ ui.write_err(msg)
+ return 2
+ other = others[0]
+ if len(other.parents()) > 1:
+ msg = _("skipping %s: divergent changeset can't be "
+ "a merge (yet)\n") % divergent
+ ui.write_err(msg)
+ hint = _("You have to fallback to solving this by hand...\n"
+ "| This probably means redoing the merge and using \n"
+ "| `hg prune` to kill older version.\n")
+ ui.write_err(hint)
+ return 2
+ if other.p1() not in divergent.parents():
+ msg = _("skipping %s: have a different parent than %s "
+ "(not handled yet)\n") % (divergent, other)
+ hint = _("| %(d)s, %(o)s are not based on the same changeset.\n"
+ "| With the current state of its implementation, \n"
+ "| evolve does not work in that case.\n"
+ "| rebase one of them next to the other and run \n"
+ "| this command again.\n"
+ "| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n"
+ "| - or: hg rebase --dest 'p1(%(o)s)' -r %(d)s\n"
+ ) % {'d': divergent, 'o': other}
+ ui.write_err(msg)
+ ui.write_err(hint)
+ return 2
+
+ displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
+ if not ui.quiet or confirm:
+ ui.write(_('merge:'))
+ displayer.show(divergent)
+ ui.write(_('with: '))
+ displayer.show(other)
+ ui.write(_('base: '))
+ displayer.show(base)
+ if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y':
+ raise error.Abort(_('evolve aborted by user'))
+ if dryrun:
+ ui.write(('hg update -c %s &&\n' % divergent))
+ ui.write(('hg merge %s &&\n' % other))
+ ui.write(('hg commit -m "auto merge resolving conflict between '
+ '%s and %s"&&\n' % (divergent, other)))
+ ui.write(('hg up -C %s &&\n' % base))
+ ui.write(('hg revert --all --rev tip &&\n'))
+ ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n'
+ % divergent))
+ return
+ if divergent not in repo[None].parents():
+ repo.ui.status(_('updating to "local" conflict\n'))
+ hg.update(repo, divergent.rev())
+ repo.ui.note(_('merging divergent changeset\n'))
+ if progresscb: progresscb()
+ try:
+ stats = merge.update(repo,
+ other.node(),
+ branchmerge=True,
+ force=False,
+ ancestor=base.node(),
+ mergeancestor=True)
+ except TypeError:
+ # Mercurial < 43c00ca887d1 (3.7)
+ stats = merge.update(repo,
+ other.node(),
+ branchmerge=True,
+ force=False,
+ partial=None,
+ ancestor=base.node(),
+ mergeancestor=True)
+
+ hg._showstats(repo, stats)
+ if stats[3]:
+ repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
+ "or 'hg update -C .' to abort\n"))
+ if stats[3] > 0:
+ raise error.Abort('merge conflict between several amendments '
+ '(this is not automated yet)',
+ hint="""/!\ You can try:
+/!\ * manual merge + resolve => new cset X
+/!\ * hg up to the parent of the amended changeset (which are named W and Z)
+/!\ * hg revert --all -r X
+/!\ * hg ci -m "same message as the amended changeset" => new cset Y
+/!\ * hg prune -n Y W Z
+""")
+ if progresscb: progresscb()
+ emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit')
+ tr = repo.currenttransaction()
+ assert tr is not None
+ try:
+ repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve')
+ repo.dirstate.beginparentchange()
+ repo.dirstate.setparents(divergent.node(), node.nullid)
+ repo.dirstate.endparentchange()
+ oldlen = len(repo)
+ amend(ui, repo, message='', logfile='')
+ if oldlen == len(repo):
+ new = divergent
+ # no changes
+ else:
+ new = repo['.']
+ obsolete.createmarkers(repo, [(other, (new,))])
+ phases.retractboundary(repo, tr, other.phase(), [new.node()])
+ finally:
+ repo.ui.restoreconfig(emtpycommitallowed)
+
+def divergentdata(ctx):
+ """return base, other part of a conflict
+
+ This only return the first one.
+
+ XXX this woobly function won't survive XXX
+ """
+ repo = ctx._repo.unfiltered()
+ for base in repo.set('reverse(allprecursors(%d))', ctx):
+ newer = obsolete.successorssets(ctx._repo, base.node())
+ # drop filter and solution including the original ctx
+ newer = [n for n in newer if n and ctx.node() not in n]
+ if newer:
+ return base, tuple(ctx._repo[o] for o in newer[0])
+ raise error.Abort("base of divergent changeset %s not found" % ctx,
+ hint='this case is not yet handled')
+
+
+
+shorttemplate = '[{rev}] {desc|firstline}\n'
+
+@command('^previous',
+ [('B', 'move-bookmark', False,
+ _('move active bookmark after update')),
+ ('', 'merge', False, _('bring uncommitted change along')),
+ ('', 'no-topic', False, _('ignore topic and move topologically')),
+ ('n', 'dry-run', False,
+ _('do not perform actions, just print what would be done'))],
+ '[OPTION]...')
+def cmdprevious(ui, repo, **opts):
+ """update to parent revision
+
+ Displays the summary line of the destination for clarity."""
+ wlock = None
+ dryrunopt = opts['dry_run']
+ if not dryrunopt:
+ wlock = repo.wlock()
+ try:
+ wkctx = repo[None]
+ wparents = wkctx.parents()
+ if len(wparents) != 1:
+ raise error.Abort('merge in progress')
+ if not opts['merge']:
+ try:
+ cmdutil.bailifchanged(repo)
+ except error.Abort as exc:
+ exc.hint = _('do you want --merge?')
+ raise
+
+ parents = wparents[0].parents()
+ topic = getattr(repo, 'currenttopic', '')
+ if topic and not opts.get("no_topic", False):
+ parents = [ctx for ctx in parents if ctx.topic() == topic]
+ displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
+ if not parents:
+ ui.warn(_('no parent in topic "%s"\n') % topic)
+ ui.warn(_('(do you want --no-topic)\n'))
+ elif len(parents) == 1:
+ p = parents[0]
+ bm = bmactive(repo)
+ shouldmove = opts.get('move_bookmark') and bm is not None
+ if dryrunopt:
+ ui.write(('hg update %s;\n' % p.rev()))
+ if shouldmove:
+ ui.write(('hg bookmark %s -r %s;\n' % (bm, p.rev())))
+ else:
+ ret = hg.update(repo, p.rev())
+ if not ret:
+ tr = lock = None
+ try:
+ lock = repo.lock()
+ tr = repo.transaction('previous')
+ if shouldmove:
+ repo._bookmarks[bm] = p.node()
+ repo._bookmarks.recordchange(tr)
+ else:
+ bmdeactivate(repo)
+ tr.close()
+ finally:
+ lockmod.release(tr, lock)
+
+ displayer.show(p)
+ return 0
+ else:
+ for p in parents:
+ displayer.show(p)
+ ui.warn(_('multiple parents, explicitly update to one\n'))
+ return 1
+ finally:
+ lockmod.release(wlock)
+
+@command('^next',
+ [('B', 'move-bookmark', False,
+ _('move active bookmark after update')),
+ ('', 'merge', False, _('bring uncommitted change along')),
+ ('', 'evolve', False, _('evolve the next changeset if necessary')),
+ ('', 'no-topic', False, _('ignore topic and move topologically')),
+ ('n', 'dry-run', False,
+ _('do not perform actions, just print what would be done'))],
+ '[OPTION]...')
+def cmdnext(ui, repo, **opts):
+ """update to next child revision
+
+ Use the ``--evolve`` flag to evolve unstable children on demand.
+
+ Displays the summary line of the destination for clarity.
+ """
+ wlock = None
+ dryrunopt = opts['dry_run']
+ if not dryrunopt:
+ wlock = repo.wlock()
+ try:
+ wkctx = repo[None]
+ wparents = wkctx.parents()
+ if len(wparents) != 1:
+ raise error.Abort('merge in progress')
+ if not opts['merge']:
+ try:
+ cmdutil.bailifchanged(repo)
+ except error.Abort as exc:
+ exc.hint = _('do you want --merge?')
+ raise
+
+ children = [ctx for ctx in wparents[0].children() if not ctx.obsolete()]
+ topic = getattr(repo, 'currenttopic', '')
+ filtered = []
+ if topic and not opts.get("no_topic", False):
+ filtered = [ctx for ctx in children if ctx.topic() != topic]
+ # XXX N-square membership on children
+ children = [ctx for ctx in children if ctx not in filtered]
+ displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
+ if len(children) == 1:
+ c = children[0]
+ bm = bmactive(repo)
+ shouldmove = opts.get('move_bookmark') and bm is not None
+ if dryrunopt:
+ ui.write(('hg update %s;\n' % c.rev()))
+ if shouldmove:
+ ui.write(('hg bookmark %s -r %s;\n' % (bm, c.rev())))
+ else:
+ ret = hg.update(repo, c.rev())
+ if not ret:
+ lock = tr = None
+ try:
+ lock = repo.lock()
+ tr = repo.transaction('next')
+ if shouldmove:
+ repo._bookmarks[bm] = c.node()
+ repo._bookmarks.recordchange(tr)
+ else:
+ bmdeactivate(repo)
+ tr.close()
+ finally:
+ lockmod.release(tr, lock)
+ displayer.show(c)
+ result = 0
+ elif children:
+ ui.warn(_("ambigious next changeset:\n"))
+ for c in children:
+ displayer.show(c)
+ ui.warn(_('explicitly update to one of them\n'))
+ result = 1
+ else:
+ aspchildren = _aspiringchildren(repo, [repo['.'].rev()])
+ if topic:
+ filtered.extend(repo[c] for c in children
+ if repo[c].topic() != topic)
+ # XXX N-square membership on children
+ aspchildren = [ctx for ctx in aspchildren if ctx not in filtered]
+ if not opts['evolve'] or not aspchildren:
+ if filtered:
+ ui.warn(_('no children on topic "%s"\n') % topic)
+ ui.warn(_('do you want --no-topic\n'))
+ else:
+ ui.warn(_('no children\n'))
+ if aspchildren:
+ msg = _('(%i unstable changesets to be evolved here, '
+ 'do you want --evolve?)\n')
+ ui.warn(msg % len(aspchildren))
+ result = 1
+ elif 1 < len(aspchildren):
+ ui.warn(_("ambigious next (unstable) changeset:\n"))
+ for c in aspchildren:
+ displayer.show(repo[c])
+ ui.warn(_("(run 'hg evolve --rev REV' on one of them)\n"))
+ return 1
+ else:
+ cmdutil.bailifchanged(repo)
+ result = _solveone(ui, repo, repo[aspchildren[0]], dryrunopt,
+ False, lambda:None, category='unstable')
+ if not result:
+ ui.status(_('working directory now at %s\n') % repo['.'])
+ return result
+ return 1
+ return result
+ finally:
+ lockmod.release(wlock)
+
+def _reachablefrombookmark(repo, revs, bookmarks):
+ """filter revisions and bookmarks reachable from the given bookmark
+ yoinked from mq.py
+ """
+ repomarks = repo._bookmarks
+ if not bookmarks.issubset(repomarks):
+ raise error.Abort(_("bookmark '%s' not found") %
+ ','.join(sorted(bookmarks - set(repomarks.keys()))))
+
+ # If the requested bookmark is not the only one pointing to a
+ # a revision we have to only delete the bookmark and not strip
+ # anything. revsets cannot detect that case.
+ nodetobookmarks = {}
+ for mark, node in repomarks.iteritems():
+ nodetobookmarks.setdefault(node, []).append(mark)
+ for marks in nodetobookmarks.values():
+ if bookmarks.issuperset(marks):
+ if util.safehasattr(repair, 'stripbmrevset'):
+ rsrevs = repair.stripbmrevset(repo, marks[0])
+ else:
+ rsrevs = repo.revs("ancestors(bookmark(%s)) - "
+ "ancestors(head() and not bookmark(%s)) - "
+ "ancestors(bookmark() and not bookmark(%s)) - "
+ "obsolete()",
+ marks[0], marks[0], marks[0])
+ revs = set(revs)
+ revs.update(set(rsrevs))
+ revs = sorted(revs)
+ return repomarks, revs
+
+def _deletebookmark(repo, repomarks, bookmarks):
+ wlock = lock = tr = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ tr = repo.transaction('prune')
+ for bookmark in bookmarks:
+ del repomarks[bookmark]
+ repomarks.recordchange(tr)
+ tr.close()
+ for bookmark in sorted(bookmarks):
+ repo.ui.write(_("bookmark '%s' deleted\n") % bookmark)
+ finally:
+ lockmod.release(tr, lock, wlock)
+
+
+
+def _getmetadata(**opts):
+ metadata = {}
+ date = opts.get('date')
+ user = opts.get('user')
+ if date:
+ metadata['date'] = '%i %i' % util.parsedate(date)
+ if user:
+ metadata['user'] = user
+ return metadata
+
+
+@command('^prune|obsolete',
+ [('n', 'new', [], _("successor changeset (DEPRECATED)")),
+ ('s', 'succ', [], _("successor changeset")),
+ ('r', 'rev', [], _("revisions to prune")),
+ ('k', 'keep', None, _("does not modify working copy during prune")),
+ ('', 'biject', False, _("do a 1-1 map between rev and successor ranges")),
+ ('', 'fold', False,
+ _("record a fold (multiple precursors, one successors)")),
+ ('', 'split', False,
+ _("record a split (on precursor, multiple successors)")),
+ ('B', 'bookmark', [], _("remove revs only reachable from given"
+ " bookmark"))] + metadataopts,
+ _('[OPTION] [-r] REV...'))
+ # -U --noupdate option to prevent wc update and or bookmarks update ?
+def cmdprune(ui, repo, *revs, **opts):
+ """hide changesets by marking them obsolete
+
+ Pruned changesets are obsolete with no successors. If they also have no
+ descendants, they are hidden (invisible to all commands).
+
+ Non-obsolete descendants of pruned changesets become "unstable". Use :hg:`evolve`
+ to handle this situation.
+
+ When you prune the parent of your working copy, Mercurial updates the working
+ copy to a non-obsolete parent.
+
+ You can use ``--succ`` to tell Mercurial that a newer version (successor) of the
+ pruned changeset exists. Mercurial records successor revisions in obsolescence
+ markers.
+
+ You can use the ``--biject`` option to specify a 1-1 mapping (bijection) between
+ revisions to pruned (precursor) and successor changesets. This option may be
+ removed in a future release (with the functionality provided automatically).
+
+ If you specify multiple revisions in ``--succ``, you are recording a "split" and
+ must acknowledge it by passing ``--split``. Similarly, when you prune multiple
+ changesets with a single successor, you must pass the ``--fold`` option.
+ """
+ revs = scmutil.revrange(repo, list(revs) + opts.get('rev'))
+ succs = opts['new'] + opts['succ']
+ bookmarks = set(opts.get('bookmark'))
+ metadata = _getmetadata(**opts)
+ biject = opts.get('biject')
+ fold = opts.get('fold')
+ split = opts.get('split')
+
+ options = [o for o in ('biject', 'fold', 'split') if opts.get(o)]
+ if 1 < len(options):
+ raise error.Abort(_("can only specify one of %s") % ', '.join(options))
+
+ if bookmarks:
+ repomarks, revs = _reachablefrombookmark(repo, revs, bookmarks)
+ if not revs:
+ # no revisions to prune - delete bookmark immediately
+ _deletebookmark(repo, repomarks, bookmarks)
+
+ if not revs:
+ raise error.Abort(_('nothing to prune'))
+
+ wlock = lock = tr = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ tr = repo.transaction('prune')
+ # defines pruned changesets
+ precs = []
+ revs.sort()
+ for p in revs:
+ cp = repo[p]
+ if not cp.mutable():
+ # note: createmarkers() would have raised something anyway
+ raise error.Abort('cannot prune immutable changeset: %s' % cp,
+ hint="see 'hg help phases' for details")
+ precs.append(cp)
+ if not precs:
+ raise error.Abort('nothing to prune')
+
+ if _disallowednewunstable(repo, revs):
+ raise error.Abort(_("cannot prune in the middle of a stack"),
+ hint = _("new unstable changesets are not allowed"))
+
+ # defines successors changesets
+ sucs = scmutil.revrange(repo, succs)
+ sucs.sort()
+ sucs = tuple(repo[n] for n in sucs)
+ if not biject and len(sucs) > 1 and len(precs) > 1:
+ msg = "Can't use multiple successors for multiple precursors"
+ hint = _("use --biject to mark a series as a replacement"
+ " for another")
+ raise error.Abort(msg, hint=hint)
+ elif biject and len(sucs) != len(precs):
+ msg = "Can't use %d successors for %d precursors" \
+ % (len(sucs), len(precs))
+ raise error.Abort(msg)
+ elif (len(precs) == 1 and len(sucs) > 1) and not split:
+ msg = "please add --split if you want to do a split"
+ raise error.Abort(msg)
+ elif len(sucs) == 1 and len(precs) > 1 and not fold:
+ msg = "please add --fold if you want to do a fold"
+ raise error.Abort(msg)
+ elif biject:
+ relations = [(p, (s,)) for p, s in zip(precs, sucs)]
+ else:
+ relations = [(p, sucs) for p in precs]
+
+ wdp = repo['.']
+
+ if len(sucs) == 1 and len(precs) == 1 and wdp in precs:
+ # '.' killed, so update to the successor
+ newnode = sucs[0]
+ else:
+ # update to an unkilled parent
+ newnode = wdp
+
+ while newnode in precs or newnode.obsolete():
+ newnode = newnode.parents()[0]
+
+
+ if newnode.node() != wdp.node():
+ if opts.get('keep', False):
+ # This is largely the same as the implementation in
+ # strip.stripcmd(). We might want to refactor this somewhere
+ # common at some point.
+
+ # only reset the dirstate for files that would actually change
+ # between the working context and uctx
+ descendantrevs = repo.revs("%d::." % newnode.rev())
+ changedfiles = []
+ for rev in descendantrevs:
+ # blindly reset the files, regardless of what actually
+ # changed
+ changedfiles.extend(repo[rev].files())
+
+ # reset files that only changed in the dirstate too
+ dirstate = repo.dirstate
+ dirchanges = [f for f in dirstate if dirstate[f] != 'n']
+ changedfiles.extend(dirchanges)
+ repo.dirstate.rebuild(newnode.node(), newnode.manifest(),
+ changedfiles)
+ writedirstate(dirstate, tr)
+ else:
+ bookactive = bmactive(repo)
+ # Active bookmark that we don't want to delete (with -B option)
+ # we deactivate and move it before the update and reactivate it
+ # after
+ movebookmark = bookactive and not bookmarks
+ if movebookmark:
+ bmdeactivate(repo)
+ repo._bookmarks[bookactive] = newnode.node()
+ repo._bookmarks.recordchange(tr)
+ commands.update(ui, repo, newnode.rev())
+ ui.status(_('working directory now at %s\n') % newnode)
+ if movebookmark:
+ bmactivate(repo, bookactive)
+
+ # update bookmarks
+ if bookmarks:
+ _deletebookmark(repo, repomarks, bookmarks)
+
+ # create markers
+ obsolete.createmarkers(repo, relations, metadata=metadata)
+
+ # informs that changeset have been pruned
+ ui.status(_('%i changesets pruned\n') % len(precs))
+
+ for ctx in repo.unfiltered().set('bookmark() and %ld', precs):
+ # used to be:
+ #
+ # ldest = list(repo.set('max((::%d) - obsolete())', ctx))
+ # if ldest:
+ # c = ldest[0]
+ #
+ # but then revset took a lazy arrow in the knee and became much
+ # slower. The new forms makes as much sense and a much faster.
+ for dest in ctx.ancestors():
+ if not dest.obsolete():
+ updatebookmarks = _bookmarksupdater(repo, ctx.node(), tr)
+ updatebookmarks(dest.node())
+ break
+
+ tr.close()
+ finally:
+ lockmod.release(tr, lock, wlock)
+
+@command('amend|refresh',
+ [('A', 'addremove', None,
+ _('mark new/missing files as added/removed before committing')),
+ ('e', 'edit', False, _('invoke editor on commit messages')),
+ ('', 'close-branch', None,
+ _('mark a branch as closed, hiding it from the branch list')),
+ ('s', 'secret', None, _('use the secret phase for committing')),
+ ] + walkopts + commitopts + commitopts2 + commitopts3 + interactiveopt,
+ _('[OPTION]... [FILE]...'))
+def amend(ui, repo, *pats, **opts):
+ """combine a changeset with updates and replace it with a new one
+
+ Commits a new changeset incorporating both the changes to the given files
+ and all the changes from the current parent changeset into the repository.
+
+ See :hg:`commit` for details about committing changes.
+
+ If you don't specify -m, the parent's message will be reused.
+
+ Behind the scenes, Mercurial first commits the update as a regular child
+ of the current parent. Then it creates a new commit on the parent's parents
+ with the updated contents. Then it changes the working copy parent to this
+ new combined changeset. Finally, the old changeset and its update are hidden
+ from :hg:`log` (unless you use --hidden with log).
+
+ Returns 0 on success, 1 if nothing changed.
+ """
+ opts = opts.copy()
+ edit = opts.pop('edit', False)
+ log = opts.get('logfile')
+ opts['amend'] = True
+ if not (edit or opts['message'] or log):
+ opts['message'] = repo['.'].description()
+ _resolveoptions(ui, opts)
+ _alias, commitcmd = cmdutil.findcmd('commit', commands.table)
+ return commitcmd[0](ui, repo, *pats, **opts)
+
+
+def _touchedbetween(repo, source, dest, match=None):
+ touched = set()
+ for files in repo.status(source, dest, match=match)[:3]:
+ touched.update(files)
+ return touched
+
+def _commitfiltered(repo, ctx, match, target=None):
+ """Recommit ctx with changed files not in match. Return the new
+ node identifier, or None if nothing changed.
+ """
+ base = ctx.p1()
+ if target is None:
+ target = base
+ # ctx
+ initialfiles = _touchedbetween(repo, base, ctx)
+ if base == target:
+ affected = set(f for f in initialfiles if match(f))
+ newcontent = set()
+ else:
+ affected = _touchedbetween(repo, target, ctx, match=match)
+ newcontent = _touchedbetween(repo, target, base, match=match)
+ # The commit touchs all existing files
+ # + all file that needs a new content
+ # - the file affected bny uncommit with the same content than base.
+ files = (initialfiles - affected) | newcontent
+ if not newcontent and files == initialfiles:
+ return None
+
+ # Filter copies
+ copied = copies.pathcopies(target, ctx)
+ copied = dict((dst, src) for dst, src in copied.iteritems()
+ if dst in files)
+ def filectxfn(repo, memctx, path, contentctx=ctx, redirect=newcontent):
+ if path in redirect:
+ return filectxfn(repo, memctx, path, contentctx=target, redirect=())
+ if path not in contentctx:
+ return None
+ fctx = contentctx[path]
+ flags = fctx.flags()
+ mctx = memfilectx(repo, fctx.path(), fctx.data(),
+ islink='l' in flags,
+ isexec='x' in flags,
+ copied=copied.get(path))
+ return mctx
+
+ new = context.memctx(repo,
+ parents=[base.node(), node.nullid],
+ text=ctx.description(),
+ files=files,
+ filectxfn=filectxfn,
+ user=ctx.user(),
+ date=ctx.date(),
+ extra=ctx.extra())
+ # commitctx always create a new revision, no need to check
+ newid = repo.commitctx(new)
+ return newid
+
+def _uncommitdirstate(repo, oldctx, match):
+ """Fix the dirstate after switching the working directory from
+ oldctx to a copy of oldctx not containing changed files matched by
+ match.
+ """
+ ctx = repo['.']
+ ds = repo.dirstate
+ copies = dict(ds.copies())
+ m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3]
+ for f in m:
+ if ds[f] == 'r':
+ # modified + removed -> removed
+ continue
+ ds.normallookup(f)
+
+ for f in a:
+ if ds[f] == 'r':
+ # added + removed -> unknown
+ ds.drop(f)
+ elif ds[f] != 'a':
+ ds.add(f)
+
+ for f in r:
+ if ds[f] == 'a':
+ # removed + added -> normal
+ ds.normallookup(f)
+ elif ds[f] != 'r':
+ ds.remove(f)
+
+ # Merge old parent and old working dir copies
+ oldcopies = {}
+ for f in (m + a):
+ src = oldctx[f].renamed()
+ if src:
+ oldcopies[f] = src[0]
+ oldcopies.update(copies)
+ copies = dict((dst, oldcopies.get(src, src))
+ for dst, src in oldcopies.iteritems())
+ # Adjust the dirstate copies
+ for dst, src in copies.iteritems():
+ if (src not in ctx or dst in ctx or ds[dst] != 'a'):
+ src = None
+ ds.copy(src, dst)
+
+@command('^uncommit',
+ [('a', 'all', None, _('uncommit all changes when no arguments given')),
+ ('r', 'rev', '', _('revert commit content to REV instead')),
+ ] + commands.walkopts,
+ _('[OPTION]... [NAME]'))
+def uncommit(ui, repo, *pats, **opts):
+ """move changes from parent revision to working directory
+
+ Changes to selected files in the checked out revision appear again as
+ uncommitted changed in the working directory. A new revision
+ without the selected changes is created, becomes the checked out
+ revision, and obsoletes the previous one.
+
+ The --include option specifies patterns to uncommit.
+ The --exclude option specifies patterns to keep in the commit.
+
+ The --rev argument let you change the commit file to a content of another
+ revision. It still does not change the content of your file in the working
+ directory.
+
+ Return 0 if changed files are uncommitted.
+ """
+
+ wlock = lock = tr = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ wctx = repo[None]
+ if len(wctx.parents()) <= 0:
+ raise error.Abort(_("cannot uncommit null changeset"))
+ if len(wctx.parents()) > 1:
+ raise error.Abort(_("cannot uncommit while merging"))
+ old = repo['.']
+ if old.phase() == phases.public:
+ raise error.Abort(_("cannot rewrite immutable changeset"))
+ if len(old.parents()) > 1:
+ raise error.Abort(_("cannot uncommit merge changeset"))
+ oldphase = old.phase()
+
+
+ rev = None
+ if opts.get('rev'):
+ rev = scmutil.revsingle(repo, opts.get('rev'))
+ ctx = repo[None]
+ if ctx.p1() == rev or ctx.p2() == rev:
+ raise error.Abort(_("cannot uncommit to parent changeset"))
+
+ onahead = old.rev() in repo.changelog.headrevs()
+ disallowunstable = not obsolete.isenabled(repo,
+ obsolete.allowunstableopt)
+ if disallowunstable and not onahead:
+ raise error.Abort(_("cannot uncommit in the middle of a stack"))
+
+ # Recommit the filtered changeset
+ tr = repo.transaction('uncommit')
+ updatebookmarks = _bookmarksupdater(repo, old.node(), tr)
+ newid = None
+ includeorexclude = opts.get('include') or opts.get('exclude')
+ if (pats or includeorexclude or opts.get('all')):
+ match = scmutil.match(old, pats, opts)
+ newid = _commitfiltered(repo, old, match, target=rev)
+ if newid is None:
+ raise error.Abort(_('nothing to uncommit'),
+ hint=_("use --all to uncommit all files"))
+ # Move local changes on filtered changeset
+ obsolete.createmarkers(repo, [(old, (repo[newid],))])
+ phases.retractboundary(repo, tr, oldphase, [newid])
+ repo.dirstate.beginparentchange()
+ repo.dirstate.setparents(newid, node.nullid)
+ _uncommitdirstate(repo, old, match)
+ repo.dirstate.endparentchange()
+ updatebookmarks(newid)
+ if not repo[newid].files():
+ ui.warn(_("new changeset is empty\n"))
+ ui.status(_("(use 'hg prune .' to remove it)\n"))
+ tr.close()
+ finally:
+ lockmod.release(tr, lock, wlock)
+
+@eh.wrapcommand('commit')
+def commitwrapper(orig, ui, repo, *arg, **kwargs):
+ tr = None
+ if kwargs.get('amend', False):
+ wlock = lock = None
+ else:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ try:
+ obsoleted = kwargs.get('obsolete', [])
+ if obsoleted:
+ obsoleted = repo.set('%lr', obsoleted)
+ result = orig(ui, repo, *arg, **kwargs)
+ if not result: # commit succeeded
+ new = repo['-1']
+ oldbookmarks = []
+ markers = []
+ for old in obsoleted:
+ oldbookmarks.extend(repo.nodebookmarks(old.node()))
+ markers.append((old, (new,)))
+ if markers:
+ obsolete.createmarkers(repo, markers)
+ for book in oldbookmarks:
+ repo._bookmarks[book] = new.node()
+ if oldbookmarks:
+ if not wlock:
+ wlock = repo.wlock()
+ if not lock:
+ lock = repo.lock()
+ tr = repo.transaction('commit')
+ repo._bookmarks.recordchange(tr)
+ tr.close()
+ return result
+ finally:
+ lockmod.release(tr, lock, wlock)
+
+@command('^split',
+ [('r', 'rev', [], _("revision to split")),
+ ] + commitopts + commitopts2,
+ _('hg split [OPTION]... [-r] REV'))
+def cmdsplit(ui, repo, *revs, **opts):
+ """split a changeset into smaller changesets
+
+ By default, split the current revision by prompting for all its hunks to be
+ redistributed into new changesets.
+
+ Use --rev to split a given changeset instead.
+ """
+ tr = wlock = lock = None
+ newcommits = []
+
+ revarg = (list(revs) + opts.get('rev')) or ['.']
+ if len(revarg) != 1:
+ msg = _("more than one revset is given")
+ hnt = _("use either `hg split <rs>` or `hg split --rev <rs>`, not both")
+ raise error.Abort(msg, hint=hnt)
+
+ rev = scmutil.revsingle(repo, revarg[0])
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ cmdutil.bailifchanged(repo)
+ tr = repo.transaction('split')
+ ctx = repo[rev]
+ r = ctx.rev()
+ disallowunstable = not obsolete.isenabled(repo,
+ obsolete.allowunstableopt)
+ if disallowunstable:
+ # XXX We should check head revs
+ if repo.revs("(%d::) - %d", rev, rev):
+ raise error.Abort(_("cannot split commit: %s not a head") % ctx)
+
+ if len(ctx.parents()) > 1:
+ raise error.Abort(_("cannot split merge commits"))
+ prev = ctx.p1()
+ bmupdate = _bookmarksupdater(repo, ctx.node(), tr)
+ bookactive = bmactive(repo)
+ if bookactive is not None:
+ repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo))
+ bmdeactivate(repo)
+ hg.update(repo, prev)
+
+ commands.revert(ui, repo, rev=r, all=True)
+ def haschanges():
+ modified, added, removed, deleted = repo.status()[:4]
+ return modified or added or removed or deleted
+ msg = ("HG: This is the original pre-split commit message. "
+ "Edit it as appropriate.\n\n")
+ msg += ctx.description()
+ opts['message'] = msg
+ opts['edit'] = True
+ while haschanges():
+ pats = ()
+ cmdutil.dorecord(ui, repo, commands.commit, 'commit', False,
+ cmdutil.recordfilter, *pats, **opts)
+ # TODO: Does no seem like the best way to do this
+ # We should make dorecord return the newly created commit
+ newcommits.append(repo['.'])
+ if haschanges():
+ if ui.prompt('Done splitting? [yN]', default='n') == 'y':
+ commands.commit(ui, repo, **opts)
+ newcommits.append(repo['.'])
+ break
+ else:
+ ui.status(_("no more change to split\n"))
+
+ if newcommits:
+ tip = repo[newcommits[-1]]
+ bmupdate(tip.node())
+ if bookactive is not None:
+ bmactivate(repo, bookactive)
+ obsolete.createmarkers(repo, [(repo[r], newcommits)])
+ tr.close()
+ finally:
+ lockmod.release(tr, lock, wlock)
+
+
+@eh.wrapcommand('strip', extension='strip', opts=[
+ ('', 'bundle', None, _("delete the commit entirely and move it to a "
+ "backup bundle")),
+ ])
+def stripwrapper(orig, ui, repo, *revs, **kwargs):
+ if (not ui.configbool('experimental', 'prunestrip') or
+ kwargs.get('bundle', False)):
+ return orig(ui, repo, *revs, **kwargs)
+
+ if kwargs.get('force'):
+ ui.warn(_("warning: --force has no effect during strip with evolve "
+ "enabled\n"))
+ if kwargs.get('no_backup', False):
+ ui.warn(_("warning: --no-backup has no effect during strips with "
+ "evolve enabled\n"))
+
+ revs = list(revs) + kwargs.pop('rev', [])
+ revs = set(scmutil.revrange(repo, revs))
+ revs = repo.revs("(%ld)::", revs)
+ kwargs['rev'] = []
+ kwargs['new'] = []
+ kwargs['succ'] = []
+ kwargs['biject'] = False
+ return cmdprune(ui, repo, *revs, **kwargs)
+
+@command('^touch',
+ [('r', 'rev', [], 'revision to update'),
+ ('D', 'duplicate', False,
+ 'do not mark the new revision as successor of the old one'),
+ ('A', 'allowdivergence', False,
+ 'mark the new revision as successor of the old one potentially creating '
+ 'divergence')],
+ # allow to choose the seed ?
+ _('[-r] revs'))
+def touch(ui, repo, *revs, **opts):
+ """create successors that are identical to their predecessors except
+ for the changeset ID
+
+ This is used to "resurrect" changesets
+ """
+ duplicate = opts['duplicate']
+ allowdivergence = opts['allowdivergence']
+ revs = list(revs)
+ revs.extend(opts['rev'])
+ if not revs:
+ revs = ['.']
+ revs = scmutil.revrange(repo, revs)
+ if not revs:
+ ui.write_err('no revision to touch\n')
+ return 1
+ if not duplicate and repo.revs('public() and %ld', revs):
+ raise error.Abort("can't touch public revision")
+ displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
+ wlock = lock = tr = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ tr = repo.transaction('touch')
+ revs.sort() # ensure parent are run first
+ newmapping = {}
+ for r in revs:
+ ctx = repo[r]
+ extra = ctx.extra().copy()
+ extra['__touch-noise__'] = random.randint(0, 0xffffffff)
+ # search for touched parent
+ p1 = ctx.p1().node()
+ p2 = ctx.p2().node()
+ p1 = newmapping.get(p1, p1)
+ p2 = newmapping.get(p2, p2)
+
+ if not (duplicate or allowdivergence):
+ # The user hasn't yet decided what to do with the revived
+ # cset, let's ask
+ sset = obsolete.successorssets(repo, ctx.node())
+ nodivergencerisk = len(sset) == 0 or (
+ len(sset) == 1 and
+ len(sset[0]) == 1 and
+ repo[sset[0][0]].rev() == ctx.rev()
+ )
+ if nodivergencerisk:
+ duplicate = False
+ else:
+ displayer.show(ctx)
+ index = ui.promptchoice(
+ _("reviving this changeset will create divergence"
+ " unless you make a duplicate.\n(a)llow divergence or"
+ " (d)uplicate the changeset? $$ &Allowdivergence $$ "
+ "&Duplicate"), 0)
+ choice = ['allowdivergence', 'duplicate'][index]
+ if choice == 'allowdivergence':
+ duplicate = False
+ else:
+ duplicate = True
+
+ new, unusedvariable = rewrite(repo, ctx, [], ctx,
+ [p1, p2],
+ commitopts={'extra': extra})
+ # store touched version to help potential children
+ newmapping[ctx.node()] = new
+
+ if not duplicate:
+ obsolete.createmarkers(repo, [(ctx, (repo[new],))])
+ phases.retractboundary(repo, tr, ctx.phase(), [new])
+ if ctx in repo[None].parents():
+ repo.dirstate.beginparentchange()
+ repo.dirstate.setparents(new, node.nullid)
+ repo.dirstate.endparentchange()
+ tr.close()
+ finally:
+ lockmod.release(tr, lock, wlock)
+
+@command('^fold|squash',
+ [('r', 'rev', [], _("revision to fold")),
+ ('', 'exact', None, _("only fold specified revisions")),
+ ('', 'from', None, _("fold revisions linearly to working copy parent"))
+ ] + commitopts + commitopts2,
+ _('hg fold [OPTION]... [-r] REV'))
+def fold(ui, repo, *revs, **opts):
+ """fold multiple revisions into a single one
+
+ With --from, folds all the revisions linearly between the given revisions
+ and the parent of the working directory.
+
+ With --exact, folds only the specified revisions while ignoring the
+ parent of the working directory. In this case, the given revisions must
+ form a linear unbroken chain.
+
+ .. container:: verbose
+
+ Some examples:
+
+ - Fold the current revision with its parent::
+
+ hg fold --from .^
+
+ - Fold all draft revisions with working directory parent::
+
+ hg fold --from 'draft()'
+
+ See :hg:`help phases` for more about draft revisions and
+ :hg:`help revsets` for more about the `draft()` keyword
+
+ - Fold revisions between 3 and 6 with the working directory parent::
+
+ hg fold --from 3::6
+
+ - Fold revisions 3 and 4:
+
+ hg fold "3 + 4" --exact
+
+ - Only fold revisions linearly between foo and @::
+
+ hg fold foo::@ --exact
+ """
+ revs = list(revs)
+ revs.extend(opts['rev'])
+ if not revs:
+ raise error.Abort(_('no revisions specified'))
+
+ revs = scmutil.revrange(repo, revs)
+
+ if opts['from'] and opts['exact']:
+ raise error.Abort(_('cannot use both --from and --exact'))
+ elif opts['from']:
+ # Try to extend given revision starting from the working directory
+ extrevs = repo.revs('(%ld::.) or (.::%ld)', revs, revs)
+ discardedrevs = [r for r in revs if r not in extrevs]
+ if discardedrevs:
+ raise error.Abort(_("cannot fold non-linear revisions"),
+ hint=_("given revisions are unrelated to parent "
+ "of working directory"))
+ revs = extrevs
+ elif opts['exact']:
+ # Nothing to do; "revs" is already set correctly
+ pass
+ else:
+ raise error.Abort(_('must specify either --from or --exact'))
+
+ if not revs:
+ raise error.Abort(_('specified revisions evaluate to an empty set'),
+ hint=_('use different revision arguments'))
+ elif len(revs) == 1:
+ ui.write_err(_('single revision specified, nothing to fold\n'))
+ return 1
+
+ wlock = lock = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+
+ root, head = _foldcheck(repo, revs)
+
+ tr = repo.transaction('fold')
+ try:
+ commitopts = opts.copy()
+ allctx = [repo[r] for r in revs]
+ targetphase = max(c.phase() for c in allctx)
+
+ if commitopts.get('message') or commitopts.get('logfile'):
+ commitopts['edit'] = False
+ else:
+ msgs = ["HG: This is a fold of %d changesets." % len(allctx)]
+ msgs += ["HG: Commit message of changeset %s.\n\n%s\n" %
+ (c.rev(), c.description()) for c in allctx]
+ commitopts['message'] = "\n".join(msgs)
+ commitopts['edit'] = True
+
+ newid, unusedvariable = rewrite(repo, root, allctx, head,
+ [root.p1().node(),
+ root.p2().node()],
+ commitopts=commitopts)
+ phases.retractboundary(repo, tr, targetphase, [newid])
+ obsolete.createmarkers(repo, [(ctx, (repo[newid],))
+ for ctx in allctx])
+ tr.close()
+ finally:
+ tr.release()
+ ui.status('%i changesets folded\n' % len(revs))
+ if repo['.'].rev() in revs:
+ hg.update(repo, newid)
+ finally:
+ lockmod.release(lock, wlock)
+
+@command('^metaedit',
+ [('r', 'rev', [], _("revision to edit")),
+ ('', 'fold', None, _("also fold specified revisions into one")),
+ ] + commitopts + commitopts2,
+ _('hg metaedit [OPTION]... [-r] [REV]'))
+def metaedit(ui, repo, *revs, **opts):
+ """edit commit information
+
+ Edits the commit information for the specified revisions. By default, edits
+ commit information for the working directory parent.
+
+ With --fold, also folds multiple revisions into one if necessary. In this
+ case, the given revisions must form a linear unbroken chain.
+
+ .. container:: verbose
+
+ Some examples:
+
+ - Edit the commit message for the working directory parent::
+
+ hg metaedit
+
+ - Change the username for the working directory parent::
+
+ hg metaedit --user 'New User <new-email@example.com>'
+
+ - Combine all draft revisions that are ancestors of foo but not of @ into
+ one::
+
+ hg metaedit --fold 'draft() and only(foo,@)'
+
+ See :hg:`help phases` for more about draft revisions, and
+ :hg:`help revsets` for more about the `draft()` and `only()` keywords.
+ """
+ revs = list(revs)
+ revs.extend(opts['rev'])
+ if not revs:
+ if opts['fold']:
+ raise error.Abort(_('revisions must be specified with --fold'))
+ revs = ['.']
+
+ wlock = lock = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+
+ revs = scmutil.revrange(repo, revs)
+ if not opts['fold'] and len(revs) > 1:
+ # TODO: handle multiple revisions. This is somewhat tricky because
+ # if we want to edit a series of commits:
+ #
+ # a ---- b ---- c
+ #
+ # we need to rewrite a first, then directly rewrite b on top of the
+ # new a, then rewrite c on top of the new b. So we need to handle
+ # revisions in topological order.
+ raise error.Abort(_('editing multiple revisions without --fold is '
+ 'not currently supported'))
+
+ if opts['fold']:
+ root, head = _foldcheck(repo, revs)
+ else:
+ if repo.revs("%ld and public()", revs):
+ raise error.Abort(_('cannot edit commit information for public '
+ 'revisions'))
+ newunstable = _disallowednewunstable(repo, revs)
+ if newunstable:
+ raise error.Abort(
+ _('cannot edit commit information in the middle of a '\
+ 'stack'), hint=_('%s will become unstable and new unstable'\
+ ' changes are not allowed') % repo[newunstable.first()])
+ root = head = repo[revs.first()]
+
+ wctx = repo[None]
+ p1 = wctx.p1()
+ tr = repo.transaction('metaedit')
+ newp1 = None
+ try:
+ commitopts = opts.copy()
+ allctx = [repo[r] for r in revs]
+ targetphase = max(c.phase() for c in allctx)
+
+ if commitopts.get('message') or commitopts.get('logfile'):
+ commitopts['edit'] = False
+ else:
+ if opts['fold']:
+ msgs = ["HG: This is a fold of %d changesets." % len(allctx)]
+ msgs += ["HG: Commit message of changeset %s.\n\n%s\n" %
+ (c.rev(), c.description()) for c in allctx]
+ else:
+ msgs = [head.description()]
+ commitopts['message'] = "\n".join(msgs)
+ commitopts['edit'] = True
+
+ # TODO: if the author and message are the same, don't create a new
+ # hash. Right now we create a new hash because the date can be
+ # different.
+ newid, created = rewrite(repo, root, allctx, head,
+ [root.p1().node(), root.p2().node()],
+ commitopts=commitopts)
+ if created:
+ if p1.rev() in revs:
+ newp1 = newid
+ phases.retractboundary(repo, tr, targetphase, [newid])
+ obsolete.createmarkers(repo, [(ctx, (repo[newid],))
+ for ctx in allctx])
+ else:
+ ui.status(_("nothing changed\n"))
+ tr.close()
+ finally:
+ tr.release()
+
+ if opts['fold']:
+ ui.status('%i changesets folded\n' % len(revs))
+ if newp1 is not None:
+ hg.update(repo, newp1)
+ finally:
+ lockmod.release(lock, wlock)
+
+def _foldcheck(repo, revs):
+ roots = repo.revs('roots(%ld)', revs)
+ if len(roots) > 1:
+ raise error.Abort(_("cannot fold non-linear revisions "
+ "(multiple roots given)"))
+ root = repo[roots.first()]
+ if root.phase() <= phases.public:
+ raise error.Abort(_("cannot fold public revisions"))
+ heads = repo.revs('heads(%ld)', revs)
+ if len(heads) > 1:
+ raise error.Abort(_("cannot fold non-linear revisions "
+ "(multiple heads given)"))
+ head = repo[heads.first()]
+ if _disallowednewunstable(repo, revs):
+ raise error.Abort(_("cannot fold chain not ending with a head "\
+ "or with branching"), hint = _("new unstable"\
+ " changesets are not allowed"))
+ return root, head
+
+def _disallowednewunstable(repo, revs):
+ allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
+ if allowunstable:
+ return revset.baseset()
+ return repo.revs("(%ld::) - %ld", revs, revs)
+
+@eh.wrapcommand('graft')
+def graftwrapper(orig, ui, repo, *revs, **kwargs):
+ kwargs = dict(kwargs)
+ revs = list(revs) + kwargs.get('rev', [])
+ kwargs['rev'] = []
+ obsoleted = kwargs.setdefault('obsolete', [])
+
+ wlock = lock = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ if kwargs.get('old_obsolete'):
+ if kwargs.get('continue'):
+ obsoleted.extend(repo.opener.read('graftstate').splitlines())
+ else:
+ obsoleted.extend(revs)
+ # convert obsolete target into revs to avoid alias joke
+ obsoleted[:] = [str(i) for i in repo.revs('%lr', obsoleted)]
+ if obsoleted and len(revs) > 1:
+
+ raise error.Abort(_('cannot graft multiple revisions while '
+ 'obsoleting (for now).'))
+
+ return commitwrapper(orig, ui, repo,*revs, **kwargs)
+ finally:
+ lockmod.release(lock, wlock)
+
+@eh.extsetup
+def oldevolveextsetup(ui):
+ for cmd in ['prune', 'uncommit', 'touch', 'fold']:
+ try:
+ entry = extensions.wrapcommand(cmdtable, cmd,
+ warnobserrors)
+ except error.UnknownCommand:
+ # Commands may be disabled
+ continue
+
+ entry = cmdutil.findcmd('commit', commands.table)[1]
+ entry[1].append(('o', 'obsolete', [],
+ _("make commit obsolete this revision (DEPRECATED)")))
+ entry = cmdutil.findcmd('graft', commands.table)[1]
+ entry[1].append(('o', 'obsolete', [],
+ _("make graft obsoletes this revision (DEPRECATED)")))
+ entry[1].append(('O', 'old-obsolete', False,
+ _("make graft obsoletes its source (DEPRECATED)")))
+
+#####################################################################
+### Obsolescence marker exchange experimenation ###
+#####################################################################
+
+def obsexcmsg(ui, message, important=False):
+ verbose = ui.configbool('experimental', 'verbose-obsolescence-exchange',
+ False)
+ if verbose:
+ message = 'OBSEXC: ' + message
+ if important or verbose:
+ ui.status(message)
+
+def obsexcprg(ui, *args, **kwargs):
+ topic = 'obsmarkers exchange'
+ if ui.configbool('experimental', 'verbose-obsolescence-exchange', False):
+ topic = 'OBSEXC'
+ ui.progress(topic, *args, **kwargs)
+
+@eh.wrapfunction(exchange, '_pushdiscoveryobsmarkers')
+def _pushdiscoveryobsmarkers(orig, pushop):
+ if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
+ and pushop.repo.obsstore
+ and 'obsolete' in pushop.remote.listkeys('namespaces')):
+ repo = pushop.repo
+ obsexcmsg(repo.ui, "computing relevant nodes\n")
+ revs = list(repo.revs('::%ln', pushop.futureheads))
+ unfi = repo.unfiltered()
+ cl = unfi.changelog
+ if not pushop.remote.capable('_evoext_obshash_0'):
+ # do not trust core yet
+ # return orig(pushop)
+ nodes = [cl.node(r) for r in revs]
+ if nodes:
+ obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n"
+ % len(nodes))
+ pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
+ else:
+ obsexcmsg(repo.ui, "markers already in sync\n")
+ pushop.outobsmarkers = []
+ pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
+ return
+
+ common = []
+ obsexcmsg(repo.ui, "looking for common markers in %i nodes\n"
+ % len(revs))
+ commonrevs = list(unfi.revs('::%ln', pushop.outgoing.commonheads))
+ common = findcommonobsmarkers(pushop.ui, unfi, pushop.remote,
+ commonrevs)
+
+ revs = list(unfi.revs('%ld - (::%ln)', revs, common))
+ nodes = [cl.node(r) for r in revs]
+ if nodes:
+ obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n"
+ % len(nodes))
+ pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes)
+ else:
+ obsexcmsg(repo.ui, "markers already in sync\n")
+ pushop.outobsmarkers = []
+
+@eh.wrapfunction(wireproto, 'capabilities')
+def discocapabilities(orig, repo, proto):
+ """wrapper to advertise new capability"""
+ caps = orig(repo, proto)
+ if obsolete.isenabled(repo, obsolete.exchangeopt):
+ caps += ' _evoext_obshash_0'
+ return caps
+
+@eh.extsetup
+def _installobsmarkersdiscovery(ui):
+ hgweb_mod.perms['evoext_obshash'] = 'pull'
+ hgweb_mod.perms['evoext_obshash1'] = 'pull'
+ # wrap command content
+ oldcap, args = wireproto.commands['capabilities']
+ def newcap(repo, proto):
+ return discocapabilities(oldcap, repo, proto)
+ wireproto.commands['capabilities'] = (newcap, args)
+ wireproto.commands['evoext_obshash'] = (srv_obshash, 'nodes')
+ wireproto.commands['evoext_obshash1'] = (srv_obshash1, 'nodes')
+ if getattr(exchange, '_pushdiscoveryobsmarkers', None) is None:
+ ui.warn(_('evolve: your mercurial version is too old\n'
+ 'evolve: (running in degraded mode, push will '
+ 'includes all markers)\n'))
+ else:
+ olddisco = exchange.pushdiscoverymapping['obsmarker']
+ def newdisco(pushop):
+ _pushdiscoveryobsmarkers(olddisco, pushop)
+ exchange.pushdiscoverymapping['obsmarker'] = newdisco
+
+### Set discovery START
+
+from mercurial import dagutil
+from mercurial import setdiscovery
+
+def _obshash(repo, nodes, version=0):
+ if version == 0:
+ hashs = _obsrelsethashtreefm0(repo)
+ elif version ==1:
+ hashs = _obsrelsethashtreefm1(repo)
+ else:
+ assert False
+ nm = repo.changelog.nodemap
+ revs = [nm.get(n) for n in nodes]
+ return [r is None and nullid or hashs[r][1] for r in revs]
+
+def srv_obshash(repo, proto, nodes):
+ return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes)))
+
+def srv_obshash1(repo, proto, nodes):
+ return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes),
+ version=1))
+
+@eh.addattr(localrepo.localpeer, 'evoext_obshash')
+def local_obshash(peer, nodes):
+ return _obshash(peer._repo, nodes)
+
+@eh.addattr(localrepo.localpeer, 'evoext_obshash1')
+def local_obshash1(peer, nodes):
+ return _obshash(peer._repo, nodes, version=1)
+
+@eh.addattr(wireproto.wirepeer, 'evoext_obshash')
+def peer_obshash(self, nodes):
+ d = self._call("evoext_obshash", nodes=wireproto.encodelist(nodes))
+ try:
+ return wireproto.decodelist(d)
+ except ValueError:
+ self._abort(error.ResponseError(_("unexpected response:"), d))
+
+@eh.addattr(wireproto.wirepeer, 'evoext_obshash1')
+def peer_obshash1(self, nodes):
+ d = self._call("evoext_obshash1", nodes=wireproto.encodelist(nodes))
+ try:
+ return wireproto.decodelist(d)
+ except ValueError:
+ self._abort(error.ResponseError(_("unexpected response:"), d))
+
+def findcommonobsmarkers(ui, local, remote, probeset,
+ initialsamplesize=100,
+ fullsamplesize=200):
+ # from discovery
+ roundtrips = 0
+ cl = local.changelog
+ dag = dagutil.revlogdag(cl)
+ missing = set()
+ common = set()
+ undecided = set(probeset)
+ totalnb = len(undecided)
+ ui.progress(_("comparing with other"), 0, total=totalnb)
+ _takefullsample = setdiscovery._takefullsample
+ if remote.capable('_evoext_obshash_1'):
+ getremotehash = remote.evoext_obshash1
+ localhash = _obsrelsethashtreefm1(local)
+ else:
+ getremotehash = remote.evoext_obshash
+ localhash = _obsrelsethashtreefm0(local)
+
+ while undecided:
+
+ ui.note(_("sampling from both directions\n"))
+ if len(undecided) < fullsamplesize:
+ sample = set(undecided)
+ else:
+ sample = _takefullsample(dag, undecided, size=fullsamplesize)
+
+ roundtrips += 1
+ ui.progress(_("comparing with other"), totalnb - len(undecided),
+ total=totalnb)
+ ui.debug("query %i; still undecided: %i, sample size is: %i\n"
+ % (roundtrips, len(undecided), len(sample)))
+ # indices between sample and externalized version must match
+ sample = list(sample)
+ remotehash = getremotehash(dag.externalizeall(sample))
+
+ yesno = [localhash[ix][1] == remotehash[si]
+ for si, ix in enumerate(sample)]
+
+ commoninsample = set(n for i, n in enumerate(sample) if yesno[i])
+ common.update(dag.ancestorset(commoninsample, common))
+
+ missinginsample = [n for i, n in enumerate(sample) if not yesno[i]]
+ missing.update(dag.descendantset(missinginsample, missing))
+
+ undecided.difference_update(missing)
+ undecided.difference_update(common)
+
+
+ ui.progress(_("comparing with other"), None)
+ result = dag.headsetofconnecteds(common)
+ ui.debug("%d total queries\n" % roundtrips)
+
+ if not result:
+ return set([nullid])
+ return dag.externalizeall(result)
+
+
+_pushkeyescape = getattr(obsolete, '_pushkeyescape', None)
+
+class pushobsmarkerStringIO(StringIO):
+ """hacky string io for progress"""
+
+ @util.propertycache
+ def length(self):
+ return len(self.getvalue())
+
+ def read(self, size=None):
+ obsexcprg(self.ui, self.tell(), unit=_("bytes"), total=self.length)
+ return StringIO.read(self, size)
+
+ def __iter__(self):
+ d = self.read(4096)
+ while d:
+ yield d
+ d = self.read(4096)
+
+@eh.wrapfunction(exchange, '_pushobsolete')
+def _pushobsolete(orig, pushop):
+ """utility function to push obsolete markers to a remote"""
+ stepsdone = getattr(pushop, 'stepsdone', None)
+ if stepsdone is not None:
+ if 'obsmarkers' in stepsdone:
+ return
+ stepsdone.add('obsmarkers')
+ if util.safehasattr(pushop, 'cgresult'):
+ cgresult = pushop.cgresult
+ else:
+ cgresult = pushop.ret
+ if cgresult == 0:
+ return
+ pushop.ui.debug('try to push obsolete markers to remote\n')
+ repo = pushop.repo
+ remote = pushop.remote
+ if (obsolete.isenabled(repo, obsolete.exchangeopt) and repo.obsstore and
+ 'obsolete' in remote.listkeys('namespaces')):
+ markers = pushop.outobsmarkers
+ if not markers:
+ obsexcmsg(repo.ui, "no marker to push\n")
+ elif remote.capable('_evoext_pushobsmarkers_0'):
+ obsdata = pushobsmarkerStringIO()
+ for chunk in obsolete.encodemarkers(markers, True):
+ obsdata.write(chunk)
+ obsdata.seek(0)
+ obsdata.ui = repo.ui
+ obsexcmsg(repo.ui, "pushing %i obsolescence markers (%i bytes)\n"
+ % (len(markers), len(obsdata.getvalue())),
+ True)
+ remote.evoext_pushobsmarkers_0(obsdata)
+ obsexcprg(repo.ui, None)
+ else:
+ rslts = []
+ remotedata = _pushkeyescape(markers).items()
+ totalbytes = sum(len(d) for k, d in remotedata)
+ sentbytes = 0
+ obsexcmsg(repo.ui, "pushing %i obsolescence markers in %i "
+ "pushkey payload (%i bytes)\n"
+ % (len(markers), len(remotedata), totalbytes),
+ True)
+ for key, data in remotedata:
+ obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"),
+ total=totalbytes)
+ rslts.append(remote.pushkey('obsolete', key, '', data))
+ sentbytes += len(data)
+ obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"),
+ total=totalbytes)
+ obsexcprg(repo.ui, None)
+ if [r for r in rslts if not r]:
+ msg = _('failed to push some obsolete markers!\n')
+ repo.ui.warn(msg)
+ obsexcmsg(repo.ui, "DONE\n")
+
+
+@eh.addattr(wireproto.wirepeer, 'evoext_pushobsmarkers_0')
+def client_pushobsmarkers(self, obsfile):
+ """wireprotocol peer method"""
+ self.requirecap('_evoext_pushobsmarkers_0',
+ _('push obsolete markers faster'))
+ ret, output = self._callpush('evoext_pushobsmarkers_0', obsfile)
+ for l in output.splitlines(True):
+ self.ui.status(_('remote: '), l)
+ return ret
+
+@eh.addattr(httppeer.httppeer, 'evoext_pushobsmarkers_0')
+def httpclient_pushobsmarkers(self, obsfile):
+ """httpprotocol peer method
+ (Cannot simply use _callpush as http is doing some special handling)"""
+ self.requirecap('_evoext_pushobsmarkers_0',
+ _('push obsolete markers faster'))
+ try:
+ r = self._call('evoext_pushobsmarkers_0', data=obsfile)
+ vals = r.split('\n', 1)
+ if len(vals) < 2:
+ raise error.ResponseError(_("unexpected response:"), r)
+
+ for l in vals[1].splitlines(True):
+ if l.strip():
+ self.ui.status(_('remote: '), l)
+ return vals[0]
+ except socket.error as err:
+ if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
+ raise error.Abort(_('push failed: %s') % err.args[1])
+ raise error.Abort(err.args[1])
+
+@eh.wrapfunction(localrepo.localrepository, '_restrictcapabilities')
+def local_pushobsmarker_capabilities(orig, repo, caps):
+ caps = orig(repo, caps)
+ caps.add('_evoext_pushobsmarkers_0')
+ return caps
+
+def _pushobsmarkers(repo, data):
+ tr = lock = None
+ try:
+ lock = repo.lock()
+ tr = repo.transaction('pushkey: obsolete markers')
+ new = repo.obsstore.mergemarkers(tr, data)
+ if new is not None:
+ obsexcmsg(repo.ui, "%i obsolescence markers added\n" % new, True)
+ tr.close()
+ finally:
+ lockmod.release(tr, lock)
+ repo.hook('evolve_pushobsmarkers')
+
+@eh.addattr(localrepo.localpeer, 'evoext_pushobsmarkers_0')
+def local_pushobsmarkers(peer, obsfile):
+ data = obsfile.read()
+ _pushobsmarkers(peer._repo, data)
+
+def srv_pushobsmarkers(repo, proto):
+ """wireprotocol command"""
+ fp = StringIO()
+ proto.redirect()
+ proto.getfile(fp)
+ data = fp.getvalue()
+ fp.close()
+ _pushobsmarkers(repo, data)
+ return wireproto.pushres(0)
+
+def _buildpullobsmarkersboundaries(pullop):
+ """small funtion returning the argument for pull markers call
+ may to contains 'heads' and 'common'. skip the key for None.
+
+ Its a separed functio to play around with strategy for that."""
+ repo = pullop.repo
+ remote = pullop.remote
+ unfi = repo.unfiltered()
+ revs = unfi.revs('::(%ln - null)', pullop.common)
+ common = [nullid]
+ if remote.capable('_evoext_obshash_0'):
+ obsexcmsg(repo.ui, "looking for common markers in %i nodes\n"
+ % len(revs))
+ common = findcommonobsmarkers(repo.ui, repo, remote, revs)
+ return {'heads': pullop.pulledsubset, 'common': common}
+
+@eh.uisetup
+def addgetbundleargs(self):
+ gboptsmap['evo_obscommon'] = 'nodes'
+
+@eh.wrapfunction(exchange, '_pullbundle2extraprepare')
+def _addobscommontob2pull(orig, pullop, kwargs):
+ ret = orig(pullop, kwargs)
+ if ('obsmarkers' in kwargs and
+ pullop.remote.capable('_evoext_getbundle_obscommon')):
+ boundaries = _buildpullobsmarkersboundaries(pullop)
+ common = boundaries['common']
+ if common != [nullid]:
+ kwargs['evo_obscommon'] = common
+ return ret
+
+@eh.wrapfunction(exchange, '_getbundleobsmarkerpart')
+def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs):
+ if 'evo_obscommon' not in kwargs:
+ return orig(bundler, repo, source, **kwargs)
+
+ heads = kwargs.get('heads')
+ if kwargs.get('obsmarkers', False):
+ if heads is None:
+ heads = repo.heads()
+ obscommon = kwargs.get('evo_obscommon', ())
+ assert obscommon
+ obsset = repo.unfiltered().set('::%ln - ::%ln', heads, obscommon)
+ subset = [c.node() for c in obsset]
+ markers = repo.obsstore.relevantmarkers(subset)
+ exchange.buildobsmarkerspart(bundler, markers)
+
+@eh.uisetup
+def installgetbundlepartgen(ui):
+ origfunc = exchange.getbundle2partsmapping['obsmarkers']
+ def newfunc(*args, **kwargs):
+ return _getbundleobsmarkerpart(origfunc, *args, **kwargs)
+ exchange.getbundle2partsmapping['obsmarkers'] = newfunc
+
+@eh.wrapfunction(exchange, '_pullobsolete')
+def _pullobsolete(orig, pullop):
+ if not obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
+ return None
+ if 'obsmarkers' not in getattr(pullop, 'todosteps', ['obsmarkers']):
+ return None
+ if 'obsmarkers' in getattr(pullop, 'stepsdone', []):
+ return None
+ wirepull = pullop.remote.capable('_evoext_pullobsmarkers_0')
+ if not wirepull:
+ return orig(pullop)
+ if 'obsolete' not in pullop.remote.listkeys('namespaces'):
+ return None # remote opted out of obsolescence marker exchange
+ tr = None
+ ui = pullop.repo.ui
+ boundaries = _buildpullobsmarkersboundaries(pullop)
+ if not set(boundaries['heads']) - set(boundaries['common']):
+ obsexcmsg(ui, "nothing to pull\n")
+ return None
+
+ obsexcmsg(ui, "pull obsolescence markers\n", True)
+ new = 0
+
+ if wirepull:
+ obsdata = pullop.remote.evoext_pullobsmarkers_0(**boundaries)
+ obsdata = obsdata.read()
+ if len(obsdata) > 5:
+ obsexcmsg(ui, "merging obsolescence markers (%i bytes)\n"
+ % len(obsdata))
+ tr = pullop.gettransaction()
+ old = len(pullop.repo.obsstore._all)
+ pullop.repo.obsstore.mergemarkers(tr, obsdata)
+ new = len(pullop.repo.obsstore._all) - old
+ obsexcmsg(ui, "%i obsolescence markers added\n" % new, True)
+ else:
+ obsexcmsg(ui, "no unknown remote markers\n")
+ obsexcmsg(ui, "DONE\n")
+ if new:
+ pullop.repo.invalidatevolatilesets()
+ return tr
+
+def _getobsmarkersstream(repo, heads=None, common=None):
+ revset = ''
+ args = []
+ repo = repo.unfiltered()
+ if heads is None:
+ revset = 'all()'
+ elif heads:
+ revset += "(::%ln)"
+ args.append(heads)
+ else:
+ assert False, 'pulling no heads?'
+ if common:
+ revset += ' - (::%ln)'
+ args.append(common)
+ nodes = [c.node() for c in repo.set(revset, *args)]
+ markers = repo.obsstore.relevantmarkers(nodes)
+ obsdata = StringIO()
+ for chunk in obsolete.encodemarkers(markers, True):
+ obsdata.write(chunk)
+ obsdata.seek(0)
+ return obsdata
+
+@eh.addattr(wireproto.wirepeer, 'evoext_pullobsmarkers_0')
+def client_pullobsmarkers(self, heads=None, common=None):
+ self.requirecap('_evoext_pullobsmarkers_0', _('look up remote obsmarkers'))
+ opts = {}
+ if heads is not None:
+ opts['heads'] = wireproto.encodelist(heads)
+ if common is not None:
+ opts['common'] = wireproto.encodelist(common)
+ if util.safehasattr(self, '_callcompressable'):
+ f = self._callcompressable("evoext_pullobsmarkers_0", **opts)
+ else:
+ f = self._callstream("evoext_pullobsmarkers_0", **opts)
+ f = self._decompress(f)
+ length = int(f.read(20))
+ chunk = 4096
+ current = 0
+ data = StringIO()
+ ui = self.ui
+ obsexcprg(ui, current, unit=_("bytes"), total=length)
+ while current < length:
+ readsize = min(length - current, chunk)
+ data.write(f.read(readsize))
+ current += readsize
+ obsexcprg(ui, current, unit=_("bytes"), total=length)
+ obsexcprg(ui, None)
+ data.seek(0)
+ return data
+
+@eh.addattr(localrepo.localpeer, 'evoext_pullobsmarkers_0')
+def local_pullobsmarkers(self, heads=None, common=None):
+ return _getobsmarkersstream(self._repo, heads=heads, common=common)
+
+# The wireproto.streamres API changed, handling chunking and compression
+# directly. Handle either case.
+if util.safehasattr(wireproto.abstractserverproto, 'groupchunks'):
+ # We need to handle chunking and compression directly
+ def streamres(d, proto):
+ return wireproto.streamres(proto.groupchunks(d))
+else:
+ # Leave chunking and compression to streamres
+ def streamres(d, proto):
+ return wireproto.streamres(reader=d, v1compressible=True)
+
+def srv_pullobsmarkers(repo, proto, others):
+ opts = wireproto.options('', ['heads', 'common'], others)
+ for k, v in opts.iteritems():
+ if k in ('heads', 'common'):
+ opts[k] = wireproto.decodelist(v)
+ obsdata = _getobsmarkersstream(repo, **opts)
+ finaldata = StringIO()
+ obsdata = obsdata.getvalue()
+ finaldata.write('%20i' % len(obsdata))
+ finaldata.write(obsdata)
+ finaldata.seek(0)
+ return streamres(finaldata, proto)
+
+def _obsrelsethashtreefm0(repo):
+ return _obsrelsethashtree(repo, obsolete._fm0encodeonemarker)
+
+def _obsrelsethashtreefm1(repo):
+ return _obsrelsethashtree(repo, obsolete._fm1encodeonemarker)
+
+def _obsrelsethashtree(repo, encodeonemarker):
+ cache = []
+ unfi = repo.unfiltered()
+ markercache = {}
+ repo.ui.progress(_("preparing locally"), 0, total=len(unfi))
+ for i in unfi:
+ ctx = unfi[i]
+ entry = 0
+ sha = hashlib.sha1()
+ # add data from p1
+ for p in ctx.parents():
+ p = p.rev()
+ if p < 0:
+ p = nullid
+ else:
+ p = cache[p][1]
+ if p != nullid:
+ entry += 1
+ sha.update(p)
+ tmarkers = repo.obsstore.relevantmarkers([ctx.node()])
+ if tmarkers:
+ bmarkers = []
+ for m in tmarkers:
+ if not m in markercache:
+ markercache[m] = encodeonemarker(m)
+ bmarkers.append(markercache[m])
+ bmarkers.sort()
+ for m in bmarkers:
+ entry += 1
+ sha.update(m)
+ if entry:
+ cache.append((ctx.node(), sha.digest()))
+ else:
+ cache.append((ctx.node(), nullid))
+ repo.ui.progress(_("preparing locally"), i, total=len(unfi))
+ repo.ui.progress(_("preparing locally"), None)
+ return cache
+
+@command('debugobsrelsethashtree',
+ [('', 'v0', None, 'hash on marker format "0"'),
+ ('', 'v1', None, 'hash on marker format "1" (default)')] , _(''))
+def debugobsrelsethashtree(ui, repo, v0=False, v1=False):
+ """display Obsolete markers, Relevant Set, Hash Tree
+ changeset-node obsrelsethashtree-node
+
+ It computed form the "orsht" of its parent and markers
+ relevant to the changeset itself."""
+ if v0 and v1:
+ raise error.Abort('cannot only specify one format')
+ elif v0:
+ treefunc = _obsrelsethashtreefm0
+ else:
+ treefunc = _obsrelsethashtreefm1
+
+ for chg, obs in treefunc(repo):
+ ui.status('%s %s\n' % (node.hex(chg), node.hex(obs)))
+
+_bestformat = max(obsolete.formats.keys())
+
+
+@eh.wrapfunction(obsolete, '_checkinvalidmarkers')
+def _checkinvalidmarkers(orig, markers):
+ """search for marker with invalid data and raise error if needed
+
+ Exist as a separated function to allow the evolve extension for a more
+ subtle handling.
+ """
+ if 'debugobsconvert' in sys.argv:
+ return
+ for mark in markers:
+ if node.nullid in mark[1]:
+ raise error.Abort(_('bad obsolescence marker detected: '
+ 'invalid successors nullid'),
+ hint=_('You should run `hg debugobsconvert`'))
+
+@command(
+ 'debugobsconvert',
+ [('', 'new-format', _bestformat, _('Destination format for markers.'))],
+ '')
+def debugobsconvert(ui, repo, new_format):
+ origmarkers = repo.obsstore._all # settle version
+ if new_format == repo.obsstore._version:
+ msg = _('New format is the same as the old format, not upgrading!')
+ raise error.Abort(msg)
+ f = repo.svfs('obsstore', 'wb', atomictemp=True)
+ known = set()
+ markers = []
+ for m in origmarkers:
+ # filter out invalid markers
+ if nullid in m[1]:
+ m = list(m)
+ m[1] = tuple(s for s in m[1] if s != nullid)
+ m = tuple(m)
+ if m in known:
+ continue
+ known.add(m)
+ markers.append(m)
+ ui.write(_('Old store is version %d, will rewrite in version %d\n') % (
+ repo.obsstore._version, new_format))
+ map(f.write, obsolete.encodemarkers(markers, True, new_format))
+ f.close()
+ ui.write(_('Done!\n'))
+
+
+@eh.wrapfunction(wireproto, 'capabilities')
+def capabilities(orig, repo, proto):
+ """wrapper to advertise new capability"""
+ caps = orig(repo, proto)
+ if obsolete.isenabled(repo, obsolete.exchangeopt):
+ caps += ' _evoext_pushobsmarkers_0'
+ caps += ' _evoext_pullobsmarkers_0'
+ caps += ' _evoext_obshash_0'
+ caps += ' _evoext_obshash_1'
+ caps += ' _evoext_getbundle_obscommon'
+ return caps
+
+
+@eh.extsetup
+def _installwireprotocol(ui):
+ localrepo.moderncaps.add('_evoext_pullobsmarkers_0')
+ hgweb_mod.perms['evoext_pushobsmarkers_0'] = 'push'
+ hgweb_mod.perms['evoext_pullobsmarkers_0'] = 'pull'
+ wireproto.commands['evoext_pushobsmarkers_0'] = (srv_pushobsmarkers, '')
+ wireproto.commands['evoext_pullobsmarkers_0'] = (srv_pullobsmarkers, '*')
+ # wrap command content
+ oldcap, args = wireproto.commands['capabilities']
+ def newcap(repo, proto):
+ return capabilities(oldcap, repo, proto)
+ wireproto.commands['capabilities'] = (newcap, args)
+
+# Mercurial >= 3.6 passes ui
+def _helploader(ui=None):
+ return help.gettext(evolutionhelptext)
+
+@eh.uisetup
+def _setuphelp(ui):
+ for entry in help.helptable:
+ if entry[0] == "evolution":
+ break
+ else:
+ help.helptable.append((["evolution"], _("Safely Rewriting History"),
+ _helploader))
+ help.helptable.sort()
+
+def _relocatecommit(repo, orig, commitmsg):
+ if commitmsg is None:
+ commitmsg = orig.description()
+ extra = dict(orig.extra())
+ if 'branch' in extra:
+ del extra['branch']
+ extra['rebase_source'] = orig.hex()
+
+ backup = repo.ui.backupconfig('phases', 'new-commit')
+ try:
+ targetphase = max(orig.phase(), phases.draft)
+ repo.ui.setconfig('phases', 'new-commit', targetphase, 'evolve')
+ # Commit might fail if unresolved files exist
+ nodenew = repo.commit(text=commitmsg, user=orig.user(),
+ date=orig.date(), extra=extra)
+ finally:
+ repo.ui.restoreconfig(backup)
+ return nodenew
+
+def _finalizerelocate(repo, orig, dest, nodenew, tr):
+ destbookmarks = repo.nodebookmarks(dest.node())
+ nodesrc = orig.node()
+ destphase = repo[nodesrc].phase()
+ oldbookmarks = repo.nodebookmarks(nodesrc)
+ if nodenew is not None:
+ phases.retractboundary(repo, tr, destphase, [nodenew])
+ obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))])
+ for book in oldbookmarks:
+ repo._bookmarks[book] = nodenew
+ else:
+ obsolete.createmarkers(repo, [(repo[nodesrc], ())])
+ # Behave like rebase, move bookmarks to dest
+ for book in oldbookmarks:
+ repo._bookmarks[book] = dest.node()
+ for book in destbookmarks: # restore bookmark that rebase move
+ repo._bookmarks[book] = dest.node()
+ if oldbookmarks or destbookmarks:
+ repo._bookmarks.recordchange(tr)
+
+evolvestateversion = 0
+
+@eh.uisetup
+def setupevolveunfinished(ui):
+ data = ('evolvestate', True, False, _('evolve in progress'),
+ _("use 'hg evolve --continue' or 'hg update -C .' to abort"))
+ cmdutil.unfinishedstates.append(data)
+
+@eh.wrapfunction(hg, 'clean')
+def clean(orig, repo, *args, **kwargs):
+ ret = orig(repo, *args, **kwargs)
+ util.unlinkpath(repo.join('evolvestate'), ignoremissing=True)
+ return ret
+
+def _evolvestatewrite(repo, state):
+ # [version]
+ # [type][length][content]
+ #
+ # `version` is a 4 bytes integer (handled at higher level)
+ # `type` is a single character, `length` is a 4 byte integer, and
+ # `content` is an arbitrary byte sequence of length `length`.
+ f = repo.vfs('evolvestate', 'w')
+ try:
+ f.write(_pack('>I', evolvestateversion))
+ current = state['current']
+ key = 'C' # as in 'current'
+ format = '>sI%is' % len(current)
+ f.write(_pack(format, key, len(current), current))
+ finally:
+ f.close()
+
+def _evolvestateread(repo):
+ try:
+ f = repo.vfs('evolvestate')
+ except IOError as err:
+ if err.errno != errno.ENOENT:
+ raise
+ return None
+ try:
+ versionblob = f.read(4)
+ if len(versionblob) < 4:
+ repo.ui.debug('ignoring corrupted evolvestte (file contains %i bits)'
+ % len(versionblob))
+ return None
+ version = _unpack('>I', versionblob)[0]
+ if version != evolvestateversion:
+ raise error.Abort(_('unknown evolvestate version %i')
+ % version, hint=_('upgrade your evolve'))
+ records = []
+ data = f.read()
+ off = 0
+ end = len(data)
+ while off < end:
+ rtype = data[off]
+ off += 1
+ length = _unpack('>I', data[off:(off + 4)])[0]
+ off += 4
+ record = data[off:(off + length)]
+ off += length
+ if rtype == 't':
+ rtype, record = record[0], record[1:]
+ records.append((rtype, record))
+ state = {}
+ for rtype, rdata in records:
+ if rtype == 'C':
+ state['current'] = rdata
+ elif rtype.lower():
+ repo.ui.debug('ignore evolve state record type %s' % rtype)
+ else:
+ raise error.Abort(_('unknown evolvestate field type %r')
+ % rtype, hint=_('upgrade your evolve'))
+ return state
+ finally:
+ f.close()
+
+def _evolvestatedelete(repo):
+ util.unlinkpath(repo.join('evolvestate'), ignoremissing=True)
+
+def _evolvemerge(repo, orig, dest, pctx, keepbranch):
+ """Used by the evolve function to merge dest on top of pctx.
+ return the same tuple as merge.graft"""
+ if repo['.'].rev() != dest.rev():
+ #assert False
+ try:
+ merge.update(repo,
+ dest,
+ branchmerge=False,
+ force=True)
+ except TypeError:
+ # Mercurial < 43c00ca887d1 (3.7)
+ merge.update(repo,
+ dest,
+ branchmerge=False,
+ force=True,
+ partial=False)
+ if bmactive(repo):
+ repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo))
+ bmdeactivate(repo)
+ if keepbranch:
+ repo.dirstate.setbranch(orig.branch())
+ if util.safehasattr(repo, 'currenttopic'):
+ # uurrgs
+ # there no other topic setter yet
+ if not orig.topic() and repo.vfs.exists('topic'):
+ repo.vfs.unlink('topic')
+ else:
+ with repo.vfs.open('topic', 'w') as f:
+ f.write(orig.topic())
+
+ try:
+ r = merge.graft(repo, orig, pctx, ['local', 'graft'], True)
+ except TypeError:
+ # not using recent enough mercurial
+ if len(orig.parents()) == 2:
+ raise error.Abort(
+ _("no support for evolving merge changesets yet"),
+ hint=_("Redo the merge and use `hg prune <old> --succ "
+ "<new>` to obsolete the old one"))
+
+ r = merge.graft(repo, orig, pctx, ['local', 'graft'])
+ return r
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/hack/directaccess.py Tue Feb 28 17:27:44 2017 +0100
@@ -0,0 +1,194 @@
+""" This extension provides direct access
+It is the ability to refer and access hidden sha in commands provided that you
+know their value.
+For example hg log -r xxx where xxx is a commit has should work whether xxx is
+hidden or not as we assume that the user knows what he is doing when referring
+to xxx.
+"""
+from mercurial import extensions
+from mercurial import cmdutil
+from mercurial import repoview
+from mercurial import branchmap
+from mercurial import revset
+from mercurial import error
+from mercurial import commands
+from mercurial import hg
+from mercurial import util
+from mercurial.i18n import _
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+# By default, all the commands have directaccess with warnings
+# List of commands that have no directaccess and directaccess with no warning
+directaccesslevel = [
+ # Format:
+ # ('nowarning', 'evolve', 'prune'),
+ # means: no directaccess warning, for the command in evolve named prune
+ #
+ # ('error', None, 'serve'),
+ # means: no directaccess for the command in core named serve
+ #
+ # The list is ordered alphabetically by command names, starting with all
+ # the commands in core then all the commands in the extensions
+ #
+ # The general guideline is:
+ # - remove directaccess warnings for read only commands
+ # - no direct access for commands with consequences outside of the repo
+ # - leave directaccess warnings for all the other commands
+ #
+ ('nowarning', None, 'annotate'),
+ ('nowarning', None, 'archive'),
+ ('nowarning', None, 'bisect'),
+ ('nowarning', None, 'bookmarks'),
+ ('nowarning', None, 'bundle'),
+ ('nowarning', None, 'cat'),
+ ('nowarning', None, 'diff'),
+ ('nowarning', None, 'export'),
+ ('nowarning', None, 'identify'),
+ ('nowarning', None, 'incoming'),
+ ('nowarning', None, 'log'),
+ ('nowarning', None, 'manifest'),
+ ('error', None, 'outgoing'), # confusing if push errors and not outgoing
+ ('error', None, 'push'), # destructive
+ ('nowarning', None, 'revert'),
+ ('error', None, 'serve'),
+ ('nowarning', None, 'tags'),
+ ('nowarning', None, 'unbundle'),
+ ('nowarning', None, 'update'),
+]
+
+def reposetup(ui, repo):
+ repo._explicitaccess = set()
+
+def _computehidden(repo):
+ hidden = repoview.filterrevs(repo, 'visible')
+ cl = repo.changelog
+ dynamic = hidden & repo._explicitaccess
+ if dynamic:
+ blocked = cl.ancestors(dynamic, inclusive=True)
+ hidden = frozenset(r for r in hidden if r not in blocked)
+ return hidden
+
+def setupdirectaccess():
+ """ Add two new filtername that behave like visible to provide direct access
+ and direct access with warning. Wraps the commands to setup direct access
+ """
+ repoview.filtertable.update({'visible-directaccess-nowarn': _computehidden})
+ repoview.filtertable.update({'visible-directaccess-warn': _computehidden})
+ branchmap.subsettable['visible-directaccess-nowarn'] = 'visible'
+ branchmap.subsettable['visible-directaccess-warn'] = 'visible'
+
+ for warn, ext, cmd in directaccesslevel:
+ try:
+ cmdtable = extensions.find(ext).cmdtable if ext else commands.table
+ wrapper = wrapwitherror if warn == 'error' else wrapwithoutwarning
+ extensions.wrapcommand(cmdtable, cmd, wrapper)
+ except (error.UnknownCommand, KeyError):
+ pass
+
+def wrapwitherror(orig, ui, repo, *args, **kwargs):
+ if repo and repo.filtername == 'visible-directaccess-warn':
+ repo = repo.filtered('visible')
+ return orig(ui, repo, *args, **kwargs)
+
+def wrapwithoutwarning(orig, ui, repo, *args, **kwargs):
+ if repo and repo.filtername == 'visible-directaccess-warn':
+ repo = repo.filtered("visible-directaccess-nowarn")
+ return orig(ui, repo, *args, **kwargs)
+
+def uisetup(ui):
+ """ Change ordering of extensions to ensure that directaccess extsetup comes
+ after the one of the extensions in the loadsafter list """
+ loadsafter = ui.configlist('directaccess','loadsafter')
+ order = list(extensions._order)
+ directaccesidx = order.index('directaccess')
+
+ # The min idx for directaccess to load after all the extensions in loadafter
+ minidxdirectaccess = directaccesidx
+
+ for ext in loadsafter:
+ try:
+ minidxdirectaccess = max(minidxdirectaccess, order.index(ext))
+ except ValueError:
+ pass # extension not loaded
+
+ if minidxdirectaccess > directaccesidx:
+ order.insert(minidxdirectaccess + 1, 'directaccess')
+ order.remove('directaccess')
+ extensions._order = order
+
+def _repository(orig, *args, **kwargs):
+ """Make visible-directaccess-warn the default filter for new repos"""
+ repo = orig(*args, **kwargs)
+ return repo.filtered("visible-directaccess-warn")
+
+def extsetup(ui):
+ extensions.wrapfunction(revset, 'posttreebuilthook', _posttreebuilthook)
+ extensions.wrapfunction(hg, 'repository', _repository)
+ setupdirectaccess()
+
+hashre = util.re.compile('[0-9a-fA-F]{1,40}')
+
+_listtuple = ('symbol', '_list')
+
+def _ishashsymbol(symbol, maxrev):
+ # Returns true if symbol looks like a hash
+ try:
+ n = int(symbol)
+ if n <= maxrev:
+ # It's a rev number
+ return False
+ except ValueError:
+ pass
+ return hashre.match(symbol)
+
+def gethashsymbols(tree, maxrev):
+ # Returns the list of symbols of the tree that look like hashes
+ # for example for the revset 3::abe3ff it will return ('abe3ff')
+ if not tree:
+ return []
+
+ results = []
+ if len(tree) == 2 and tree[0] == "symbol":
+ results.append(tree[1])
+ elif tree[0] == "func" and tree[1] == _listtuple:
+ # the optimiser will group sequence of hash request
+ results += tree[2][1].split('\0')
+ elif len(tree) >= 3:
+ for subtree in tree[1:]:
+ results += gethashsymbols(subtree, maxrev)
+ # return directly, we don't need to filter symbols again
+ return results
+ return [s for s in results if _ishashsymbol(s, maxrev)]
+
+def _posttreebuilthook(orig, tree, repo):
+ # This is use to enabled direct hash access
+ # We extract the symbols that look like hashes and add them to the
+ # explicitaccess set
+ orig(tree, repo)
+ filternm = ""
+ if repo is not None:
+ filternm = repo.filtername
+ if filternm is not None and filternm.startswith('visible-directaccess'):
+ prelength = len(repo._explicitaccess)
+ accessbefore = set(repo._explicitaccess)
+ cl = repo.unfiltered().changelog
+ repo.symbols = gethashsymbols(tree, len(cl))
+ for node in repo.symbols:
+ try:
+ node = cl._partialmatch(node)
+ except error.LookupError:
+ node = None
+ if node is not None:
+ rev = cl.rev(node)
+ if rev not in repo.changelog:
+ repo._explicitaccess.add(rev)
+ if prelength != len(repo._explicitaccess):
+ if repo.filtername != 'visible-directaccess-nowarn':
+ unhiddencommits = repo._explicitaccess - accessbefore
+ repo.ui.warn(_("Warning: accessing hidden changesets %s "
+ "for write operation\n") %
+ (",".join([str(repo.unfiltered()[l])
+ for l in unhiddencommits])))
+ repo.invalidatevolatilesets()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/hack/drophack.py Tue Feb 28 17:27:44 2017 +0100
@@ -0,0 +1,163 @@
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+'''This extension add a hacky command to drop changeset during review
+
+This extension is intended as a temporary hack to allow Matt Mackall to use
+evolve in the Mercurial review it self. You should probably not use it if your
+name is not Matt Mackall.
+'''
+
+import os
+import time
+import contextlib
+
+from mercurial.i18n import _
+from mercurial import cmdutil
+from mercurial import repair
+from mercurial import scmutil
+from mercurial import lock as lockmod
+from mercurial import util
+from mercurial import commands
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+
+@contextlib.contextmanager
+def timed(ui, caption):
+ ostart = os.times()
+ cstart = time.time()
+ yield
+ cstop = time.time()
+ ostop = os.times()
+ wall = cstop - cstart
+ user = ostop[0] - ostart[0]
+ sys = ostop[1] - ostart[1]
+ comb = user + sys
+ ui.write("%s: wall %f comb %f user %f sys %f\n"
+ % (caption, wall, comb, user, sys))
+
+def obsmarkerchainfrom(obsstore, nodes):
+ """return all marker chain starting from node
+
+ Starting from mean "use as successors"."""
+ # XXX need something smarter for descendant of bumped changeset
+ seennodes = set(nodes)
+ seenmarkers = set()
+ pendingnodes = set(nodes)
+ precursorsmarkers = obsstore.precursors
+ while pendingnodes:
+ current = pendingnodes.pop()
+ new = set()
+ for precmark in precursorsmarkers.get(current, ()):
+ if precmark in seenmarkers:
+ continue
+ seenmarkers.add(precmark)
+ new.add(precmark[0])
+ yield precmark
+ new -= seennodes
+ pendingnodes |= new
+
+def stripmarker(ui, repo, markers):
+ """remove <markers> from the repo obsstore
+
+ The old obsstore content is saved in a `obsstore.prestrip` file
+ """
+ repo = repo.unfiltered()
+ repo.destroying()
+ oldmarkers = list(repo.obsstore._all)
+ util.rename(repo.sjoin('obsstore'),
+ repo.join('obsstore.prestrip'))
+ del repo.obsstore # drop the cache
+ newstore = repo.obsstore
+ assert not newstore # should be empty after rename
+ newmarkers = [m for m in oldmarkers if m not in markers]
+ tr = repo.transaction('drophack')
+ try:
+ newstore.add(tr, newmarkers)
+ tr.close()
+ finally:
+ tr.release()
+ repo.destroyed()
+
+
+@command('drop', [('r', 'rev', [], 'revision to update')], _('[-r] revs'))
+def cmddrop(ui, repo, *revs, **opts):
+ """I'm hacky do not use me!
+
+ This command strip a changeset, its precursors and all obsolescence marker
+ associated to its chain.
+
+ There is no way to limit the extend of the purge yet. You may have to
+ repull from other source to get some changeset and obsolescence marker
+ back.
+
+ This intended for Matt Mackall usage only. do not use me.
+ """
+ revs = list(revs)
+ revs.extend(opts['rev'])
+ if not revs:
+ revs = ['.']
+ # get the changeset
+ revs = scmutil.revrange(repo, revs)
+ if not revs:
+ ui.write_err('no revision to drop\n')
+ return 1
+ # lock from the beginning to prevent race
+ wlock = lock = None
+ try:
+ wlock = repo.wlock()
+ lock = repo.lock()
+ # check they have no children
+ if repo.revs('%ld and public()', revs):
+ ui.write_err('cannot drop public revision')
+ return 1
+ if repo.revs('children(%ld) - %ld', revs, revs):
+ ui.write_err('cannot drop revision with children')
+ return 1
+ if repo.revs('. and %ld', revs):
+ newrevs = repo.revs('max(::. - %ld)', revs)
+ if newrevs:
+ assert len(newrevs) == 1
+ newrev = newrevs.first()
+ else:
+ newrev = -1
+ commands.update(ui, repo, newrev)
+ ui.status(_('working directory now at %s\n') % repo[newrev])
+ # get all markers and successors up to root
+ nodes = [repo[r].node() for r in revs]
+ with timed(ui, 'search obsmarker'):
+ markers = set(obsmarkerchainfrom(repo.obsstore, nodes))
+ ui.write('%i obsmarkers found\n' % len(markers))
+ cl = repo.unfiltered().changelog
+ with timed(ui, 'search nodes'):
+ allnodes = set(nodes)
+ allnodes.update(m[0] for m in markers if cl.hasnode(m[0]))
+ ui.write('%i nodes found\n' % len(allnodes))
+ cl = repo.changelog
+ visiblenodes = set(n for n in allnodes if cl.hasnode(n))
+ # check constraint again
+ if repo.revs('%ln and public()', visiblenodes):
+ ui.write_err('cannot drop public revision')
+ return 1
+ if repo.revs('children(%ln) - %ln', visiblenodes, visiblenodes):
+ ui.write_err('cannot drop revision with children')
+ return 1
+
+ if markers:
+ # strip them
+ with timed(ui, 'strip obsmarker'):
+ stripmarker(ui, repo, markers)
+ # strip the changeset
+ with timed(ui, 'strip nodes'):
+ repair.strip(ui, repo, list(allnodes), backup="all",
+ topic='drophack')
+
+ finally:
+ lockmod.release(lock, wlock)
+
+ # rewrite the whole file.
+ # print data.
+ # - time to compute the chain
+ # - time to strip the changeset
+ # - time to strip the obs marker.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/hack/inhibit.py Tue Feb 28 17:27:44 2017 +0100
@@ -0,0 +1,310 @@
+"""reduce the changesets evolution feature scope for early and noob friendly ui
+
+the full scale changeset evolution have some massive bleeding edge and it is
+very easy for people not very intimate with the concept to end up in intricate
+situation. in order to get some of the benefit sooner, this extension is
+disabling some of the less polished aspect of evolution. it should gradually
+get thinner and thinner as changeset evolution will get more polished. this
+extension is only recommended for large scale organisations. individual user
+should probably stick on using evolution in its current state, understand its
+concept and provide feedback
+
+This extension provides the ability to "inhibit" obsolescence markers. obsolete
+revision can be cheaply brought back to life that way.
+However as the inhibitor are not fitting in an append only model, this is
+incompatible with sharing mutable history.
+"""
+from mercurial import localrepo
+from mercurial import obsolete
+from mercurial import extensions
+from mercurial import cmdutil
+from mercurial import error
+from mercurial import scmutil
+from mercurial import commands
+from mercurial import lock as lockmod
+from mercurial import bookmarks
+from mercurial import util
+from mercurial.i18n import _
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+
+def _inhibitenabled(repo):
+ return util.safehasattr(repo, '_obsinhibit')
+
+def reposetup(ui, repo):
+
+ class obsinhibitedrepo(repo.__class__):
+
+ @localrepo.storecache('obsinhibit')
+ def _obsinhibit(self):
+ # XXX we should make sure it is invalidated by transaction failure
+ obsinhibit = set()
+ raw = self.svfs.tryread('obsinhibit')
+ for i in xrange(0, len(raw), 20):
+ obsinhibit.add(raw[i:i + 20])
+ return obsinhibit
+
+ def commit(self, *args, **kwargs):
+ newnode = super(obsinhibitedrepo, self).commit(*args, **kwargs)
+ if newnode is not None:
+ _inhibitmarkers(repo, [newnode])
+ return newnode
+
+ repo.__class__ = obsinhibitedrepo
+
+def _update(orig, ui, repo, *args, **kwargs):
+ """
+ When moving to a commit we want to inhibit any obsolete commit affecting
+ the changeset we are updating to. In other words we don't want any visible
+ commit to be obsolete.
+ """
+ wlock = None
+ try:
+ # Evolve is running a hook on lock release to display a warning message
+ # if the workind dir's parent is obsolete.
+ # We take the lock here to make sure that we inhibit the parent before
+ # that hook get a chance to run.
+ wlock = repo.wlock()
+ res = orig(ui, repo, *args, **kwargs)
+ newhead = repo['.'].node()
+ _inhibitmarkers(repo, [newhead])
+ return res
+ finally:
+ lockmod.release(wlock)
+
+def _bookmarkchanged(orig, bkmstoreinst, *args, **kwargs):
+ """ Add inhibition markers to every obsolete bookmarks """
+ repo = bkmstoreinst._repo
+ bkmstorenodes = [repo[v].node() for v in bkmstoreinst.values()]
+ _inhibitmarkers(repo, bkmstorenodes)
+ return orig(bkmstoreinst, *args, **kwargs)
+
+def _bookmark(orig, ui, repo, *bookmarks, **opts):
+ """ Add a -D option to the bookmark command, map it to prune -B """
+ haspruneopt = opts.get('prune', False)
+ if not haspruneopt:
+ return orig(ui, repo, *bookmarks, **opts)
+ elif opts.get('rename'):
+ raise error.Abort('Cannot use both -m and -D')
+ elif len(bookmarks) == 0:
+ hint = _('make sure to put a space between -D and your bookmark name')
+ raise error.Abort(_('Error, please check your command'), hint=hint)
+
+ # Call prune -B
+ evolve = extensions.find('evolve')
+ optsdict = {
+ 'new': [],
+ 'succ': [],
+ 'rev': [],
+ 'bookmark': bookmarks,
+ 'keep': None,
+ 'biject': False,
+ }
+ evolve.cmdprune(ui, repo, **optsdict)
+
+# obsolescence inhibitor
+########################
+
+def _schedulewrite(tr, obsinhibit):
+ """Make sure on disk content will be updated on transaction commit"""
+ def writer(fp):
+ """Serialize the inhibited list to disk.
+ """
+ raw = ''.join(obsinhibit)
+ fp.write(raw)
+ tr.addfilegenerator('obsinhibit', ('obsinhibit',), writer)
+ tr.hookargs['obs_inbihited'] = '1'
+
+def _filterpublic(repo, nodes):
+ """filter out inhibitor on public changeset
+
+ Public changesets are already immune to obsolescence"""
+ getrev = repo.changelog.nodemap.get
+ getphase = repo._phasecache.phase
+ return (n for n in nodes
+ if getrev(n) is not None and getphase(repo, getrev(n)))
+
+def _inhibitmarkers(repo, nodes):
+ """add marker inhibitor for all obsolete revision under <nodes>
+
+ Content of <nodes> and all mutable ancestors are considered. Marker for
+ obsolete revision only are created.
+ """
+ if not _inhibitenabled(repo):
+ return
+
+ # we add (non public()) as a lower boundary to
+ # - use the C code in 3.6 (no ancestors in C as this is written)
+ # - restrict the search space. Otherwise, the ancestors can spend a lot of
+ # time iterating if you have a check very low in the repo. We do not need
+ # to iterate over tens of thousand of public revisions with higher
+ # revision number
+ #
+ # In addition, the revset logic could be made significantly smarter here.
+ newinhibit = repo.revs('(not public())::%ln and obsolete()', nodes)
+ if newinhibit:
+ node = repo.changelog.node
+ lock = tr = None
+ try:
+ lock = repo.lock()
+ tr = repo.transaction('obsinhibit')
+ repo._obsinhibit.update(node(r) for r in newinhibit)
+ _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
+ repo.invalidatevolatilesets()
+ tr.close()
+ finally:
+ lockmod.release(tr, lock)
+
+def _deinhibitmarkers(repo, nodes):
+ """lift obsolescence inhibition on a set of nodes
+
+ This will be triggered when inhibited nodes received new obsolescence
+ markers. Otherwise the new obsolescence markers would also be inhibited.
+ """
+ if not _inhibitenabled(repo):
+ return
+
+ deinhibited = repo._obsinhibit & set(nodes)
+ if deinhibited:
+ tr = repo.transaction('obsinhibit')
+ try:
+ repo._obsinhibit -= deinhibited
+ _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
+ repo.invalidatevolatilesets()
+ tr.close()
+ finally:
+ tr.release()
+
+def _createmarkers(orig, repo, relations, flag=0, date=None, metadata=None):
+ """wrap markers create to make sure we de-inhibit target nodes"""
+ # wrapping transactio to unify the one in each function
+ lock = tr = None
+ try:
+ lock = repo.lock()
+ tr = repo.transaction('add-obsolescence-marker')
+ orig(repo, relations, flag, date, metadata)
+ precs = (r[0].node() for r in relations)
+ _deinhibitmarkers(repo, precs)
+ tr.close()
+ finally:
+ lockmod.release(tr, lock)
+
+def _filterobsoleterevswrap(orig, repo, rebasesetrevs, *args, **kwargs):
+ repo._notinhibited = rebasesetrevs
+ try:
+ repo.invalidatevolatilesets()
+ r = orig(repo, rebasesetrevs, *args, **kwargs)
+ finally:
+ del repo._notinhibited
+ repo.invalidatevolatilesets()
+ return r
+
+def transactioncallback(orig, repo, desc, *args, **kwargs):
+ """ Wrap localrepo.transaction to inhibit new obsolete changes """
+ def inhibitposttransaction(transaction):
+ # At the end of the transaction we catch all the new visible and
+ # obsolete commit to inhibit them
+ visibleobsolete = repo.revs('obsolete() - hidden()')
+ ignoreset = set(getattr(repo, '_rebaseset', []))
+ ignoreset |= set(getattr(repo, '_obsoletenotrebased', []))
+ visibleobsolete = list(r for r in visibleobsolete if r not in ignoreset)
+ if visibleobsolete:
+ _inhibitmarkers(repo, [repo[r].node() for r in visibleobsolete])
+ transaction = orig(repo, desc, *args, **kwargs)
+ if desc != 'strip' and _inhibitenabled(repo):
+ transaction.addpostclose('inhibitposttransaction',
+ inhibitposttransaction)
+ return transaction
+
+
+# We wrap these two functions to address the following scenario:
+# - Assuming that we have markers between commits in the rebase set and
+# destination and that these markers are inhibited
+# - At the end of the rebase the nodes are still visible because rebase operate
+# without inhibition and skip these nodes
+# We keep track in repo._obsoletenotrebased of the obsolete commits skipped by
+# the rebase and lift the inhibition in the end of the rebase.
+
+def _computeobsoletenotrebased(orig, repo, *args, **kwargs):
+ r = orig(repo, *args, **kwargs)
+ repo._obsoletenotrebased = r.keys()
+ return r
+
+def _clearrebased(orig, ui, repo, *args, **kwargs):
+ r = orig(ui, repo, *args, **kwargs)
+ tonode = repo.changelog.node
+ if util.safehasattr(repo, '_obsoletenotrebased'):
+ _deinhibitmarkers(repo, [tonode(k) for k in repo._obsoletenotrebased])
+ return r
+
+
+def extsetup(ui):
+ # lets wrap the computation of the obsolete set
+ # We apply inhibition there
+ obsfunc = obsolete.cachefuncs['obsolete']
+ def _computeobsoleteset(repo):
+ """remove any inhibited nodes from the obsolete set
+
+ This will trickle down to other part of mercurial (hidden, log, etc)"""
+ obs = obsfunc(repo)
+ if _inhibitenabled(repo):
+ getrev = repo.changelog.nodemap.get
+ blacklist = getattr(repo, '_notinhibited', set())
+ for n in repo._obsinhibit:
+ if getrev(n) not in blacklist:
+ obs.discard(getrev(n))
+ return obs
+ try:
+ extensions.find('directaccess')
+ except KeyError:
+ errormsg = _('cannot use inhibit without the direct access extension\n')
+ hint = _("(please enable it or inhibit won\'t work)\n")
+ ui.warn(errormsg)
+ ui.warn(hint)
+ return
+
+ # Wrapping this to inhibit obsolete revs resulting from a transaction
+ extensions.wrapfunction(localrepo.localrepository,
+ 'transaction', transactioncallback)
+
+ obsolete.cachefuncs['obsolete'] = _computeobsoleteset
+ # wrap create marker to make it able to lift the inhibition
+ extensions.wrapfunction(obsolete, 'createmarkers', _createmarkers)
+ # drop divergence computation since it is incompatible with "light revive"
+ obsolete.cachefuncs['divergent'] = lambda repo: set()
+ # drop bumped computation since it is incompatible with "light revive"
+ obsolete.cachefuncs['bumped'] = lambda repo: set()
+ # wrap update to make sure that no obsolete commit is visible after an
+ # update
+ extensions.wrapcommand(commands.table, 'update', _update)
+ try:
+ rebase = extensions.find('rebase')
+ if rebase:
+ if util.safehasattr(rebase, '_filterobsoleterevs'):
+ extensions.wrapfunction(rebase,
+ '_filterobsoleterevs',
+ _filterobsoleterevswrap)
+ extensions.wrapfunction(rebase, 'clearrebased', _clearrebased)
+ if util.safehasattr(rebase, '_computeobsoletenotrebased'):
+ extensions.wrapfunction(rebase,
+ '_computeobsoletenotrebased',
+ _computeobsoletenotrebased)
+
+ except KeyError:
+ pass
+ # There are two ways to save bookmark changes during a transation, we
+ # wrap both to add inhibition markers.
+ extensions.wrapfunction(bookmarks.bmstore, 'recordchange', _bookmarkchanged)
+ if getattr(bookmarks.bmstore, 'write', None) is not None:# mercurial < 3.9
+ extensions.wrapfunction(bookmarks.bmstore, 'write', _bookmarkchanged)
+ # Add bookmark -D option
+ entry = extensions.wrapcommand(commands.table, 'bookmark', _bookmark)
+ entry[1].append(('D','prune',None,
+ _('delete the bookmark and prune the commits underneath')))
+
+@command('debugobsinhibit', [], '')
+def cmddebugobsinhibit(ui, repo, *revs):
+ """inhibit obsolescence markers effect on a set of revs"""
+ nodes = (repo[r].node() for r in scmutil.revrange(repo, revs))
+ _inhibitmarkers(repo, nodes)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/legacy.py Tue Feb 28 17:27:44 2017 +0100
@@ -0,0 +1,163 @@
+# 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.
+"""Deprecated extension that formerly introduced "Changeset Obsolescence".
+
+This concept is now partially in Mercurial core (starting with Mercurial 2.3).
+The remaining logic has been grouped with the evolve extension.
+
+Some code remains in this extensions to detect and convert prehistoric format
+of obsolete marker than early user may have create. Keep it enabled if you
+were such user.
+"""
+
+from mercurial import error
+
+try:
+ from mercurial import obsolete
+except ImportError:
+ raise error.Abort('Obsolete extension requires Mercurial 2.3 (or later)')
+
+import sys
+import json
+
+from mercurial import cmdutil
+from mercurial.i18n import _
+from mercurial.node import bin, nullid
+from mercurial import util
+
+
+#####################################################################
+### Older format management ###
+#####################################################################
+
+# Code related to detection and management of older legacy format never
+# handled by core
+
+
+def reposetup(ui, repo):
+ """Detect that a repo still contains some old obsolete format
+ """
+ if not repo.local():
+ return
+ evolveopts = ui.configlist('experimental', 'evolution')
+ if not evolveopts:
+ evolveopts = 'all'
+ ui.setconfig('experimental', 'evolution', evolveopts)
+ for arg in sys.argv:
+ if 'debugc' in arg:
+ break
+ else:
+ data = repo.opener.tryread('obsolete-relations')
+ if not data:
+ data = repo.svfs.tryread('obsoletemarkers')
+ if data:
+ raise error.Abort('old format of obsolete marker detected!\n'
+ 'run `hg debugconvertobsolete` once.')
+
+def _obsdeserialize(flike):
+ """read a file like object serialized with _obsserialize
+
+ this deserialize into a {subject -> objects} mapping
+
+ this was the very first format ever."""
+ rels = {}
+ for line in flike:
+ subhex, objhex = line.split()
+ subnode = bin(subhex)
+ if subnode == nullid:
+ subnode = None
+ rels.setdefault(subnode, set()).add(bin(objhex))
+ return rels
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+@command('debugconvertobsolete', [], '')
+def cmddebugconvertobsolete(ui, repo):
+ """import markers from an .hg/obsolete-relations file"""
+ cnt = 0
+ err = 0
+ l = repo.lock()
+ some = False
+ try:
+ unlink = []
+ tr = repo.transaction('convert-obsolete')
+ try:
+ repo._importoldobsolete = True
+ store = repo.obsstore
+ ### very first format
+ try:
+ f = repo.opener('obsolete-relations')
+ try:
+ some = True
+ for line in f:
+ subhex, objhex = line.split()
+ suc = bin(subhex)
+ prec = bin(objhex)
+ sucs = (suc==nullid) and [] or [suc]
+ meta = {
+ 'date': '%i %i' % util.makedate(),
+ 'user': ui.username(),
+ }
+ try:
+ store.create(tr, prec, sucs, 0, metadata=meta)
+ cnt += 1
+ except ValueError:
+ repo.ui.write_err("invalid old marker line: %s"
+ % (line))
+ err += 1
+ finally:
+ f.close()
+ unlink.append(repo.join('obsolete-relations'))
+ except IOError:
+ pass
+ ### second (json) format
+ data = repo.svfs.tryread('obsoletemarkers')
+ if data:
+ some = True
+ for oldmark in json.loads(data):
+ del oldmark['id'] # dropped for now
+ del oldmark['reason'] # unused until then
+ oldobject = str(oldmark.pop('object'))
+ oldsubjects = [str(s) for s in oldmark.pop('subjects', [])]
+ LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError)
+ if len(oldobject) != 40:
+ try:
+ oldobject = repo[oldobject].node()
+ except LOOKUP_ERRORS:
+ pass
+ if any(len(s) != 40 for s in oldsubjects):
+ try:
+ oldsubjects = [repo[s].node() for s in oldsubjects]
+ except LOOKUP_ERRORS:
+ pass
+
+ oldmark['date'] = '%i %i' % tuple(oldmark['date'])
+ meta = dict((k.encode('utf-8'), v.encode('utf-8'))
+ for k, v in oldmark.iteritems())
+ try:
+ succs = [bin(n) for n in oldsubjects]
+ succs = [n for n in succs if n != nullid]
+ store.create(tr, bin(oldobject), succs,
+ 0, metadata=meta)
+ cnt += 1
+ except ValueError:
+ repo.ui.write_err("invalid marker %s -> %s\n"
+ % (oldobject, oldsubjects))
+ err += 1
+ unlink.append(repo.sjoin('obsoletemarkers'))
+ tr.close()
+ for path in unlink:
+ util.unlink(path)
+ finally:
+ tr.release()
+ finally:
+ del repo._importoldobsolete
+ l.release()
+ if not some:
+ ui.warn(_('nothing to do\n'))
+ ui.status('%i obsolete marker converted\n' % cnt)
+ if err:
+ ui.write_err('%i conversion failed. check you graph!\n' % err)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/serveronly.py Tue Feb 28 17:27:44 2017 +0100
@@ -0,0 +1,321 @@
+'''enable experimental obsolescence feature of Mercurial
+
+OBSOLESCENCE IS AN EXPERIMENTAL FEATURE MAKE SURE YOU UNDERSTOOD THE INVOLVED
+CONCEPT BEFORE USING IT.
+
+/!\ THIS EXTENSION IS INTENDED FOR SERVER SIDE ONLY USAGE /!\
+
+For client side usages it is recommended to use the evolve extension for
+improved user interface.'''
+
+testedwith = '3.3 3.4-rc'
+buglink = 'https://bz.mercurial-scm.org/'
+
+import mercurial.obsolete
+
+import hashlib
+import struct
+from mercurial import error
+from mercurial import util
+from mercurial import wireproto
+from mercurial import extensions
+from mercurial import obsolete
+from cStringIO import StringIO
+from mercurial import node
+from mercurial.hgweb import hgweb_mod
+from mercurial import bundle2
+from mercurial import localrepo
+from mercurial import exchange
+from mercurial import node
+_pack = struct.pack
+
+gboptslist = gboptsmap = None
+try:
+ from mercurial import obsolete
+ from mercurial import wireproto
+ gboptslist = getattr(wireproto, 'gboptslist', None)
+ gboptsmap = getattr(wireproto, 'gboptsmap', None)
+except (ImportError, AttributeError):
+ raise error.Abort('Your Mercurial is too old for this version of Evolve\n'
+ 'requires version 3.0.1 or above')
+
+# Start of simple4server specific content
+
+from mercurial import pushkey
+
+# specific content also include the wrapping int extsetup
+def _nslist(orig, repo):
+ rep = orig(repo)
+ if not repo.ui.configbool('__temporary__', 'advertiseobsolete', True):
+ rep.pop('obsolete')
+ return rep
+
+# End of simple4server specific content
+
+
+
+# from evolve extension: 1a23c7c52a43
+def srv_pushobsmarkers(repo, proto):
+ """That receives a stream of markers and apply then to the repo"""
+ fp = StringIO()
+ proto.redirect()
+ proto.getfile(fp)
+ data = fp.getvalue()
+ fp.close()
+ lock = repo.lock()
+ try:
+ tr = repo.transaction('pushkey: obsolete markers')
+ try:
+ repo.obsstore.mergemarkers(tr, data)
+ tr.close()
+ finally:
+ tr.release()
+ finally:
+ lock.release()
+ repo.hook('evolve_pushobsmarkers')
+ return wireproto.pushres(0)
+
+# from evolve extension: 1a23c7c52a43
+def _getobsmarkersstream(repo, heads=None, common=None):
+ """Get a binary stream for all markers relevant to `::<heads> - ::<common>`
+ """
+ revset = ''
+ args = []
+ repo = repo.unfiltered()
+ if heads is None:
+ revset = 'all()'
+ elif heads:
+ revset += "(::%ln)"
+ args.append(heads)
+ else:
+ assert False, 'pulling no heads?'
+ if common:
+ revset += ' - (::%ln)'
+ args.append(common)
+ nodes = [c.node() for c in repo.set(revset, *args)]
+ markers = repo.obsstore.relevantmarkers(nodes)
+ obsdata = StringIO()
+ for chunk in obsolete.encodemarkers(markers, True):
+ obsdata.write(chunk)
+ obsdata.seek(0)
+ return obsdata
+
+if not util.safehasattr(obsolete.obsstore, 'relevantmarkers'):
+ # from evolve extension: 1a23c7c52a43
+ class pruneobsstore(obsolete.obsstore):
+ """And extended obsstore class that read parent information from v1
+ format
+
+ Evolve extension adds parent information in prune marker.
+ We use it to make markers relevant to pushed changeset."""
+
+ def __init__(self, *args, **kwargs):
+ self.prunedchildren = {}
+ return super(pruneobsstore, self).__init__(*args, **kwargs)
+
+ def _load(self, markers):
+ markers = self._prunedetectingmarkers(markers)
+ return super(pruneobsstore, self)._load(markers)
+
+
+ def _prunedetectingmarkers(self, markers):
+ for m in markers:
+ if not m[1]: # no successors
+ meta = obsolete.decodemeta(m[3])
+ if 'p1' in meta:
+ p1 = node.bin(meta['p1'])
+ self.prunedchildren.setdefault(p1, set()).add(m)
+ if 'p2' in meta:
+ p2 = node.bin(meta['p2'])
+ self.prunedchildren.setdefault(p2, set()).add(m)
+ yield m
+
+ # from evolve extension: 1a23c7c52a43
+ def relevantmarkers(self, nodes):
+ """return a set of all obsolescence marker relevant to a set of node.
+
+ "relevant" to a set of node mean:
+
+ - marker that use this changeset as successors
+ - prune marker of direct children on this changeset.
+ - recursive application of the two rules on precursors of these markers
+
+ It is a set so you cannot rely on order"""
+ seennodes = set(nodes)
+ seenmarkers = set()
+ pendingnodes = set(nodes)
+ precursorsmarkers = self.precursors
+ prunedchildren = self.prunedchildren
+ while pendingnodes:
+ direct = set()
+ for current in pendingnodes:
+ direct.update(precursorsmarkers.get(current, ()))
+ direct.update(prunedchildren.get(current, ()))
+ direct -= seenmarkers
+ pendingnodes = set([m[0] for m in direct])
+ seenmarkers |= direct
+ pendingnodes -= seennodes
+ seennodes |= pendingnodes
+ return seenmarkers
+
+# The wireproto.streamres API changed, handling chunking and compression
+# directly. Handle either case.
+if util.safehasattr(wireproto.abstractserverproto, 'groupchunks'):
+ # We need to handle chunking and compression directly
+ def streamres(d, proto):
+ return wireproto.streamres(proto.groupchunks(d))
+else:
+ # Leave chunking and compression to streamres
+ def streamres(d, proto):
+ return wireproto.streamres(reader=d, v1compressible=True)
+
+# from evolve extension: cf35f38d6a10
+def srv_pullobsmarkers(repo, proto, others):
+ """serves a binary stream of markers.
+
+ Serves relevant to changeset between heads and common. The stream is prefix
+ by a -string- representation of an integer. This integer is the size of the
+ stream."""
+ opts = wireproto.options('', ['heads', 'common'], others)
+ for k, v in opts.iteritems():
+ if k in ('heads', 'common'):
+ opts[k] = wireproto.decodelist(v)
+ obsdata = _getobsmarkersstream(repo, **opts)
+ finaldata = StringIO()
+ obsdata = obsdata.getvalue()
+ finaldata.write('%20i' % len(obsdata))
+ finaldata.write(obsdata)
+ finaldata.seek(0)
+ return streamres(finaldata, proto)
+
+
+# from evolve extension: 3249814dabd1
+def _obsrelsethashtreefm0(repo):
+ return _obsrelsethashtree(repo, obsolete._fm0encodeonemarker)
+
+# from evolve extension: 3249814dabd1
+def _obsrelsethashtreefm1(repo):
+ return _obsrelsethashtree(repo, obsolete._fm1encodeonemarker)
+
+# from evolve extension: 3249814dabd1
+def _obsrelsethashtree(repo, encodeonemarker):
+ cache = []
+ unfi = repo.unfiltered()
+ markercache = {}
+ for i in unfi:
+ ctx = unfi[i]
+ entry = 0
+ sha = hashlib.sha1()
+ # add data from p1
+ for p in ctx.parents():
+ p = p.rev()
+ if p < 0:
+ p = node.nullid
+ else:
+ p = cache[p][1]
+ if p != node.nullid:
+ entry += 1
+ sha.update(p)
+ tmarkers = repo.obsstore.relevantmarkers([ctx.node()])
+ if tmarkers:
+ bmarkers = []
+ for m in tmarkers:
+ if not m in markercache:
+ markercache[m] = encodeonemarker(m)
+ bmarkers.append(markercache[m])
+ bmarkers.sort()
+ for m in bmarkers:
+ entry += 1
+ sha.update(m)
+ if entry:
+ cache.append((ctx.node(), sha.digest()))
+ else:
+ cache.append((ctx.node(), node.nullid))
+ return cache
+
+# from evolve extension: 3249814dabd1
+def _obshash(repo, nodes, version=0):
+ if version == 0:
+ hashs = _obsrelsethashtreefm0(repo)
+ elif version ==1:
+ hashs = _obsrelsethashtreefm1(repo)
+ else:
+ assert False
+ nm = repo.changelog.nodemap
+ revs = [nm.get(n) for n in nodes]
+ return [r is None and node.nullid or hashs[r][1] for r in revs]
+
+# from evolve extension: 3249814dabd1
+def srv_obshash(repo, proto, nodes):
+ return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes)))
+
+# from evolve extension: 3249814dabd1
+def srv_obshash1(repo, proto, nodes):
+ return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes),
+ version=1))
+
+# from evolve extension: 3249814dabd1
+def capabilities(orig, repo, proto):
+ """wrapper to advertise new capability"""
+ caps = orig(repo, proto)
+ advertise = repo.ui.configbool('__temporary__', 'advertiseobsolete', True)
+ if obsolete.isenabled(repo, obsolete.exchangeopt) and advertise:
+ caps += ' _evoext_pushobsmarkers_0'
+ caps += ' _evoext_pullobsmarkers_0'
+ caps += ' _evoext_obshash_0'
+ caps += ' _evoext_obshash_1'
+ caps += ' _evoext_getbundle_obscommon'
+ return caps
+
+def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs):
+ if 'evo_obscommon' not in kwargs:
+ return orig(bundler, repo, source, **kwargs)
+
+ heads = kwargs.get('heads')
+ if 'evo_obscommon' not in kwargs:
+ return orig(bundler, repo, source, **kwargs)
+
+ if kwargs.get('obsmarkers', False):
+ if heads is None:
+ heads = repo.heads()
+ obscommon = kwargs.get('evo_obscommon', ())
+ obsset = repo.set('::%ln - ::%ln', heads, obscommon)
+ subset = [c.node() for c in obsset]
+ markers = repo.obsstore.relevantmarkers(subset)
+ exchange.buildobsmarkerspart(bundler, markers)
+
+# from evolve extension: 10867a8e27c6
+# heavily modified
+def extsetup(ui):
+ localrepo.moderncaps.add('_evoext_b2x_obsmarkers_0')
+ gboptsmap['evo_obscommon'] = 'nodes'
+ if not util.safehasattr(obsolete.obsstore, 'relevantmarkers'):
+ obsolete.obsstore = pruneobsstore
+ obsolete.obsstore.relevantmarkers = relevantmarkers
+ hgweb_mod.perms['evoext_pushobsmarkers_0'] = 'push'
+ hgweb_mod.perms['evoext_pullobsmarkers_0'] = 'pull'
+ hgweb_mod.perms['evoext_obshash'] = 'pull'
+ wireproto.commands['evoext_pushobsmarkers_0'] = (srv_pushobsmarkers, '')
+ wireproto.commands['evoext_pullobsmarkers_0'] = (srv_pullobsmarkers, '*')
+ # wrap module content
+ origfunc = exchange.getbundle2partsmapping['obsmarkers']
+ def newfunc(*args, **kwargs):
+ return _getbundleobsmarkerpart(origfunc, *args, **kwargs)
+ exchange.getbundle2partsmapping['obsmarkers'] = newfunc
+ extensions.wrapfunction(wireproto, 'capabilities', capabilities)
+ # wrap command content
+ oldcap, args = wireproto.commands['capabilities']
+ def newcap(repo, proto):
+ return capabilities(oldcap, repo, proto)
+ wireproto.commands['capabilities'] = (newcap, args)
+ wireproto.commands['evoext_obshash'] = (srv_obshash, 'nodes')
+ wireproto.commands['evoext_obshash1'] = (srv_obshash1, 'nodes')
+ # specific simple4server content
+ extensions.wrapfunction(pushkey, '_nslist', _nslist)
+ pushkey._namespaces['namespaces'] = (lambda *x: False, pushkey._nslist)
+
+def reposetup(ui, repo):
+ evolveopts = ui.configlist('experimental', 'evolution')
+ if not evolveopts:
+ evolveopts = 'all'
+ ui.setconfig('experimental', 'evolution', evolveopts)
--- a/setup.py Tue Feb 28 17:22:21 2017 +0100
+++ b/setup.py Tue Feb 28 17:27:44 2017 +0100
@@ -16,16 +16,19 @@
return line.split("'")[1]
py_modules = [
- 'hgext.evolve',
+ 'hgext3rd.evolve.serveronly'
+]
+py_packages = [
+ 'hgext3rd',
]
if os.environ.get('INCLUDE_INHIBIT'):
- py_modules.append('hgext.inhibit')
- py_modules.append('hgext.directaccess')
+ py_modules.append('hgext3rd.evolve.hack.inhibit')
+ py_modules.append('hgext3rd.evolve.hack.directaccess')
setup(
name='hg-evolve',
- version=get_version('hgext/evolve.py'),
+ version=get_version('hgext3rd/evolve/__init__.py'),
author='Pierre-Yves David',
maintainer='Pierre-Yves David',
maintainer_email='pierre-yves.david@ens-lyon.org',
@@ -34,5 +37,6 @@
long_description=open('README').read(),
keywords='hg mercurial',
license='GPLv2+',
- py_modules=py_modules
+ py_modules=py_modules,
+ packages=py_packages
)
--- a/tests/_exc-util.sh Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/_exc-util.sh Tue Feb 28 17:27:44 2017 +0100
@@ -22,7 +22,7 @@
[extensions]
hgext.strip=
EOF
-echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
mkcommit() {
echo "$1" > "$1"
--- a/tests/test-amend.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-amend.t Tue Feb 28 17:27:44 2017 +0100
@@ -2,7 +2,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ glog() {
> hg glog --template '{rev}@{branch}({phase}) {desc|firstline}\n' "$@"
--- a/tests/test-corrupt.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-corrupt.t Tue Feb 28 17:27:44 2017 +0100
@@ -15,7 +15,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" >> "$1"
> hg add "$1"
--- a/tests/test-divergent.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-divergent.t Tue Feb 28 17:27:44 2017 +0100
@@ -17,7 +17,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
--- a/tests/test-drop.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-drop.t Tue Feb 28 17:27:44 2017 +0100
@@ -3,8 +3,8 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "drophack=$(echo $(dirname $TESTDIR))/hgext/drophack.py" >> $HGRCPATH
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "drophack=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/drophack.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
--- a/tests/test-evolve-bumped.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-evolve-bumped.t Tue Feb 28 17:27:44 2017 +0100
@@ -10,7 +10,7 @@
adding a
$ cd ..
- $ evolvepath=$(echo $(dirname $TESTDIR))/hgext/evolve.py
+ $ evolvepath=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/
$ hg clone -U public private
$ cd private
$ cat >> .hg/hgrc <<EOF
--- a/tests/test-evolve-list.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-evolve-list.t Tue Feb 28 17:27:44 2017 +0100
@@ -3,7 +3,7 @@
> [extensions]
> rebase=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
Test the instability listing
$ hg init r2
--- a/tests/test-evolve-order.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-evolve-order.t Tue Feb 28 17:27:44 2017 +0100
@@ -18,7 +18,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
--- a/tests/test-evolve-split.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-evolve-split.t Tue Feb 28 17:27:44 2017 +0100
@@ -17,7 +17,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
--- a/tests/test-evolve-topic.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-evolve-topic.t Tue Feb 28 17:27:44 2017 +0100
@@ -20,7 +20,7 @@
> rebase =
> topic = $HGTEST_TOPICROOT/hgext3rd/topic/
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
--- a/tests/test-evolve.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-evolve.t Tue Feb 28 17:27:44 2017 +0100
@@ -16,7 +16,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
--- a/tests/test-import.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-import.t Tue Feb 28 17:27:44 2017 +0100
@@ -9,7 +9,7 @@
$ hg init auto-obsolete
$ cd auto-obsolete
$ echo '[extensions]' >> $HGRCPATH
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ echo A > a
$ hg commit -Am A
adding a
--- a/tests/test-inhibit.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-inhibit.t Tue Feb 28 17:27:44 2017 +0100
@@ -8,9 +8,9 @@
> rebase=
> strip=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
- $ echo "directaccess=$(echo $(dirname $TESTDIR))/hgext/directaccess.py" >> $HGRCPATH
- $ echo "inhibit=$(echo $(dirname $TESTDIR))/hgext/inhibit.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
+ $ echo "directaccess=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/directaccess.py" >> $HGRCPATH
+ $ echo "inhibit=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/inhibit.py" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
@@ -728,7 +728,7 @@
$ cat >> $HGRCPATH <<EOF
> [extensions]
> EOF
- $ echo "inhibit=$(echo $(dirname $TESTDIR))/hgext/inhibit.py" >> $HGRCPATH
+ $ echo "inhibit=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/inhibit.py" >> $HGRCPATH
Empty commit
$ hg amend
@@ -781,7 +781,7 @@
cannot use inhibit without the direct access extension
(please enable it or inhibit won't work)
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
- $ echo "directaccess=$(echo $(dirname $TESTDIR))/hgext/directaccess.py" >> $HGRCPATH
+ $ echo "directaccess=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/directaccess.py" >> $HGRCPATH
$ cd ..
hg push should not allow directaccess unless forced with --hidden
--- a/tests/test-obsconvert.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-obsconvert.t Tue Feb 28 17:27:44 2017 +0100
@@ -1,7 +1,7 @@
$ cat >> $HGRCPATH <<EOF
> [extensions]
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ hg init alpha
$ cd alpha
$ echo foo > foo
--- a/tests/test-obsolete-push.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-obsolete-push.t Tue Feb 28 17:27:44 2017 +0100
@@ -4,7 +4,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ template='{rev}:{node|short}@{branch}({separate("/", obsolete, phase)}) {desc|firstline}\n'
$ glog() {
--- a/tests/test-obsolete.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-obsolete.t Tue Feb 28 17:27:44 2017 +0100
@@ -9,7 +9,7 @@
> [extensions]
> hgext.rebase=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
--- a/tests/test-oldconvert.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-oldconvert.t Tue Feb 28 17:27:44 2017 +0100
@@ -32,7 +32,7 @@
enable the extensions
- $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH
+ $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/legacy.py" >> $HGRCPATH
$ hg glog
abort: old format of obsolete marker detected!
--- a/tests/test-options.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-options.t Tue Feb 28 17:27:44 2017 +0100
@@ -3,7 +3,7 @@
> logtemplate={rev}:{node|short}[{bookmarks}] ({obsolete}/{phase}) {desc|firstline}\n
> [extensions]
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
--- a/tests/test-prev-next.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-prev-next.t Tue Feb 28 17:27:44 2017 +0100
@@ -2,7 +2,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
hg prev -B should move active bookmark
$ hg init
--- a/tests/test-prune.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-prune.t Tue Feb 28 17:27:44 2017 +0100
@@ -3,7 +3,7 @@
> logtemplate={rev}:{node|short}[{bookmarks}] ({separate('/', obsolete ,phase)}) {desc|firstline}\n
> [extensions]
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
--- a/tests/test-sharing.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-sharing.t Tue Feb 28 17:27:44 2017 +0100
@@ -9,7 +9,7 @@
> [extensions]
> rebase =
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ hg init public
$ hg clone public test-repo
updating to branch default
--- a/tests/test-simple4server-bundle2.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-simple4server-bundle2.t Tue Feb 28 17:27:44 2017 +0100
@@ -21,7 +21,7 @@
$ hg init server
$ echo "[extensions]" >> ./server/.hg/hgrc
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/simple4server.py" >> ./server/.hg/hgrc
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/serveronly.py" >> ./server/.hg/hgrc
$ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
$ cat hg.pid >> $DAEMON_PIDS
@@ -31,7 +31,7 @@
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cat ./errors.log
$ echo "[extensions]" >> ./client/.hg/hgrc
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> ./client/.hg/hgrc
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> ./client/.hg/hgrc
$ cp -r client other
Smoke testing
--- a/tests/test-simple4server.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-simple4server.t Tue Feb 28 17:27:44 2017 +0100
@@ -24,7 +24,7 @@
$ hg init server
$ echo "[extensions]" >> ./server/.hg/hgrc
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/simple4server.py" >> ./server/.hg/hgrc
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/serveronly.py" >> ./server/.hg/hgrc
$ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
$ cat hg.pid >> $DAEMON_PIDS
@@ -34,7 +34,7 @@
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cat ./errors.log
$ echo "[extensions]" >> ./client/.hg/hgrc
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> ./client/.hg/hgrc
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> ./client/.hg/hgrc
$ cp -r client other
Smoke testing
--- a/tests/test-split.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-split.t Tue Feb 28 17:27:44 2017 +0100
@@ -20,7 +20,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
--- a/tests/test-stabilize-conflict.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-stabilize-conflict.t Tue Feb 28 17:27:44 2017 +0100
@@ -17,7 +17,7 @@
> touch.args=babar
> [extensions]
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ safesed() {
> sed "$1" "$2" > `pwd`/sed.temp
--- a/tests/test-stabilize-order.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-stabilize-order.t Tue Feb 28 17:27:44 2017 +0100
@@ -4,7 +4,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ glog() {
> hg glog --template '{rev}:{node|short}@{branch}({phase}) {desc|firstline}\n' "$@"
--- a/tests/test-stabilize-result.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-stabilize-result.t Tue Feb 28 17:27:44 2017 +0100
@@ -5,7 +5,7 @@
> hgext.rebase=
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ glog() {
> hg glog --template \
--- a/tests/test-touch.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-touch.t Tue Feb 28 17:27:44 2017 +0100
@@ -7,7 +7,7 @@
> [extensions]
> hgext.rebase=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ hg init repo
$ cd repo
--- a/tests/test-tutorial.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-tutorial.t Tue Feb 28 17:27:44 2017 +0100
@@ -59,7 +59,7 @@
$ cat >> $HGRCPATH <<EOF
> [extensions]
- > evolve = $TESTDIR/../hgext/evolve.py
+ > evolve = $TESTDIR/../hgext3rd/evolve/
> # enabling rebase is also needed for now
> rebase =
> EOF
--- a/tests/test-uncommit.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-uncommit.t Tue Feb 28 17:27:44 2017 +0100
@@ -2,7 +2,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ glog() {
> hg glog --template '{rev}:{node|short}@{branch}({separate("/", obsolete, phase)}) {desc|firstline}\n' "$@"
--- a/tests/test-unstable.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-unstable.t Tue Feb 28 17:27:44 2017 +0100
@@ -15,7 +15,7 @@
> [extensions]
> hgext.graphlog=
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
> hg add "$1"
--- a/tests/test-userguide.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-userguide.t Tue Feb 28 17:27:44 2017 +0100
@@ -32,7 +32,7 @@
> [extensions]
> rebase =
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
example 3: safe amend with "hg commit --amend" (figure 2)
$ echo 'tweak feature Y' >> file1.c
--- a/tests/test-wireproto-bundle1.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-wireproto-bundle1.t Tue Feb 28 17:27:44 2017 +0100
@@ -8,7 +8,7 @@
> publish = False
> [extensions]
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"
--- a/tests/test-wireproto.t Tue Feb 28 17:22:21 2017 +0100
+++ b/tests/test-wireproto.t Tue Feb 28 17:27:44 2017 +0100
@@ -11,7 +11,7 @@
> publish = False
> [extensions]
> EOF
- $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH
+ $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
$ mkcommit() {
> echo "$1" > "$1"