# HG changeset patch # User Pierre-Yves David # Date 1345802035 -7200 # Node ID a9c27df23129f21e7116ce1a024af4183c1b53e3 # Parent f17a0f801e0bcf8b4a5fe2f6744598251c26f437# Parent 53d7e34133377bceade8815d081ee511a28a210e Prepare 0.1.0 by merging default into stable stable is now compatible with 2.3 only. diff -r f17a0f801e0b -r a9c27df23129 .hgignore --- a/.hgignore Tue Aug 21 12:43:21 2012 +0200 +++ b/.hgignore Fri Aug 24 11:53:55 2012 +0200 @@ -5,6 +5,9 @@ ^html/ \.pyc$ ~$ +\.swp$ \.orig$ \.rej$ \.err$ +^tests/easy_run.sh$ +^build/ diff -r f17a0f801e0b -r a9c27df23129 README --- a/README Tue Aug 21 12:43:21 2012 +0200 +++ b/README Fri Aug 24 11:53:55 2012 +0200 @@ -43,6 +43,26 @@ Changelog ================== +1.0 + +- Align with Mercurial version 2.3 (drop 2.2 support). +- stabilize handle killed parent +- stabilize handle late comer +- stabilize handle conflicting +- stabilize get a --continue switch +- merge and update ignore extinct changeset in most case. +- new "troubled()" revset +- summary now reports troubles changesets +- new touch command +- new fold command +- new basic olog alias + +- rebase refuse to work on public changeset again +- rebase explicitly state that there is nothing to rebase because everything is + extinct() when that happen. +- amend now cleanly abort when --change switch is misused + + 0.7 -- 2012-08-06 - hook: work around insanely huge value in obsolete pushkey call diff -r f17a0f801e0b -r a9c27df23129 contrib/nopushpublish.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/nopushpublish.py Fri Aug 24 11:53:55 2012 +0200 @@ -0,0 +1,36 @@ +# Extension which prevent changeset to be turn public by push operation +# +# Copyright 2011 Logilab SA +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + + +from mercurial import extensions, util +from mercurial import discovery + +def checkpublish(orig, repo, remote, outgoing, *args): + + # is remote publishing? + publish = True + if 'phases' in remote.listkeys('namespaces'): + remotephases = remote.listkeys('phases') + publish = remotephases.get('publishing', False) + + npublish = 0 + if publish: + for rev in outgoing.missing: + if repo[rev].phase(): + npublish += 1 + if npublish: + repo.ui.warn("Push would publish %s changesets" % npublish) + + ret = orig(repo, remote, outgoing, *args) + if npublish: + raise util.Abort("Publishing push forbiden", + hint="Use `hg phase -p ` to manually publish them") + + return ret + +def uisetup(ui): + extensions.wrapfunction(discovery, 'checkheads', checkpublish) diff -r f17a0f801e0b -r a9c27df23129 docs/evolve-collaboration.rst --- a/docs/evolve-collaboration.rst Tue Aug 21 12:43:21 2012 +0200 +++ b/docs/evolve-collaboration.rst Fri Aug 24 11:53:55 2012 +0200 @@ -111,7 +111,7 @@ Being now done with grammar and typo fixes, Alice decides it is time to stabilize again the tree. She does:: - $ hg stabilize + $ hg evolve two times, one for each unstable descendant. The last time, hgview shows her a straight line again. Wow! that feels a bit like a diff -r f17a0f801e0b -r a9c27df23129 docs/evolve-faq.rst --- a/docs/evolve-faq.rst Tue Aug 21 12:43:21 2012 +0200 +++ b/docs/evolve-faq.rst Fri Aug 24 11:53:55 2012 +0200 @@ -96,9 +96,9 @@ Getting changes out of a commit ------------------------------------------------------------ -the ``hg uncommit`` commands allow you to rewrite the current commit to not -include change for some file. The content of target files are not altered on -disk and back as "modified":: +The ``hg uncommit`` command lets you rewrite the parent commit without +selected changed files. Target files content is not altered and +appears again as "modified":: $ hg st M babar @@ -112,8 +112,7 @@ Split a changeset ----------------------- -I you just want to split whole file, you can just use the ``uncommit`` command. - +To split on file boundaries, just use ``uncommit`` command. If you need fine-grained split, there is no official command for that yet. However, it is easily achieved by manual operation:: @@ -153,7 +152,7 @@ .. warning:: Beware that rebasing obsolete changesets will result in conflicting versions of the changesets. -Stabilize history: ``stabilize`` +Stabilize history: ``evolve`` ------------------------------------------------------------ When you rewrite (amend) a changeset with children without rewriting @@ -165,16 +164,6 @@ newest version. This is not done automatically to avoid the proliferation of useless hidden changesets. -.. warning:: ``hg stabilize`` have no --continue to use after conflict - resolution - -.. warning:: stabilization does not handle deletion yet. - -.. warning:: obsolete currently relies on changesets in secret phase - to avoid exchanging obsolete and unstable changesets. - - XXX details issue here - Fix my history afterward: ``prune -n`` ------------------------------------------------------------ @@ -209,7 +198,7 @@ You can also use a debug command - $ hg debugsuccessors + $ hg debugobsolete 5eb72dbe0cb4 e8db4aa611f6 c4cbebac3751 4f1c269eab68 @@ -223,8 +212,8 @@ Extinct changesets are hidden using the *hidden* feature of mercurial. -Only ``hg log`` and ``hgview`` support it. ``hg glog`` Only support that since -2.2. Other visual viewer don't. +Only ``hg log``, ``hg glog`` and ``hgview`` support it, other +graphical viewer do not. diff -r f17a0f801e0b -r a9c27df23129 docs/from-mq.rst --- a/docs/from-mq.rst Tue Aug 21 12:43:21 2012 +0200 +++ b/docs/from-mq.rst Fri Aug 24 11:53:55 2012 +0200 @@ -12,7 +12,7 @@ qnew ``commit`` qrefresh ``amend`` qpop ``update`` or ``qdown`` -qpush ``update`` or ``gup`` sometimes ``stabilize`` +qpush ``update`` or ``gup`` sometimes ``evolve`` qrm ``prune`` qfold ``amend -c`` (for now, ``collapse`` soon) qdiff ``odiff`` @@ -84,7 +84,7 @@ hg qref -X ```````````` -To remove change from you current commit use:: +To remove changes from you current commit use:: $ hg uncommit not-ready.txt @@ -113,7 +113,7 @@ The evolution extension adds a command to rewrite the "out of sync" changesets::: - $ hg stabilize + $ hg evolve You can also decide to do it manually using:: diff -r f17a0f801e0b -r a9c27df23129 docs/index.rst --- a/docs/index.rst Tue Aug 21 12:43:21 2012 +0200 +++ b/docs/index.rst Fri Aug 24 11:53:55 2012 +0200 @@ -16,13 +16,14 @@ The effort is split in two parts: - * The **obsolete marker** concept aims to provide an alternative to ``strip`` - to get rid of changesets. + * The **obsolescence marker** concept aims to provide an alternative to ``strip`` + to get rid of changesets. This concept have been partially implemented in + Mercurial 2.3. * The **evolve** mercurial extension rewrites history using obsolete *marker* under the hood. -The first and most important step is by far the **obsolete marker**. However +The first and most important step is by far the **obsolescence marker**. However most users will never be directly exposed to the concept. For this reason this manual starts with changeset evolution. @@ -69,19 +70,20 @@ Production ready version should hide such details to normal user. +The evolve extension require mercurial 2.3 + To enable the evolve extension use:: $ hg clone https://bitbucket.org/marmoute/mutable-history -u stable - $ mutable-history/enable.sh >> ~/.hgrc + $ echo '[extensions]\nevolve=$PWD/mutable-history/hgext/evolve.py' >> ~/.hgrc -You will probably want to use the associated version of hgview (qt viewer -recommended). :: +You will probably want to use hgview_ to visualize obsolescence. Version 1.6.2 +or later is required. - $ hg clone http://hg-lab.logilab.org/wip/hgview/ -u obsolete - $ cd hgview - $ python setup.py install --user +.. _hgview: http://www.logilab.org/project/hgview/ -Works with mercurial 2.2 +(The old version 0.7 of evolve works with mercurial 2.2 but have far less feature) + --- @@ -137,27 +139,13 @@ Here is a list of know issue that will be fixed later: -* ``hg stabilize`` does not handle merge conflict. - - You must fallback to graft or rebase when that happen. - -* rewriting conflict are not detected yet``hg stabilize`` does not - handle them. - -* ``hg update`` can move an obsolete parent - * you need to provide to `graft --continue -O` if you started you graft using `-O`. -* ``hg merge`` considers an extinct head to be a valid target, hence requiring you to manually specify target all the time. * trying to exchange obsolete marker with a static http repo will crash. -* trying to exchange a lot of obsolete markers through http crash. - -* Extinct changesets are turned secret by various commands. - * Extinct changesets are hidden using the *hidden* feature of mercurial only supported by a few commands. diff -r f17a0f801e0b -r a9c27df23129 docs/instability.rst --- a/docs/instability.rst Tue Aug 21 12:43:21 2012 +0200 +++ b/docs/instability.rst Fri Aug 24 11:53:55 2012 +0200 @@ -183,11 +183,11 @@ $ hg pull added 3 changeset +2 unstable changeset - (do you want "hg stabilize" ?) + (do you want "hg evolve" ?) working directory parent is obsolete! $ hg push outgoing unstable changesets - (use "hg stabilize" or force the push) + (use "hg evolve" or force the push) And should not not encourage people to create instability diff -r f17a0f801e0b -r a9c27df23129 docs/obs-terms.rst --- a/docs/obs-terms.rst Tue Aug 21 12:43:21 2012 +0200 +++ b/docs/obs-terms.rst Fri Aug 24 11:53:55 2012 +0200 @@ -89,16 +89,16 @@ | | | | | +--------------------------+-----------------------------+ | | | | -| | **troublesome** | **unstable** | +| | **troubled** | **unstable** | | | | | -| | *troublesome* has | *unstable* is a changeset | +| | *troubled* has | *unstable* is a changeset | | | unresolved issue caused | with obsolete ancestors. | | | by *obsolete* relations. | | | | | | | | Possible issues are | It must be rebased on a | -| | listed in the next | non *troublesome* base to | +| | listed in the next | non *troubled* base to | | | column. It is possible | solve the problem. | -| | for *troublesome* | | +| | for *troubled* | | | | changeset to combine | (possible alternative name: | | | multiple issue at once. | precarious) | | | (a.k.a. conflicting and | | @@ -106,7 +106,7 @@ | | | | | | (possible alternative | **latecomer** | | | names: unsettled, | | -| | troubled) | *latecomer* is a changeset | +| | troublesome | *latecomer* is a changeset | | | | that tries to be successor | | | | of public changesets. | | | | | @@ -141,10 +141,13 @@ | | | properly not detected as a | | | | conflict) | | | | | +| | | (possible alternative names:| +| | | clashing, rival) | +| | | | | +--------------------------+-----------------------------+ | | | | | Mutable changesets which are neither *obsolete* or | -| | *troublesome* are *"ok"*. | +| | *troubled* are *"ok"*. | | | | | | Do we really need a name for it ? *"ok"* is a pretty | | | crappy name :-/ other possibilities are: | @@ -176,7 +179,7 @@ - "ok" changeset - latecomer - - troublesome + - conflicting Any better idea are welcome. @@ -222,7 +225,7 @@ - kill: shall has funny effects when you forget "hg" in front of ``hg kill``. - obsolete: too vague, too long and too generic. -Stabilize +evolve ``````````````` Automatically resolve *troublesome* changesets @@ -231,9 +234,7 @@ This is an important name as hg pull/push will suggest it the same way it suggest merging when you add heads. -I do not like stabilize much. - alternative names: - solve (too generic ?) -- evolve (too vague) +- stabilize diff -r f17a0f801e0b -r a9c27df23129 docs/tutorials/tutorial.t --- a/docs/tutorials/tutorial.t Tue Aug 21 12:43:21 2012 +0200 +++ b/docs/tutorials/tutorial.t Fri Aug 24 11:53:55 2012 +0200 @@ -223,7 +223,7 @@ adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) - (run 'hg update' to get a working copy) + (run 'hg heads .' to see heads, 'hg merge' to merge) I now have a new heads. Note that this remote head is immutable @@ -585,7 +585,7 @@ | | @ ffa278c50818 (draft): bathroom stuff | | - o | 8a79ae8b029e (draft): bathroom stuff + x | 8a79ae8b029e (draft): bathroom stuff |/ o a2fccc2e7b08 (public): SPAM SPAM | @@ -610,7 +610,7 @@ pushing to $TESTTMP/other searching for changes abort: push includes an unstable changeset: 9ac5d0e790a2! - (use 'hg stabilize' to get a stable history (or --force to proceed)) + (use 'hg stabilize' to get a stable history or --force to ignore warnings) [255] @@ -723,7 +723,7 @@ $ hg log -G o ae45c0c3092a (draft): SPAM SPAM SPAM | - o 437efbcaf700 (draft): animals + x 437efbcaf700 (draft): animals | @ ffa278c50818 (draft): bathroom stuff | diff -r f17a0f801e0b -r a9c27df23129 enable.sh --- a/enable.sh Tue Aug 21 12:43:21 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,52 +0,0 @@ -#!/bin/sh - -here=`python -c "import os; print os.path.realpath('$0')"` -repo_root=`dirname "$here"` - -if !( hg --version -q | grep -qe 'version 2\.[2-9]' ); then - echo 'You need mercurial 2.2 or later' >&2 - exit 2 -fi - - - -cat << EOF >&2 -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -XXX Add lines below to the [extensions] section of you hgrc XXX -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - - -EOF - -cat << EOF | sed -e "s#XXXREPOPATHXXX#${repo_root}#" -[extensions] -### experimental extensions for history rewriting - -# obsolete relation support (will move in core) -obsolete=XXXREPOPATHXXX/hgext/obsolete.py - -# history rewriting UI -# needed by evolve -hgext.rebase= -evolve=XXXREPOPATHXXX/hgext/evolve.py - - -[alias] -### useful alias to check future amend result -# equivalent to the qdiff command for mq - -# diff -pdiff=diff --rev .^ - -# status -pstatus=status --rev .^ - -# diff with the previous amend -odiff=diff --rev 'limit(precursors(.),1)' --rev . -EOF - -cat << EOF >&2 - - -### check qsync-enable.sh if your need mq export too. -EOF diff -r f17a0f801e0b -r a9c27df23129 hgext/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/__init__.py Fri Aug 24 11:53:55 2012 +0200 @@ -0,0 +1,1 @@ +#f00 diff -r f17a0f801e0b -r a9c27df23129 hgext/evolve.py --- a/hgext/evolve.py Tue Aug 21 12:43:21 2012 +0200 +++ b/hgext/evolve.py Fri Aug 24 11:53:55 2012 +0200 @@ -7,23 +7,1098 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -'''a set of commands to handle changeset mutation''' +'''Extends Mercurial feature related to Changeset Evolution + +This extension Provide several command tommutate history and deal with issue it may raise. + +It also: + + - enable the "Changeset Obsolescence" feature of mercurial, + - alter core command and extension that rewrite history to use this feature, + - improve some aspect of the early implementation in 2.3 +''' +import random + +from mercurial import util + +try: + from mercurial import obsolete + if not obsolete._enabled: + obsolete._enabled = True +except ImportError: + raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)') + +from mercurial import bookmarks from mercurial import cmdutil -from mercurial import scmutil -from mercurial import node -from mercurial import error -from mercurial import extensions -from mercurial import commands -from mercurial import bookmarks -from mercurial import phases from mercurial import commands from mercurial import context from mercurial import copies -from mercurial import util +from mercurial import discovery +from mercurial import error +from mercurial import extensions +from mercurial import hg +from mercurial import localrepo +from mercurial import merge +from mercurial import node +from mercurial import phases +from mercurial import revset +from mercurial import scmutil +from mercurial import templatekw from mercurial.i18n import _ -from mercurial.commands import walkopts, commitopts, commitopts2, logopts -from mercurial import hg +from mercurial.commands import walkopts, commitopts, commitopts2 +from mercurial.node import nullid + + + +# 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 instanciated for each extension. Helper + methods are then used as decorator 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 in self._commandwrappers: + extensions.wrapcommand(commands.table, command, wrapper) + 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 in self._extcommandwrappers: + if ext not in knownexts: + e = extensions.find(ext) + if e is None: + raise util.Abort('extension %s not found' % ext) + knownexts[ext] = e.cmdtable + extensions.wrapcommand(knownexts[ext], commands, wrapper) + for c in self._extcallables: + c(ui) + + def final_reposetup(self, ui, repo): + """Method to be used as a 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._uicallables.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 revset 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): + """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) + + """ + def dec(wrapper): + if extension is None: + self._commandwrappers.append((command, wrapper)) + else: + self._extcommandwrappers.append((extension, command, wrapper)) + 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 + +##################################################################### +### Obsolescence Caching Logic ### +##################################################################### + +# Obsolescence related logic can be very slow if we don't have efficient cache. +# +# This section implements a cache mechanism that did not make it into core for +# time reason. It store meaningful set of revision related to obsolescence +# (obsolete, unstabletble ... +# +# Here is: +# +# - Computation of meaningful set, +# - Cache access logic, +# - Cache invalidation logic, +# - revset and ctx using this cache. +# + + +### Computation of meaningful set +# +# Most set can be computed with "simple" revset. + +#: { set name -> function to compute this set } mapping +#: function take a single "repo" argument. +#: +#: Use the `cachefor` decorator to register new cache function +cachefuncs = {} +def cachefor(name): + """Decorator to register a function as computing the cache for a set""" + def decorator(func): + assert name not in cachefuncs + cachefuncs[name] = func + return func + return decorator + +@cachefor('obsolete') +def _computeobsoleteset(repo): + """the set of obsolete revisions""" + obs = set() + nm = repo.changelog.nodemap + for prec in repo.obsstore.precursors: + rev = nm.get(prec) + if rev is not None: + obs.add(rev) + return set(repo.revs('%ld - public()', obs)) + +@cachefor('unstable') +def _computeunstableset(repo): + """the set of non obsolete revisions with obsolete parents""" + return set(repo.revs('(obsolete()::) - obsolete()')) + +@cachefor('suspended') +def _computesuspendedset(repo): + """the set of obsolete parents with non obsolete descendants""" + return set(repo.revs('obsolete() and obsolete()::unstable()')) + +@cachefor('extinct') +def _computeextinctset(repo): + """the set of obsolete parents without non obsolete descendants""" + return set(repo.revs('obsolete() - obsolete()::unstable()')) + +@eh.wrapfunction(obsolete.obsstore, '__init__') +def _initobsstorecache(orig, obsstore, *args, **kwargs): + """add a cache attribute to obsstore""" + obsstore.caches = {} + return orig(obsstore, *args, **kwargs) + +### Cache access + +def getobscache(repo, name): + """Return the set of revision that belong to the set + + Such access may compute the set and cache it for future use""" + if not repo.obsstore: + return () + if name not in repo.obsstore.caches: + repo.obsstore.caches[name] = cachefuncs[name](repo) + return repo.obsstore.caches[name] + +### Cache clean up +# +# To be simple we need to invalidate obsolescence cache when: +# +# - new changeset is added: +# - public phase is changed +# - obsolescence marker are added +# - strip is used a repo + + +def clearobscaches(repo): + """Remove all obsolescence related cache from a repo + + This remove all cache in obsstore is the obsstore already exist on the + repo. + + (We could be smarter here)""" + if 'obsstore' in repo._filecache: + repo.obsstore.caches.clear() + +@eh.wrapfunction(localrepo.localrepository, 'addchangegroup') # new changeset +@eh.wrapfunction(phases, 'retractboundary') # phase movement +@eh.wrapfunction(phases, 'advanceboundary') # phase movement +@eh.wrapfunction(localrepo.localrepository, 'destroyed') # strip +def wrapclearcache(orig, repo, *args, **kwargs): + try: + return orig(repo, *args, **kwargs) + finally: + # we are a bit wide here + # we could restrict to: + # advanceboundary + phase==public + # retractboundary + phase==draft + clearobscaches(repo) + +@eh.wrapfunction(obsolete.obsstore, 'add') # new marker +def clearonadd(orig, obsstore, *args, **kwargs): + try: + return orig(obsstore, *args, **kwargs) + finally: + obsstore.caches.clear() + +### Use the case +# Function in core that could benefic from the cache are overwritten by cache using version + +# changectx method + +@eh.addattr(context.changectx, 'unstable') +def unstable(ctx): + """is the changeset unstable (have obsolete ancestor)""" + if ctx.node() is None: + return False + return ctx.rev() in getobscache(ctx._repo, 'unstable') + + +@eh.addattr(context.changectx, 'extinct') +def extinct(ctx): + """is the changeset extinct by other""" + if ctx.node() is None: + return False + return ctx.rev() in getobscache(ctx._repo, 'extinct') + +# revset + +@eh.revset('obsolete') +def revsetobsolete(repo, subset, x): + """``obsolete()`` + Changeset is obsolete. + """ + args = revset.getargs(x, 0, 0, 'obsolete takes no argument') + obsoletes = getobscache(repo, 'obsolete') + return [r for r in subset if r in obsoletes] + +@eh.revset('unstable') +def revsetunstable(repo, subset, x): + """``unstable()`` + Unstable changesets are non-obsolete with obsolete ancestors. + """ + args = revset.getargs(x, 0, 0, 'unstable takes no arguments') + unstables = getobscache(repo, 'unstable') + return [r for r in subset if r in unstables] + +@eh.revset('extinct') +def revsetextinct(repo, subset, x): + """``extinct()`` + Obsolete changesets with obsolete descendants only. + """ + args = revset.getargs(x, 0, 0, 'extinct takes no arguments') + extincts = getobscache(repo, 'extinct') + return [r for r in subset if r in extincts] + +##################################################################### +### Complete troubles computation logic ### +##################################################################### + +# there is two kind of trouble not handled by core right now: +# - latecomer: (successors for public changeset) +# - conflicting: (two changeset try to succeed to the same precursors) +# +# This section add support for those two addition trouble +# +# - Cache computation +# - revset and ctx method +# - push warning + +### Cache computation +latediff = 1 # flag to prevent taking late comer fix into account + +@cachefor('latecomer') +def _computelatecomerset(repo): + """the set of rev trying to obsolete public revision""" + candidates = _allsuccessors(repo, repo.revs('public()'), + haltonflags=latediff) + query = '%ld - obsolete() - public()' + return set(repo.revs(query, candidates)) + +@cachefor('conflicting') +def _computeconflictingset(repo): + """the set of rev trying to obsolete public revision""" + conflicting = set() + obsstore = repo.obsstore + newermap = {} + for ctx in repo.set('(not public()) - obsolete()'): + prec = obsstore.successors.get(ctx.node(), ()) + toprocess = set(prec) + while toprocess: + prec = toprocess.pop()[0] + if prec not in newermap: + newermap[prec] = newerversion(repo, prec) + newer = [n for n in newermap[prec] if n] # filter kill + if len(newer) > 1: + conflicting.add(ctx.rev()) + break + toprocess.update(obsstore.successors.get(prec, ())) + return conflicting + +### changectx method + +@eh.addattr(context.changectx, 'latecomer') +def latecomer(ctx): + """is the changeset latecomer (Try to succeed to public change)""" + if ctx.node() is None: + return False + return ctx.rev() in getobscache(ctx._repo, 'latecomer') + +@eh.addattr(context.changectx, 'conflicting') +def conflicting(ctx): + """is the changeset conflicting (Try to succeed to public change)""" + if ctx.node() is None: + return False + return ctx.rev() in getobscache(ctx._repo, 'conflicting') + +### revset symbol + +@eh.revset('latecomer') +def revsetlatecomer(repo, subset, x): + """``latecomer()`` + Changesets marked as successors of public changesets. + """ + args = revset.getargs(x, 0, 0, 'latecomer takes no arguments') + lates = getobscache(repo, 'latecomer') + return [r for r in subset if r in lates] + +@eh.revset('conflicting') +def revsetconflicting(repo, subset, x): + """``conflicting()`` + Changesets marked as successors of a same changeset. + """ + args = revset.getargs(x, 0, 0, 'conflicting takes no arguments') + conf = getobscache(repo, 'conflicting') + return [r for r in subset if r in conf] + + +### Discovery wrapping + +@eh.wrapfunction(discovery, 'checkheads') +def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs): + """wrap mercurial.discovery.checkheads + + * prevent latecomer and unstable to be pushed + """ + # do not push instability + for h in outgoing.missingheads: + # Checking heads is enough, obsolete descendants are either + # obsolete or unstable. + ctx = repo[h] + if ctx.latecomer(): + raise util.Abort(_("push includes a latecomer changeset: %s!") + % ctx) + if ctx.conflicting(): + raise util.Abort(_("push includes a conflicting changeset: %s!") + % ctx) + return orig(repo, remote, outgoing, *args, **kwargs) + +##################################################################### +### Filter extinct changeset from common operation ### +##################################################################### + +@eh.wrapfunction(merge, 'update') +def wrapmergeupdate(orig, repo, node, *args, **kwargs): + """ensure we don't automatically update on hidden changeset""" + if node is None: + # tip of current branch + branch = repo[None].branch() + node = repo.revs('last((.:: and branch(%s)) - hidden())', branch)[0] + return orig(repo, node, *args, **kwargs) + +@eh.wrapfunction(localrepo.localrepository, 'branchtip') +def obsbranchtip(orig, repo, branch): + """ensure "stable" reference does not end on a hidden changeset""" + result = () + heads = repo.branchmap().get(branch, ()) + if heads: + result = list(repo.set('last(heads(branch(%n) - hidden()))', heads[0])) + if not result: + raise error.RepoLookupError(_("unknown branch '%s'") % branch) + return result[0].node() + + +##################################################################### +### 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 throught the obsolescence graph +# - function to find useful changeset to stabilize + +### Marker Create + +def createmarkers(repo, relations, metadata=None, flag=0): + """Add obsolete markers between changeset in a repo + + must be an iterable of (, (, ...)) tuple. + `old` and `news` are changectx. + + Current user and date are used except if specified otherwise in the + metadata attribute. + + /!\ assume the repo have been locked by the user /!\ + """ + # prepare metadata + if metadata is None: + metadata = {} + if 'date' not in metadata: + metadata['date'] = '%i %i' % util.makedate() + if 'user' not in metadata: + metadata['user'] = repo.ui.username() + # check future marker + tr = repo.transaction('add-obsolescence-marker') + try: + for prec, sucs in relations: + if not prec.mutable(): + raise util.Abort("Cannot obsolete immutable changeset: %s" % prec) + nprec = prec.node() + nsucs = tuple(s.node() for s in sucs) + if nprec in nsucs: + raise util.Abort("Changeset %s cannot obsolete himself" % prec) + repo.obsstore.create(tr, nprec, nsucs, flag, metadata) + clearobscaches(repo) + tr.close() + finally: + tr.release() + + +### Useful alias + +@eh.uisetup +def _installalias(ui): + if ui.config('alias', 'pstatus', None) is None: + ui.setconfig('alias', 'pstatus', 'status --rev .^') + if ui.config('alias', 'pdiff', None) is None: + ui.setconfig('alias', 'pdiff', 'diff --rev .^') + if ui.config('alias', 'olog', None) is None: + ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden") + if ui.config('alias', 'odiff', None) is None: + ui.setconfig('alias', 'odiff', "diff --rev 'limit(precursors(.),1)' --rev .") + +# - "troubles" method on changectx + +@eh.addattr(context.changectx, 'troubles') +def troubles(ctx): + """Return a tuple listing all the troubles that affect a changeset + + Troubles may be "unstable", "latecomer" or "conflicting". + """ + troubles = [] + if ctx.unstable(): + troubles.append('unstable') + if ctx.latecomer(): + troubles.append('latecomer') + if ctx.conflicting(): + troubles.append('conflicting') + return tuple(troubles) + +### Troubled revset symbol + +@eh.revset('troubled') +def revsetlatecomer(repo, subset, x): + """``troubled()`` + Changesets with troubles. + """ + _ = revset.getargs(x, 0, 0, 'troubled takes no arguments') + return list(repo.revs('%ld and (unstable() + latecomer() + conflicting())', + subset)) + + +### 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.successors + for r in s: + for p in markerbysubj.get(repo[r].node(), ()): + pr = nm.get(p[0]) + if pr is not None: + cs.add(pr) + return cs + +def _allprecursors(repo, s): # XXX we need a better naming + """transitive precursors of a subset""" + toproceed = [repo[r].node() for r in s] + seen = set() + allsubjects = repo.obsstore.successors + 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) + return cs + +def _successors(repo, s): + """Successors of a changeset""" + cs = set() + nm = repo.changelog.nodemap + markerbyobj = repo.obsstore.precursors + for r in s: + for p in markerbyobj.get(repo[r].node(), ()): + for sub in p[1]: + sr = nm.get(sub) + if sr is not None: + cs.add(sr) + 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. """ + toproceed = [repo[r].node() for r in s] + seen = set() + allobjects = repo.obsstore.precursors + 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) + return cs + + + +def newerversion(repo, obs): + """Return the newer version of an obsolete changeset""" + toproceed = set([(obs,)]) + # XXX known optimization available + newer = set() + objectrels = repo.obsstore.precursors + while toproceed: + current = toproceed.pop() + assert len(current) <= 1, 'splitting not handled yet. %r' % current + current = [n for n in current if n != nullid] + if current: + n, = current + if n in objectrels: + markers = objectrels[n] + for mark in markers: + toproceed.add(tuple(mark[1])) + else: + newer.add(tuple(current)) + else: + newer.add(()) + return sorted(newer) + + +##################################################################### +### Extending revset and template ### +##################################################################### + +# this section add several useful revset symbol not yet in core. +# they are subject to changes + +### hidden revset is not in core yet + +@eh.revset('hidden') +def revsethidden(repo, subset, x): + """``hidden()`` + Changeset is hidden. + """ + args = revset.getargs(x, 0, 0, 'hidden takes no argument') + return [r for r in subset if r in repo.hiddenrevs] + +### XXX I'm not sure this revset is useful +@eh.revset('suspended') +def revsetsuspended(repo, subset, x): + """``suspended()`` + Obsolete changesets with non-obsolete descendants. + """ + args = revset.getargs(x, 0, 0, 'suspended takes no arguments') + suspended = getobscache(repo, 'suspended') + return [r for r in subset if r in suspended] + + +@eh.revset('precursors') +def revsetprecursors(repo, subset, x): + """``precursors(set)`` + Immediate precursors of changesets in set. + """ + s = revset.getset(repo, range(len(repo)), x) + cs = _precursors(repo, s) + return [r for r in subset if r in cs] + + +@eh.revset('allprecursors') +def revsetallprecursors(repo, subset, x): + """``allprecursors(set)`` + Transitive precursors of changesets in set. + """ + s = revset.getset(repo, range(len(repo)), x) + cs = _allprecursors(repo, s) + return [r for r in subset if r in cs] + + +@eh.revset('successors') +def revsetsuccessors(repo, subset, x): + """``successors(set)`` + Immediate successors of changesets in set. + """ + s = revset.getset(repo, range(len(repo)), x) + cs = _successors(repo, s) + return [r for r in subset if r in cs] + +@eh.revset('allsuccessors') +def revsetallsuccessors(repo, subset, x): + """``allsuccessors(set)`` + Transitive successors of changesets in set. + """ + s = revset.getset(repo, range(len(repo)), x) + cs = _allsuccessors(repo, s) + return [r for r in subset if r in cs] + +### template keywords +# XXX it does not handle troubles well :-/ + +@eh.templatekw('obsolete') +def obsoletekw(repo, ctx, templ, **args): + """:obsolete: String. The obsolescence level of the node, could be + ``stable``, ``unstable``, ``suspended`` or ``extinct``. + """ + rev = ctx.rev() + if ctx.obsolete(): + if ctx.extinct(): + return 'extinct' + else: + return 'suspended' + elif ctx.unstable(): + return 'unstable' + return 'stable' + +##################################################################### +### Various trouble warning ### +##################################################################### + +# This section take care of issue warning to the user when troubles appear + +@eh.wrapcommand("update") +@eh.wrapcommand("pull") +def wrapmayobsoletewc(origfn, ui, repo, *args, **opts): + """Warn that the working directory parent is an obsolete changeset""" + res = origfn(ui, repo, *args, **opts) + if repo['.'].obsolete(): + ui.warn(_('Working directory parent is obsolete\n')) + return res + +# XXX this could wrap transaction code +# XXX (but this is a bit a layer violation) +@eh.wrapcommand("commit") +@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""" + priorunstables = len(repo.revs('unstable()')) + priorlatecomers = len(repo.revs('latecomer()')) + priorconflictings = len(repo.revs('conflicting()')) + ret = orig(ui, repo, *args, **kwargs) + newunstables = len(repo.revs('unstable()')) - priorunstables + newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers + newconflictings = len(repo.revs('conflicting()')) - priorconflictings + if newunstables > 0: + ui.warn(_('%i new unstable changesets\n') % newunstables) + if newlatecomers > 0: + ui.warn(_('%i new latecomer changesets\n') % newlatecomers) + if newconflictings > 0: + ui.warn(_('%i new conflicting changesets\n') % newconflictings) + return ret + +@eh.reposetup +def _repostabilizesetup(ui, repo): + """Add a hint for "hg evolve" when troubles make push fails + """ + if not repo.local(): + return + + opush = repo.push + + class evolvingrepo(repo.__class__): + def push(self, remote, *args, **opts): + """wrapper around pull that pull obsolete relation""" + try: + result = opush(remote, *args, **opts) + except util.Abort, 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 + return result + repo.__class__ = evolvingrepo + +@eh.wrapcommand("summary") +def obssummary(orig, ui, repo, *args, **kwargs): + ret = orig(ui, repo, *args, **kwargs) + nbunstable = len(getobscache(repo, 'unstable')) + nblatecomer = len(getobscache(repo, 'latecomer')) + nbconflicting = len(getobscache(repo, 'unstable')) + if nbunstable: + ui.write('unstable: %i changesets\n' % nbunstable) + else: + ui.note('unstable: 0 changesets\n') + if nblatecomer: + ui.write('latecomer: %i changesets\n' % nblatecomer) + else: + ui.note('latecomer: 0 changesets\n') + if nbconflicting: + ui.write('conflicting: %i changesets\n' % nbconflicting) + else: + ui.note('conflicting: 0 changesets\n') + return ret + + +##################################################################### +### Core Other extension compat ### +##################################################################### + +# This section make official history rewritter create obsolete marker + + +### commit --amend +# make commit --amend create obsolete marker +# +# The precursor is still strip from the repository. + +@eh.wrapfunction(cmdutil, 'amend') +def wrapcmdutilamend(orig, ui, repo, commitfunc, old, *args, **kwargs): + oldnode = old.node() + new = orig(ui, repo, commitfunc, old, *args, **kwargs) + if new != oldnode: + lock = repo.lock() + try: + tr = repo.transaction('post-amend-obst') + try: + meta = { + 'date': '%i %i' % util.makedate(), + 'user': ui.username(), + } + repo.obsstore.create(tr, oldnode, [new], 0, meta) + tr.close() + clearobscaches(repo) + finally: + tr.release() + finally: + lock.release() + return new + +### rebase +# +# - ignore obsolete changeset +# - create obsolete marker *instead of* striping + +def buildstate(orig, repo, dest, rebaseset, *ags, **kws): + """wrapper for rebase 's buildstate that exclude obsolete changeset""" + + rebaseset = repo.revs('%ld - extinct()', rebaseset) + if not rebaseset: + repo.ui.warn(_('whole rebase set is extinct and ignored.\n')) + return {} + root = min(rebaseset) + if not repo._rebasekeep and not repo[root].mutable(): + raise util.Abort(_("can't rebase immutable changeset %s") % repo[root], + hint=_('see hg help phases for details')) + return orig(repo, dest, rebaseset, *ags, **kws) + +def defineparents(orig, repo, rev, target, state, *args, **kwargs): + rebasestate = getattr(repo, '_rebasestate', None) + if rebasestate is not None: + repo._rebasestate = dict(state) + repo._rebasetarget = target + return orig(repo, rev, target, state, *args, **kwargs) + +def concludenode(orig, repo, rev, p1, *args, **kwargs): + """wrapper for rebase 's concludenode that set obsolete relation""" + newrev = orig(repo, rev, p1, *args, **kwargs) + rebasestate = getattr(repo, '_rebasestate', None) + if rebasestate is not None: + if newrev is not None: + nrev = repo[newrev].rev() + else: + nrev = p1 + repo._rebasestate[rev] = nrev + return newrev + +def cmdrebase(orig, ui, repo, *args, **kwargs): + + reallykeep = kwargs.get('keep', False) + kwargs = dict(kwargs) + kwargs['keep'] = True + repo._rebasekeep = reallykeep + + # We want to mark rebased revision as obsolete and set their + # replacements if any. Doing it in concludenode() prevents + # aborting the rebase, and is not called with all relevant + # revisions in --collapse case. Instead, we try to track the + # rebase state structure by sampling/updating it in + # defineparents() and concludenode(). The obsolete markers are + # added from this state after a successful call. + repo._rebasestate = {} + repo._rebasetarget = None + try: + l = repo.lock() + try: + res = orig(ui, repo, *args, **kwargs) + if not reallykeep: + # Filter nullmerge or unrebased entries + repo._rebasestate = dict(p for p in repo._rebasestate.iteritems() + if p[1] >= 0) + if not res and not kwargs.get('abort') and repo._rebasestate: + # Rebased revisions are assumed to be descendants of + # targetrev. If a source revision is mapped to targetrev + # or to another rebased revision, it must have been + # removed. + markers = [] + if kwargs.get('collapse'): + # collapse assume revision disapear because they are all + # in the created revision + newrevs = set(repo._rebasestate.values()) + newrevs.remove(repo._rebasetarget) + if newrevs: + # we create new revision. + # A single one by --collapse design + assert len(newrevs) == 1 + new = tuple(repo[n] for n in newrevs) + else: + # every body died. no new changeset created + new = (repo[repo._rebasetarget],) + for rev, newrev in sorted(repo._rebasestate.items()): + markers.append((repo[rev], new)) + else: + # no collapse assume revision disapear because they are + # contained in parent + for rev, newrev in sorted(repo._rebasestate.items()): + markers.append((repo[rev], (repo[newrev],))) + createmarkers(repo, markers) + return res + finally: + l.release() + finally: + delattr(repo, '_rebasestate') + delattr(repo, '_rebasetarget') + +@eh.extsetup +def _rebasewrapping(ui): + # warning about more obsolete + try: + rebase = extensions.find('rebase') + if rebase: + entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) + extensions.wrapfunction(rebase, 'buildstate', buildstate) + extensions.wrapfunction(rebase, 'defineparents', defineparents) + extensions.wrapfunction(rebase, 'concludenode', concludenode) + extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase) + except KeyError: + pass # rebase not found + + +##################################################################### +### Old Evolve extension content ### +##################################################################### + +# XXX need clean up and proper sorting in other section ### util function ############################# @@ -118,8 +1193,9 @@ if created: updatebookmarks(newid) # add evolution metadata - collapsed = set([u.node() for u in updates] + [old.node()]) - repo.addcollapsedobsolete(collapsed, new.node()) + markers = [(u, (new,)) for u in updates] + markers.append((old, (new,))) + createmarkers(repo, markers) else: # newid is an existing revision. It could make sense to # replace revisions with existing ones but probably not by @@ -130,6 +1206,9 @@ return newid, created +class MergeFailure(util.Abort): + pass + def relocate(repo, orig, dest): """rewrite on dest""" try: @@ -151,19 +1230,19 @@ try: nodenew = rebase.concludenode(repo, orig.node(), dest.node(), node.nullid) - except util.Abort: - repo.ui.write_err(_('/!\\ stabilize failed /!\\\n')) - repo.ui.write_err(_('/!\\ Their is no "hg stabilize --continue" /!\\\n')) - repo.ui.write_err(_('/!\\ use "hg up -C . ; hg stabilize --dry-run" /!\\\n')) + except util.Abort, exc: + class LocalMergeFailure(MergeFailure, exc.__class__): + pass + exc.__class__ = LocalMergeFailure raise oldbookmarks = repo.nodebookmarks(nodesrc) if nodenew is not None: phases.retractboundary(repo, destphase, [nodenew]) - repo.addobsolete(nodenew, nodesrc) + createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))]) for book in oldbookmarks: repo._bookmarks[book] = nodenew else: - repo.addobsolete(node.nullid, nodesrc) + createmarkers(repo, [(repo[nodesrc], ())]) # Behave like rebase, move bookmarks to dest for book in oldbookmarks: repo._bookmarks[book] = dest.node() @@ -171,12 +1250,13 @@ repo._bookmarks[book] = dest.node() if oldbookmarks or destbookmarks: bookmarks.write(repo) + return nodenew except util.Abort: # Invalidate the previous setparents repo.dirstate.invalidate() raise -def stabilizableunstable(repo, pctx): +def _stabilizableunstable(repo, pctx): """Return a changectx for an unstable changeset which can be stabilized on top of pctx or one of its descendants. None if none can be found. @@ -189,7 +1269,7 @@ # Look for an unstable which can be stabilized as a child of # node. The unstable must be a child of one of node predecessors. for ctx in selfanddescendants(repo, pctx): - unstables = list(repo.set('unstable() and children(obsancestors(%d))', + unstables = list(repo.set('unstable() and children(allprecursors(%d))', ctx.rev())) if unstables: return unstables[0] @@ -219,51 +1299,101 @@ cmdtable = {} command = cmdutil.command(cmdtable) -@command('^stabilize|evolve|solve', +@command('^evolve|stabilize|evolve|solve', [('n', 'dry-run', False, 'do not perform actions, print what to be done'), - ('A', 'any', False, 'stabilize any unstable changeset'),], + ('A', 'any', False, 'evolve any troubled changeset'), + ('c', 'continue', False, 'continue an interrupted evolution'), ], _('[OPTIONS]...')) -def stabilize(ui, repo, **opts): - """rebase an unstable changeset to make it stable again +def evolve(ui, repo, **opts): + """Solve trouble in your repository + + - rebase unstable changeset to make it stable again, + - create proper diff from latecomer changeset, + - merge conflicting changeset. - By default, take the first unstable changeset which could be - rebased as child of the working directory parent revision or one - of its descendants and rebase it. + By default, take the first troubles changeset that looks relevant. + + (The logic is still a bit fuzzy) - With --any, stabilize any unstable changeset. + - For unstable, that mean the first which could be rebased as child of the + working directory parent revision or one of its descendants and rebase + it. - The working directory is updated to the rebased revision. + - For conflicting this mean "." if applicable. + + With --any, evolve pick any troubled changeset to solve + + The working directory is updated to the newly created revision. """ - obsolete = extensions.find('obsolete') + contopt = opts['continue'] + anyopt = opts['any'] + + if contopt: + if anyopt: + raise util.Abort('can not specify both "--any" and "--continue"') + graftcmd = commands.table['graft'][0] + return graftcmd(ui, repo, old_obsolete=True, **{'continue': True}) + + troubled = list(repo.revs('troubled()')) + tr = _picknexttroubled(ui, repo, anyopt) + if tr is None: + if troubled: + ui.write_err(_('nothing to evolve here\n')) + ui.status(_('(%i troubled changesets, do you want --any ?)\n') + % len(troubled)) + return 2 + else: + ui.write_err(_('no troubled changeset\n')) + return 1 + cmdutil.bailifchanged(repo) + troubles = tr.troubles() + if 'unstable' in troubles: + return _solveunstable(ui, repo, tr, opts['dry_run']) + elif 'latecomer' in troubles: + return _solvelatecomer(ui, repo, tr, opts['dry_run']) + elif 'conflicting' in troubles: + return _solveconflicting(ui, repo, tr, opts['dry_run']) + else: + assert False # WHAT? unknown troubles - node = None - if not opts['any']: - node = stabilizableunstable(repo, repo['.']) - if node is None: - unstables = list(repo.set('unstable()')) - if unstables and not opts['any']: - ui.write_err(_('nothing to stabilize here\n')) - ui.status(_('(%i unstable changesets, do you want --any ?)\n') - % len(unstables)) - return 2 - elif not unstables: - ui.write_err(_('no unstable changeset\n')) - return 1 - node = unstables[0] +def _picknexttroubled(ui, repo, any=False): + """Pick a the next trouble changeset to solve""" + tr = _stabilizableunstable(repo, repo['.']) + if tr is None: + wdp = repo['.'] + if 'conflicting' in wdp.troubles(): + tr = wdp + if tr is None and any: + troubled = list(repo.set('unstable()')) + if not troubled: + troubled = list(repo.set('latecomer()')) + if not troubled: + troubled = list(repo.set('conflicting()')) + if troubled: + tr = troubled[0] - obs = node.parents()[0] + return tr + + +def _solveunstable(ui, repo, orig, dryrun=False): + """Stabilize a unstable changeset""" + obs = orig.parents()[0] if not obs.obsolete(): - obs = node.parents()[1] + obs = orig.parents()[1] assert obs.obsolete() - newer = obsolete.newerversion(repo, obs.node()) + newer = newerversion(repo, obs.node()) + # search of a parent which is not killed + while newer == [()]: + ui.debug("stabilize target %s is plain dead," + " trying to stabilize on it's parent") + obs = obs.parents()[0] + newer = newerversion(repo, obs.node()) if len(newer) > 1: ui.write_err(_("conflict rewriting. can't choose destination\n")) return 2 targets = newer[0] - if not targets: - ui.write_err(_("does not handle kill parent yet\n")) - return 2 + assert targets if len(targets) > 1: ui.write_err(_("does not handle splitted parent yet\n")) return 2 @@ -272,21 +1402,221 @@ target = repo[target] repo.ui.status(_('move:')) if not ui.quiet: - displayer.show(node) + displayer.show(orig) repo.ui.status(_('atop:')) if not ui.quiet: displayer.show(target) - todo= 'hg rebase -Dr %s -d %s\n' % (node, target) - if opts['dry_run']: + todo = 'hg rebase -Dr %s -d %s\n' % (orig, target) + if dryrun: repo.ui.write(todo) else: repo.ui.note(todo) lock = repo.lock() try: - relocate(repo, node, target) + relocate(repo, orig, target) + except MergeFailure: + repo.opener.write('graftstate', orig.hex() + '\n') + repo.ui.write_err(_('evolve failed!\n')) + repo.ui.write_err(_('fix conflict and run "hg evolve --continue"\n')) + raise finally: lock.release() +def _solvelatecomer(ui, repo, latecomer, dryrun=False): + """Stabilize a latecomer changeset""" + # For now we deny latecomer merge + if len(latecomer.parents()) > 1: + raise util.Abort('late comer stabilization is confused by latecomer' + ' %s being a merge' % latecomer) + prec = repo.set('last(allprecursors(%d) and public())', latecomer).next() + # For now we deny target merge + if len(prec.parents()) > 1: + raise util.Abort('late comer evolution is confused by precursors' + ' %s being a merge' % prec) + + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + repo.ui.status(_('recreate:')) + if not ui.quiet: + displayer.show(latecomer) + repo.ui.status(_('atop:')) + if not ui.quiet: + displayer.show(prec) + if dryrun: + todo = 'hg rebase --rev %s --detach %s;\n' % (latecomer, prec.p1()) + repo.ui.write(todo) + repo.ui.write('hg update %s;\n' % prec) + repo.ui.write('hg revert --all --rev %s;\n' % latecomer) + repo.ui.write('hg commit --msg "latecomer update to %s"') + return 0 + wlock = repo.wlock() + try: + newid = tmpctx = None + tmpctx = latecomer + lock = repo.lock() + try: + bmupdate = _bookmarksupdater(repo, latecomer.node()) + # Basic check for common parent. Far too complicated and fragile + tr = repo.transaction('latecomer-stabilize') + try: + if not list(repo.set('parents(%d) and parents(%d)', latecomer, 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, latecomer, prec.p1()) + if tmpid is not None: + tmpctx = repo[tmpid] + createmarkers(repo, [(latecomer, (tmpctx,))]) + except MergeFailure: + repo.opener.write('graftstate', latecomer.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, latecomer) + precmanifest = prec.manifest() + for key, val in latecomer.manifest().iteritems(): + if precmanifest.pop(key, None) != 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 latecomer: + fctx = latecomer[path] + flags = fctx.flags() + mctx = context.memfilectx(fctx.path(), fctx.data(), + islink='l' in flags, + isexec='x' in flags, + copied=copied.get(path)) + return mctx + raise IOError() + text = 'latecomer update to %s:\n\n' % prec + text += latecomer.description() + + new = context.memctx(repo, + parents=[prec.node(), node.nullid], + text=text, + files=files, + filectxfn=filectxfn, + user=latecomer.user(), + date=latecomer.date(), + extra=latecomer.extra()) + + newid = repo.commitctx(new) + if newid is None: + createmarkers(repo, [(tmpctx, ())]) + newid = prec.node() + else: + phases.retractboundary(repo, latecomer.phase(), [newid]) + createmarkers(repo, [(tmpctx, (repo[newid],))], + flag=latediff) + bmupdate(newid) + tr.close() + repo.ui.status(_('commited as %s\n') % node.short(newid)) + finally: + tr.release() + finally: + lock.release() + # reroute the working copy parent to the new changeset + repo.dirstate.setparents(newid, node.nullid) + finally: + wlock.release() + +def _solveconflicting(ui, repo, conflicting, dryrun=False): + base, others = conflictingdata(conflicting) + if len(others) > 1: + raise util.Abort("We do not handle split yet") + other = others[0] + if conflicting.phase() <= phases.public: + raise util.Abort("We can't resolve this conflict from the public side") + if len(other.parents()) > 1: + raise util.Abort("conflicting changeset can't be a merge (yet)") + if other.p1() not in conflicting.parents(): + raise util.Abort("parent are not common (not handled yet)") + + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + ui.status(_('merge:')) + if not ui.quiet: + displayer.show(conflicting) + ui.status(_('with: ')) + if not ui.quiet: + displayer.show(other) + ui.status(_('base: ')) + if not ui.quiet: + displayer.show(base) + if dryrun: + ui.write('hg update -c %s &&\n' % conflicting) + ui.write('hg merge %s && \n' % other) + ui.write('hg commit -m "auto merge resolving conflict between %s and %s"&&\n' + % (conflicting, 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' % conflicting) + return + #oldphase = max(conflicting.phase(), other.phase()) + wlock = repo.wlock() + try: + lock = repo.lock() + try: + if conflicting not in repo[None].parents(): + repo.ui.status(_('updating to "local" conflict\n')) + hg.update(repo, conflicting.rev()) + repo.ui.note(_('merging conflicting changeset\n')) + 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 abandon\n")) + #repo.dirstate.write() + if stats[3] > 0: + raise util.Abort('GASP! Merge Conflict! You are on you own chap!', + hint='/!\\ hg evolve --continue will NOT work /!\\') + tr = repo.transaction('stabilize-conflicting') + try: + repo.dirstate.setparents(conflicting.node(), node.nullid) + oldlen = len(repo) + amend(ui, repo) + if oldlen == len(repo): + new = conflicting + # no changes + else: + new = repo['.'] + createmarkers(repo, [(other, (new,))]) + phases.retractboundary(repo, other.phase(), [new.node()]) + tr.close() + finally: + tr.release() + finally: + lock.release() + finally: + wlock.release() + + +def conflictingdata(ctx): + """return base, other part of a conflict + + This only return the first one. + + XXX this woobly function won't survive XXX + """ + for base in ctx._repo.set('reverse(precursors(%d))', ctx): + newer = newerversion(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 KeyError('Base seem unknown. This case is not handled yet.') + + + shorttemplate = '[{rev}] {desc|firstline}\n' @command('^gdown', @@ -353,25 +1683,29 @@ """ wlock = repo.wlock() try: - new = set(noderange(repo, opts['new'])) - targetnodes = set(noderange(repo, revs)) - if not new: - new = [node.nullid] - for n in targetnodes: - if not repo[n].mutable(): - ui.warn(_("cannot kill immutable changeset %s\n") % repo[n]) + lock = repo.lock() + try: + new = set(noderange(repo, opts['new'])) + targetnodes = set(noderange(repo, revs)) + if new: + sucs = tuple(repo[n] for n in new) else: - for ne in new: - repo.addobsolete(ne, n) - # update to an unkilled parent - wdp = repo['.'] - newnode = wdp - while newnode.obsolete(): - newnode = newnode.parents()[0] - if newnode.node() != wdp.node(): - commands.update(ui, repo, newnode.rev()) - ui.status(_('working directory now at %s\n') % newnode) + sucs = () + markers = [] + for n in targetnodes: + markers.append((repo[n], sucs)) + createmarkers(repo, markers) + # update to an unkilled parent + wdp = repo['.'] + newnode = wdp + while newnode.obsolete(): + newnode = newnode.parents()[0] + if newnode.node() != wdp.node(): + commands.update(ui, repo, newnode.rev()) + ui.status(_('working directory now at %s\n') % newnode) + finally: + lock.release() finally: wlock.release() @@ -465,7 +1799,7 @@ # the intermediate revision if any. No need to update # phases or parents. if tempid is not None: - repo.addobsolete(node.nullid, tempid) + createmarkers(repo, [(repo[tempid], ())]) # XXX: need another message in collapse case. tr.close() raise error.Abort(_('no updates found')) @@ -601,7 +1935,7 @@ if newid is None: raise util.Abort(_('nothing to uncommit')) # Move local changes on filtered changeset - repo.addobsolete(newid, old.node()) + createmarkers(repo, [(old, (repo[newid],))]) phases.retractboundary(repo, oldphase, [newid]) repo.dirstate.setparents(newid, node.nullid) _uncommitdirstate(repo, old, match) @@ -614,8 +1948,12 @@ finally: lock.release() +@eh.wrapcommand('commit') def commitwrapper(orig, ui, repo, *arg, **kwargs): - lock = repo.lock() + if kwargs.get('amend', False): + lock = None + else: + lock = repo.lock() try: obsoleted = kwargs.get('obsolete', []) if obsoleted: @@ -624,19 +1962,122 @@ if not result: # commit successed new = repo['-1'] oldbookmarks = [] + markers = [] for old in obsoleted: oldbookmarks.extend(repo.nodebookmarks(old.node())) - repo.addobsolete(new.node(), old.node()) + markers.append((old, (new,))) + if markers: + createmarkers(repo, markers) for book in oldbookmarks: repo._bookmarks[book] = new.node() if oldbookmarks: bookmarks.write(repo) return result finally: - lock.release() + if lock is not None: + lock.release() + +@command('^touch', + [('r', 'rev', [], 'revision to update'),], + # allow to choose the seed ? + _('[-r] revs')) +def touch(ui, repo, *revs, **opts): + """Create successors with exact same property but hash + + This is used to "resurect" changeset""" + 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 repo.revs('public() and %ld', revs): + raise util.Abort("can't touch public revision") + wlock = repo.wlock() + try: + lock = repo.lock() + try: + tr = repo.transaction('touch') + try: + for r in revs: + ctx = repo[r] + extra = ctx.extra().copy() + extra['__touch-noise__'] = random.randint(0, 0xffffffff) + new, _ = rewrite(repo, ctx, [], ctx, + [ctx.p1().node(), ctx.p2().node()], + commitopts={'extra': extra}) + createmarkers(repo, [(ctx, (repo[new],))]) + phases.retractboundary(repo, ctx.phase(), [new]) + if ctx in repo[None].parents(): + repo.dirstate.setparents(new, node.nullid) + tr.close() + finally: + tr.release() + finally: + lock.release() + finally: + wlock.release() +@command('^fold', + [('r', 'rev', [], 'revision to fold'),], + # allow to choose the seed ? + _('[-r] revs')) +def fold(ui, repo, *revs, **opts): + """Fold multiple revision into a single one""" + 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 fold\n') + return 1 + roots = repo.revs('roots(%ld)', revs) + if len(roots) > 1: + raise util.Abort("set have multiple roots") + root = repo[roots[0]] + if root.phase() <= phases.public: + raise util.Abort("can't touch public revision") + heads = repo.revs('heads(%ld)', revs) + if len(heads) > 1: + raise util.Abort("set have multiple heads") + head = repo[heads[0]] + wlock = repo.wlock() + try: + lock = repo.lock() + try: + tr = repo.transaction('touch') + try: + allctx = [repo[r] for r in revs] + targetphase = max(c.phase() for c in allctx) + msg = '\n\n***\n\n'.join(c.description() for c in allctx) + newid, _ = rewrite(repo, root, allctx, head, + [root.p1().node(), root.p2().node()], + commitopts={'message': msg}) + phases.retractboundary(repo, targetphase, [newid]) + createmarkers(repo, [(ctx, (repo[newid],)) + for ctx in allctx]) + tr.close() + finally: + tr.release() + finally: + lock.release() + ui.status('%i changesets folded\n' % len(revs)) + if repo.revs('. and %ld', revs): + repo.dirstate.setparents(newid, node.nullid) + finally: + wlock.release() + + +@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', []) + lock = repo.lock() try: if kwargs.get('old_obsolete'): @@ -655,25 +2096,21 @@ finally: lock.release() -def extsetup(ui): - try: - obsolete = extensions.find('obsolete') - except KeyError: - raise error.Abort(_('evolution extension requires obsolete extension.')) +@eh.extsetup +def oldevolveextsetup(ui): try: rebase = extensions.find('rebase') except KeyError: - rebase = None raise error.Abort(_('evolution extension requires rebase extension.')) for cmd in ['amend', 'kill', 'uncommit']: entry = extensions.wrapcommand(cmdtable, cmd, - obsolete.warnobserrors) + warnobserrors) - entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper) + entry = cmdutil.findcmd('commit', commands.table)[1] entry[1].append(('o', 'obsolete', [], _("make commit obsolete this revision"))) - entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper) + entry = cmdutil.findcmd('graft', commands.table)[1] entry[1].append(('o', 'obsolete', [], _("make graft obsoletes this revision"))) entry[1].append(('O', 'old-obsolete', False, diff -r f17a0f801e0b -r a9c27df23129 hgext/obsolete.py --- a/hgext/obsolete.py Tue Aug 21 12:43:21 2012 +0200 +++ b/hgext/obsolete.py Fri Aug 24 11:53:55 2012 +0200 @@ -5,777 +5,60 @@ # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -"""Introduce the Obsolete concept to mercurial - -General concept -=============== - -This extension introduces the *obsolete* concept. It adds a new *obsolete* -relation between two changesets. A relation `` obsolete `` is set to denote that ```` is new version of ````. - -The *obsolete* relation act as a **perpendicular history** to the standard -changeset history. Standard changeset history versions files. The *obsolete* -relation versions changesets. - -:obsolete: a changeset that has been replaced by another one. -:unstable: a changeset that is not obsolete but has an obsolete ancestor. -:suspended: an obsolete changeset with unstable descendant. -:extinct: an obsolete changeset without unstable descendant. - (subject to garbage collection) - -Another name for unstable could be out of sync. - - -Usage and Feature -================= - -Display and Exchange --------------------- - -obsolete changesets are hidden. (except if they have non obsolete changeset) +"""Deprecated extension that formely introduces "Changeset Obsolescence". -obsolete changesets are not exchanged. This will probably change later but it -was the simpler solution for now. - -New commands ------------- - -Note that rebased changesets are now marked obsolete instead of being stripped. - -Context object --------------- - -Context gains a ``obsolete`` method that will return True if a changeset is -obsolete False otherwise. - -revset ------- +This concept is now partially in Mercurial core (starting with mercurial 2.3). The remaining logic have been grouped with the evolve extension. -Add an ``obsolete()`` entry. - -repo extension --------------- - -To Do -~~~~~ - -- refuse to obsolete published changesets - -- handle split - -- handle conflict - -- handle unstable // out of sync - +Some code cemains 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. """ -import os -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -from mercurial.i18n import _ - -import base64 -import json - -import struct -from mercurial import util, base85 - -_pack = struct.pack -_unpack = struct.unpack - from mercurial import util -from mercurial import context -from mercurial import revset -from mercurial import scmutil -from mercurial import extensions -from mercurial import pushkey -from mercurial import discovery -from mercurial import error -from mercurial import commands -from mercurial import changelog -from mercurial import phases -from mercurial.node import hex, bin, short, nullid -from mercurial.lock import release -from mercurial import localrepo -from mercurial import cmdutil -from mercurial import templatekw try: - from mercurial.localrepo import storecache - storecache('babar') # to trigger import -except (TypeError, ImportError): - def storecache(*args): - return scmutil.filecache(*args, instore=True) - - -### Patch changectx -############################# - -def obsolete(ctx): - """is the changeset obsolete by other""" - if ctx.node()is None: - return False - return bool(ctx._repo.obsoletedby(ctx.node())) and ctx.phase() - -context.changectx.obsolete = obsolete - -def unstable(ctx): - """is the changeset unstable (have obsolete ancestor)""" - if ctx.node() is None: - return False - return ctx.rev() in ctx._repo._unstableset - -context.changectx.unstable = unstable - -def extinct(ctx): - """is the changeset extinct by other""" - if ctx.node() is None: - return False - return ctx.rev() in ctx._repo._extinctset - -context.changectx.extinct = extinct - -def latecomer(ctx): - """is the changeset latecomer (Try to succeed to public change)""" - if ctx.node() is None: - return False - return ctx.rev() in ctx._repo._latecomerset - -context.changectx.latecomer = latecomer - -def conflicting(ctx): - """is the changeset conflicting (Try to succeed to public change)""" - if ctx.node() is None: - return False - return ctx.rev() in ctx._repo._conflictingset - -context.changectx.conflicting = conflicting - - -### revset -############################# - -def revsethidden(repo, subset, x): - """hidden changesets""" - args = revset.getargs(x, 0, 0, 'hidden takes no argument') - return [r for r in subset if r in repo.changelog.hiddenrevs] - -def revsetobsolete(repo, subset, x): - """obsolete changesets""" - args = revset.getargs(x, 0, 0, 'obsolete takes no argument') - return [r for r in subset if r in repo._obsoleteset and repo._phasecache.phase(repo, r) > 0] - -# XXX Backward compatibility, to be removed once stabilized -if '_phasecache' not in vars(localrepo.localrepository): # new api - def revsetobsolete(repo, subset, x): - """obsolete changesets""" - args = revset.getargs(x, 0, 0, 'obsolete takes no argument') - return [r for r in subset if r in repo._obsoleteset and repo._phaserev[r] > 0] - -def revsetunstable(repo, subset, x): - """non obsolete changesets descendant of obsolete one""" - args = revset.getargs(x, 0, 0, 'unstable takes no arguments') - return [r for r in subset if r in repo._unstableset] - -def revsetsuspended(repo, subset, x): - """obsolete changesets with non obsolete descendants""" - args = revset.getargs(x, 0, 0, 'suspended takes no arguments') - return [r for r in subset if r in repo._suspendedset] - -def revsetextinct(repo, subset, x): - """obsolete changesets without obsolete descendants""" - args = revset.getargs(x, 0, 0, 'extinct takes no arguments') - return [r for r in subset if r in repo._extinctset] - -def revsetlatecomer(repo, subset, x): - """latecomer, Try to succeed to public change""" - args = revset.getargs(x, 0, 0, 'latecomer takes no arguments') - return [r for r in subset if r in repo._latecomerset] - -def revsetconflicting(repo, subset, x): - """conflicting, Try to succeed to public change""" - args = revset.getargs(x, 0, 0, 'conflicting takes no arguments') - return [r for r in subset if r in repo._conflictingset] + from mercurial import obsolete + if not obsolete._enabled: + obsolete._enabled = True +except ImportError: + raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)') -def _precursors(repo, s): - """Precursor of a changeset""" - cs = set() - nm = repo.changelog.nodemap - markerbysubj = repo.obsstore.successors - for r in s: - for p in markerbysubj.get(repo[r].node(), ()): - pr = nm.get(p[0]) - if pr is not None: - cs.add(pr) - return cs - -def revsetprecursors(repo, subset, x): - """precursors of a subset""" - s = revset.getset(repo, range(len(repo)), x) - cs = _precursors(repo, s) - return [r for r in subset if r in cs] - -def _allprecursors(repo, s): # XXX we need a better naming - """transitive precursors of a subset""" - toproceed = [repo[r].node() for r in s] - seen = set() - allsubjects = repo.obsstore.successors - 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) - return cs - -def revsetallprecursors(repo, subset, x): - """obsolete parents""" - s = revset.getset(repo, range(len(repo)), x) - cs = _allprecursors(repo, s) - return [r for r in subset if r in cs] +import sys +import json -def _successors(repo, s): - """Successors of a changeset""" - cs = set() - nm = repo.changelog.nodemap - markerbyobj = repo.obsstore.precursors - for r in s: - for p in markerbyobj.get(repo[r].node(), ()): - for sub in p[1]: - sr = nm.get(sub) - if sr is not None: - cs.add(sr) - return cs - -def revsetsuccessors(repo, subset, x): - """successors of a subset""" - s = revset.getset(repo, range(len(repo)), x) - cs = _successors(repo, s) - return [r for r in subset if r in cs] - -def _allsuccessors(repo, s): # XXX we need a better naming - """transitive successors of a subset""" - toproceed = [repo[r].node() for r in s] - seen = set() - allobjects = repo.obsstore.precursors - while toproceed: - nc = toproceed.pop() - for mark in allobjects.get(nc, ()): - 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) - return cs - -def revsetallsuccessors(repo, subset, x): - """obsolete parents""" - s = revset.getset(repo, range(len(repo)), x) - cs = _allsuccessors(repo, s) - return [r for r in subset if r in cs] - - -### template keywords -##################### - -def obsoletekw(repo, ctx, templ, **args): - """:obsolete: String. The obsolescence level of the node, could be - ``stable``, ``unstable``, ``suspended`` or ``extinct``. - """ - rev = ctx.rev() - if rev in repo._extinctset: - return 'extinct' - if rev in repo._suspendedset: - return 'suspended' - if rev in repo._unstableset: - return 'unstable' - return 'stable' - -### Other Extension compat -############################ +from mercurial import cmdutil +from mercurial import error +from mercurial.node import bin, nullid -def buildstate(orig, repo, dest, rebaseset, *ags, **kws): - """wrapper for rebase 's buildstate that exclude obsolete changeset""" - rebaseset = repo.revs('%ld - extinct()', rebaseset) - return orig(repo, dest, rebaseset, *ags, **kws) - -def defineparents(orig, repo, rev, target, state, *args, **kwargs): - rebasestate = getattr(repo, '_rebasestate', None) - if rebasestate is not None: - repo._rebasestate = dict(state) - repo._rebasetarget = target - return orig(repo, rev, target, state, *args, **kwargs) - -def concludenode(orig, repo, rev, p1, *args, **kwargs): - """wrapper for rebase 's concludenode that set obsolete relation""" - newrev = orig(repo, rev, p1, *args, **kwargs) - rebasestate = getattr(repo, '_rebasestate', None) - if rebasestate is not None: - if newrev is not None: - nrev = repo[newrev].rev() - else: - nrev = p1 - repo._rebasestate[rev] = nrev - return newrev - -def cmdrebase(orig, ui, repo, *args, **kwargs): - - reallykeep = kwargs.get('keep', False) - kwargs = dict(kwargs) - kwargs['keep'] = True +##################################################################### +### Older format management ### +##################################################################### - # We want to mark rebased revision as obsolete and set their - # replacements if any. Doing it in concludenode() prevents - # aborting the rebase, and is not called with all relevant - # revisions in --collapse case. Instead, we try to track the - # rebase state structure by sampling/updating it in - # defineparents() and concludenode(). The obsolete markers are - # added from this state after a successful call. - repo._rebasestate = {} - repo._rebasetarget = None - try: - res = orig(ui, repo, *args, **kwargs) - if not reallykeep: - # Filter nullmerge or unrebased entries - repo._rebasestate = dict(p for p in repo._rebasestate.iteritems() - if p[1] >= 0) - if not res and not kwargs.get('abort') and repo._rebasestate: - # Rebased revisions are assumed to be descendants of - # targetrev. If a source revision is mapped to targetrev - # or to another rebased revision, it must have been - # removed. - targetrev = repo[repo._rebasetarget].rev() - newrevs = set([targetrev]) - replacements = {} - for rev, newrev in sorted(repo._rebasestate.items()): - oldnode = repo[rev].node() - if newrev not in newrevs: - newnode = repo[newrev].node() - newrevs.add(newrev) - else: - newnode = nullid - replacements[oldnode] = newnode - - if kwargs.get('collapse'): - newnodes = set(n for n in replacements.values() if n != nullid) - if newnodes: - # Collapsing into more than one revision? - assert len(newnodes) == 1, newnodes - newnode = newnodes.pop() - else: - newnode = nullid - repo.addcollapsedobsolete(replacements, newnode) - else: - for oldnode, newnode in replacements.iteritems(): - repo.addobsolete(newnode, oldnode) - return res - finally: - delattr(repo, '_rebasestate') - delattr(repo, '_rebasetarget') +# Code related to detection and management of older legacy format never +# handled by core -def extsetup(ui): - - revset.symbols["hidden"] = revsethidden - revset.symbols["obsolete"] = revsetobsolete - revset.symbols["unstable"] = revsetunstable - revset.symbols["suspended"] = revsetsuspended - revset.symbols["extinct"] = revsetextinct - revset.symbols["latecomer"] = revsetlatecomer - revset.symbols["conflicting"] = revsetconflicting - revset.symbols["obsparents"] = revsetprecursors # DEPR - revset.symbols["precursors"] = revsetprecursors - revset.symbols["obsancestors"] = revsetallprecursors # DEPR - revset.symbols["allprecursors"] = revsetallprecursors # bad name - revset.symbols["successors"] = revsetsuccessors - revset.symbols["allsuccessors"] = revsetallsuccessors # bad name - - templatekw.keywords['obsolete'] = obsoletekw - - # warning about more obsolete - for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']: - entry = extensions.wrapcommand(commands.table, cmd, warnobserrors) - try: - rebase = extensions.find('rebase') - if rebase: - entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) - extensions.wrapfunction(rebase, 'buildstate', buildstate) - extensions.wrapfunction(rebase, 'defineparents', defineparents) - extensions.wrapfunction(rebase, 'concludenode', concludenode) - extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase) - except KeyError: - pass # rebase not found - -# Pushkey mechanism for mutable -######################################### - -def listmarkers(repo): - """List markers over pushkey""" - if not repo.obsstore: - return {} - data = repo.obsstore._writemarkers() - encdata = base85.b85encode(data) - return {'dump0': encdata, - 'dump': encdata} # legacy compat - -def pushmarker(repo, key, old, new): - """Push markers over pushkey""" - if not key.startswith('dump'): - repo.ui.warn(_('unknown key: %r') % key) - return 0 - if old: - repo.ui.warn(_('unexpected old value') % key) - return 0 - data = base85.b85decode(new) - lock = repo.lock() - try: - try: - repo.obsstore.mergemarkers(data) - return 1 - except util.Abort: - return 0 - finally: - lock.release() - -pushkey.register('obsolete', pushmarker, listmarkers) - -### Discovery wrapping -############################# - -class blist(list, object): - """silly class to have non False but empty list""" - - def __nonzero__(self): - return bool(len(self.orig)) - -def wrapfindcommonoutgoing(orig, repo, *args, **kwargs): - """wrap mercurial.discovery.findcommonoutgoing to remove extinct changeset - - Such excluded changeset are removed from excluded and will *not* appear - are excluded secret changeset. - """ - outgoing = orig(repo, *args, **kwargs) - orig = outgoing.excluded - outgoing.excluded = blist(n for n in orig if not repo[n].extinct()) - # when no revision is specified (push everything) a shortcut is taken when - # nothign was exclude. taking this code path when extinct changeset have - # been excluded leads to repository corruption. - outgoing.excluded.orig = orig - return outgoing - -def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs): - """wrap mercurial.discovery.checkheads - - * prevent unstability to be pushed - * patch remote to ignore obsolete heads on remote - """ - # do not push instability - for h in outgoing.missingheads: - # checking heads only is enought because any thing base on obsolete - # changeset is either obsolete or unstable. - ctx = repo[h] - if ctx.unstable(): - raise util.Abort(_("push includes an unstable changeset: %s!") - % ctx) - if ctx.obsolete(): - raise util.Abort(_("push includes an obsolete changeset: %s!") - % ctx) - if ctx.latecomer(): - raise util.Abort(_("push includes an latecomer changeset: %s!") - % ctx) - if ctx.conflicting(): - raise util.Abort(_("push includes conflicting changeset: %s!") - % ctx) - ### patch remote branch map - # do not read it this burn eyes - try: - if 'oldbranchmap' not in vars(remote): - remote.oldbranchmap = remote.branchmap - def branchmap(): - newbm = {} - oldbm = None - if (util.safehasattr(phases, 'visiblebranchmap') - and not util.safehasattr(remote, 'ignorevisiblebranchmap') - ): - remote.ignorevisiblebranchmap = False - remote.branchmap = remote.oldbranchmap - oldbm = phases.visiblebranchmap(remote) - remote.branchmap = remote.newbranchmap - remote.ignorevisiblebranchmap = True - if oldbm is None: - oldbm = remote.oldbranchmap() - for branch, nodes in oldbm.iteritems(): - nodes = list(nodes) - new = set() - while nodes: - n = nodes.pop() - if n in repo.obsstore.precursors: - markers = repo.obsstore.precursors[n] - for mark in markers: - for newernode in mark[1]: - if newernode is not None: - nodes.append(newernode) - else: - new.add(n) - if new: - newbm[branch] = list(new) - return newbm - remote.ignorevisiblebranchmap = True - remote.branchmap = branchmap - remote.newbranchmap = branchmap - return orig(repo, remote, outgoing, *args, **kwargs) - finally: - remote.__dict__.pop('branchmap', None) # restore class one - remote.__dict__.pop('oldbranchmap', None) - remote.__dict__.pop('newbranchmap', None) - remote.__dict__.pop('ignorevisiblebranchmap', None) - -# eye are still burning -def wrapvisiblebranchmap(orig, repo): - ignore = getattr(repo, 'ignorevisiblebranchmap', None) - if ignore is None: - return orig(repo) - elif ignore: - return repo.branchmap() - else: - return None # break recursion - -def wrapclearcache(orig, repo, *args, **kwargs): - try: - return orig(repo, *args, **kwargs) - finally: - repo._clearobsoletecache() - - -### New commands -############################# - -cmdtable = {} -command = cmdutil.command(cmdtable) - -@command('debugobsolete', [], _('SUBJECT OBJECT')) -def cmddebugobsolete(ui, repo, subject, object): - """add an obsolete relation between two nodes - - The subject is expected to be a newer version of the object. +def reposetup(ui, repo): + """Detect that a repo still contains some old obsolete format """ - lock = repo.lock() - try: - sub = repo[subject] - obj = repo[object] - repo.addobsolete(sub.node(), obj.node()) - finally: - lock.release() - return 0 - -@command('debugconvertobsolete', [], '') -def cmddebugconvertobsolete(ui, repo): - """import markers from an .hg/obsolete-relations file""" - cnt = 0 - err = 0 - l = repo.lock() - some = False - 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(prec, sucs, 0, meta) - cnt += 1 - except ValueError: - repo.ui.write_err("invalid old marker line: %s" - % (line)) - err += 1 - finally: - f.close() - util.unlink(repo.join('obsolete-relations')) - except IOError: - pass - ### second (json) format - data = repo.sopener.tryread('obsoletemarkers') + if not repo.local(): + return + for arg in sys.argv: + if 'debugc' in arg: + break + else: + data = repo.opener.tryread('obsolete-relations') + if not data: + data = repo.sopener.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(bin(oldobject), succs, - 0, meta) - cnt += 1 - except ValueError: - repo.ui.write_err("invalid marker %s -> %s\n" - % (oldobject, oldsubjects)) - err += 1 - util.unlink(repo.sjoin('obsoletemarkers')) - 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) - -@command('debugsuccessors', [], '') -def cmddebugsuccessors(ui, repo): - """dump obsolete changesets and their successors - - Each line matches an existing marker, the first identifier is the - obsolete changeset identifier, followed by it successors. - """ - lock = repo.lock() - try: - allsuccessors = repo.obsstore.precursors - for old in sorted(allsuccessors): - successors = [sorted(m[1]) for m in allsuccessors[old]] - for i, group in enumerate(sorted(successors)): - ui.write('%s' % short(old)) - for new in group: - ui.write(' %s' % short(new)) - ui.write('\n') - finally: - lock.release() - -### Altering existing command -############################# - -def wrapmayobsoletewc(origfn, ui, repo, *args, **opts): - res = origfn(ui, repo, *args, **opts) - if repo['.'].obsolete(): - ui.warn(_('Working directory parent is obsolete\n')) - return res - -def warnobserrors(orig, ui, repo, *args, **kwargs): - """display warning is the command resulted in more instable changeset""" - priorunstables = len(repo.revs('unstable()')) - priorlatecomers = len(repo.revs('latecomer()')) - priorconflictings = len(repo.revs('conflicting()')) - #print orig, priorunstables - #print len(repo.revs('secret() - obsolete()')) - try: - return orig(ui, repo, *args, **kwargs) - finally: - newunstables = len(repo.revs('unstable()')) - priorunstables - newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers - newconflictings = len(repo.revs('conflicting()')) - priorconflictings - #print orig, newunstables - #print len(repo.revs('secret() - obsolete()')) - if newunstables > 0: - ui.warn(_('%i new unstables changesets\n') % newunstables) - if newlatecomers > 0: - ui.warn(_('%i new latecomers changesets\n') % newlatecomers) - if newconflictings > 0: - ui.warn(_('%i new conflictings changesets\n') % newconflictings) - -def noextinctsvisibleheads(orig, repo): - repo._turn_extinct_secret() - return orig(repo) - -def wrapcmdutilamend(orig, ui, repo, commitfunc, old, *args, **kwargs): - oldnode = old.node() - new = orig(ui, repo, commitfunc, old, *args, **kwargs) - if new != oldnode: - lock = repo.lock() - try: - meta = { - 'subjects': [new], - 'object': oldnode, - 'date': util.makedate(), - 'user': ui.username(), - 'reason': 'commit --amend', - } - repo.obsstore.create(oldnode, [new], 0, meta) - repo._clearobsoletecache() - repo._turn_extinct_secret() - finally: - lock.release() - return new - -def uisetup(ui): - extensions.wrapcommand(commands.table, "update", wrapmayobsoletewc) - extensions.wrapcommand(commands.table, "pull", wrapmayobsoletewc) - if util.safehasattr(cmdutil, 'amend'): - extensions.wrapfunction(cmdutil, 'amend', wrapcmdutilamend) - extensions.wrapfunction(discovery, 'findcommonoutgoing', wrapfindcommonoutgoing) - extensions.wrapfunction(discovery, 'checkheads', wrapcheckheads) - extensions.wrapfunction(phases, 'visibleheads', noextinctsvisibleheads) - extensions.wrapfunction(phases, 'advanceboundary', wrapclearcache) - if util.safehasattr(phases, 'visiblebranchmap'): - extensions.wrapfunction(phases, 'visiblebranchmap', wrapvisiblebranchmap) - -### serialisation -############################# - -def _obsserialise(obssubrels, flike): - """serialise an obsolete relation mapping in a plain text one - - this is for subject -> [objects] mapping - - format is:: - - \n""" - for sub, objs in obssubrels.iteritems(): - for obj in objs: - if sub is None: - sub = nullid - flike.write('%s %s\n' % (hex(sub), hex(obj))) + raise util.Abort('old format of obsolete marker detected!\n' + 'run `hg debugconvertobsolete` once.') def _obsdeserialise(flike): """read a file like object serialised with _obsserialise - this desierialize into a {subject -> objects} mapping""" + this desierialize into a {subject -> objects} mapping + + this was the very first format ever.""" rels = {} for line in flike: subhex, objhex = line.split() @@ -785,527 +68,92 @@ rels.setdefault( subnode, set()).add(bin(objhex)) return rels -### diagnostique tools -############################# - -def unstables(repo): - """Return all unstable changeset""" - return scmutil.revrange(repo, ['obsolete():: and (not obsolete())']) - -def newerversion(repo, obs): - """Return the newer version of an obsolete changeset""" - toproceed = set([(obs,)]) - # XXX known optimization available - newer = set() - objectrels = repo.obsstore.precursors - while toproceed: - current = toproceed.pop() - assert len(current) <= 1, 'splitting not handled yet. %r' % current - current = [n for n in current if n != nullid] - if current: - n, = current - if n in objectrels: - markers = objectrels[n] - for mark in markers: - toproceed.add(tuple(mark[1])) - else: - newer.add(tuple(current)) - else: - newer.add(()) - return sorted(newer) - -### obsolete relation storage -############################# -def add2set(d, key, mark): - """add to a `set` in []""" - d.setdefault(key, []).append(mark) - -def markerid(marker): - KEYS = ['subjects', "object", "date", "user", "reason"] - for key in KEYS: - assert key in marker - keys = sorted(marker.keys()) - a = util.sha1() - for key in keys: - if key == 'subjects': - for sub in sorted(marker[key]): - a.update(sub) - elif key == 'id': - pass - else: - a.update(str(marker[key])) - a.update('\0') - return a.digest() - -# mercurial backport - -def encodemeta(meta): - """Return encoded metadata string to string mapping. - - Assume no ':' in key and no '\0' in both key and value.""" - for key, value in meta.iteritems(): - if ':' in key or '\0' in key: - raise ValueError("':' and '\0' are forbidden in metadata key'") - if '\0' in value: - raise ValueError("':' are forbidden in metadata value'") - return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)]) - -def decodemeta(data): - """Return string to string dictionary from encoded version.""" - d = {} - for l in data.split('\0'): - if l: - key, value = l.split(':') - d[key] = value - return d - -# data used for parsing and writing -_fmversion = 0 -_fmfixed = '>BIB20s' -_fmnode = '20s' -_fmfsize = struct.calcsize(_fmfixed) -_fnodesize = struct.calcsize(_fmnode) - -def _readmarkers(data): - """Read and enumerate markers from raw data""" - off = 0 - diskversion = _unpack('>B', data[off:off + 1])[0] - off += 1 - if diskversion != _fmversion: - raise util.Abort(_('parsing obsolete marker: unknown version %r') - % diskversion) - - # Loop on markers - l = len(data) - while off + _fmfsize <= l: - # read fixed part - cur = data[off:off + _fmfsize] - off += _fmfsize - nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur) - # read replacement - sucs = () - if nbsuc: - s = (_fnodesize * nbsuc) - cur = data[off:off + s] - sucs = _unpack(_fmnode * nbsuc, cur) - off += s - # read metadata - # (metadata will be decoded on demand) - metadata = data[off:off + mdsize] - if len(metadata) != mdsize: - raise util.Abort(_('parsing obsolete marker: metadata is too ' - 'short, %d bytes expected, got %d') - % (len(metadata), mdsize)) - off += mdsize - yield (pre, sucs, flags, metadata) - -class obsstore(object): - """Store obsolete markers - - Markers can be accessed with two mappings: - - precursors: old -> set(new) - - successors: new -> set(old) - """ - - def __init__(self): - self._all = [] - # new markers to serialize - self._new = [] - self.precursors = {} - self.successors = {} - - def __iter__(self): - return iter(self._all) - - def __nonzero__(self): - return bool(self._all) - - def create(self, prec, succs=(), flag=0, metadata=None): - """obsolete: add a new obsolete marker - - * ensuring it is hashable - * check mandatory metadata - * encode metadata - """ - if metadata is None: - metadata = {} - if len(prec) != 20: - raise ValueError(repr(prec)) - for succ in succs: - if len(succ) != 20: - raise ValueError((succs)) - marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata)) - self.add(marker) - - def add(self, marker): - """Add a new marker to the store - - This marker still needs to be written to disk""" - self._new.append(marker) - self._load(marker) - - def loadmarkers(self, data): - """Load all markers in data, mark them as known.""" - for marker in _readmarkers(data): - self._load(marker) - - def mergemarkers(self, data): - other = set(_readmarkers(data)) - local = set(self._all) - new = other - local - for marker in new: - self.add(marker) - - def flushmarkers(self, stream): - """Write all markers to a stream - - After this operation, "new" markers are considered "known".""" - self._writemarkers(stream) - self._new[:] = [] - - def _load(self, marker): - self._all.append(marker) - pre, sucs = marker[:2] - self.precursors.setdefault(pre, set()).add(marker) - for suc in sucs: - self.successors.setdefault(suc, set()).add(marker) - - def _writemarkers(self, stream=None): - # Kept separate from flushmarkers(), it will be reused for - # markers exchange. - if stream is None: - final = [] - w = final.append - else: - w = stream.write - w(_pack('>B', _fmversion)) - for marker in self._all: - pre, sucs, flags, metadata = marker - nbsuc = len(sucs) - format = _fmfixed + (_fmnode * nbsuc) - data = [nbsuc, len(metadata), flags, pre] - data.extend(sucs) - w(_pack(format, *data)) - w(metadata) - if stream is None: - return ''.join(final) - - -### repo subclassing -############################# - -def reposetup(ui, repo): - if not repo.local(): - return - - if not util.safehasattr(repo.opener, 'tryread'): - raise util.Abort('Obsolete extension requires Mercurial 2.2 (or later)') - opull = repo.pull - opush = repo.push - olock = repo.lock - o_rollback = repo._rollback - o_updatebranchcache = repo.updatebranchcache - - # /!\ api change in Hg 2.2 (97efd26eb9576f39590812ea9) /!\ - if util.safehasattr(repo, '_journalfiles'): # Hg 2.2 - o_journalfiles = repo._journalfiles - o_writejournal = repo._writejournal - o_hook = repo.hook - - - class obsoletingrepo(repo.__class__): - - # workaround - def hook(self, name, throw=False, **args): - if 'pushkey' in name: - args.pop('new') - args.pop('old') - return o_hook(name, throw=False, **args) - - ### Public method - def obsoletedby(self, node): - """return the set of node that make obsolete (obj)""" - others = set() - for marker in self.obsstore.precursors.get(node, []): - others.update(marker[1]) - return others - - def obsolete(self, node): - """return the set of node that make obsolete (sub)""" - return set(marker[0] for marker in self.obsstore.successors.get(node, [])) - - @storecache('obsstore') - def obsstore(self): - if not getattr(self, '_importoldobsolete', False): - data = repo.opener.tryread('obsolete-relations') - if not data: - data = repo.sopener.tryread('obsoletemarkers') - if data: - raise util.Abort('old format of obsolete marker detected!\n' - 'run `hg debugconvertobsolete` once.') - store = obsstore() - data = self.sopener.tryread('obsstore') +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, 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.sopener.tryread('obsoletemarkers') if data: - store.loadmarkers(data) - return store - - @util.propertycache - def _obsoleteset(self): - """the set of obsolete revision""" - obs = set() - nm = self.changelog.nodemap - for obj in self.obsstore.precursors: - try: # /!\api change in Hg 2.2 (e8d37b78acfb22ae2c1fb126c2)/!\ - rev = nm.get(obj) - except TypeError: #XXX to remove while breaking Hg 2.1 support - rev = nm.get(obj, None) - if rev is not None: - obs.add(rev) - return obs - - @util.propertycache - def _unstableset(self): - """the set of non obsolete revision with obsolete parent""" - return set(self.revs('(obsolete()::) - obsolete()')) - - @util.propertycache - def _suspendedset(self): - """the set of obsolete parent with non obsolete descendant""" - return set(self.revs('obsolete() and obsolete()::unstable()')) - - @util.propertycache - def _extinctset(self): - """the set of obsolete parent without non obsolete descendant""" - return set(self.revs('obsolete() - obsolete()::unstable()')) - - @util.propertycache - def _latecomerset(self): - """the set of rev trying to obsolete public revision""" - query = 'allsuccessors(public()) - obsolete() - public()' - return set(self.revs(query)) - - @util.propertycache - def _conflictingset(self): - """the set of rev trying to obsolete public revision""" - conflicting = set() - obsstore = self.obsstore - newermap = {} - for ctx in self.set('(not public()) - obsolete()'): - prec = obsstore.successors.get(ctx.node(), ()) - toprocess = set(prec) - while toprocess: - prec = toprocess.pop()[0] - if prec not in newermap: - newermap[prec] = newerversion(self, prec) - newer = [n for n in newermap[prec] if n] # filter kill - if len(newer) > 1: - conflicting.add(ctx.rev()) - break - toprocess.update(obsstore.successors.get(prec, ())) - return conflicting - - def _clearobsoletecache(self): - if '_obsoleteset' in vars(self): - del self._obsoleteset - self._clearunstablecache() - - def updatebranchcache(self): - o_updatebranchcache() - self._clearunstablecache() - - def _clearunstablecache(self): - if '_unstableset' in vars(self): - del self._unstableset - if '_suspendedset' in vars(self): - del self._suspendedset - if '_extinctset' in vars(self): - del self._extinctset - if '_latecomerset' in vars(self): - del self._latecomerset - if '_conflictingset' in vars(self): - del self._conflictingset - - def addobsolete(self, sub, obj): - """Add a relation marking that node is a new version of """ - assert sub != obj - if not repo[obj].phase(): - if sub is None: - self.ui.warn( - _("trying to kill immutable changeset %(obj)s\n") - % {'obj': short(obj)}) - if sub is not None: - self.ui.warn( - _("%(sub)s try to obsolete immutable changeset %(obj)s\n") - % {'sub': short(sub), 'obj': short(obj)}) - lock = self.lock() - try: - meta = { - 'date': util.makedate(), - 'user': ui.username(), - 'reason': 'unknown', - } - subs = (sub == nullid) and [] or [sub] - mid = self.obsstore.create(obj, subs, 0, meta) - self._clearobsoletecache() - self._turn_extinct_secret() - return mid - finally: - lock.release() - - def addcollapsedobsolete(self, oldnodes, newnode): - """Mark oldnodes as collapsed into newnode.""" - # Assume oldnodes are all descendants of a single rev - rootrevs = self.revs('roots(%ln)', oldnodes) - assert len(rootrevs) == 1, rootrevs - rootnode = self[rootrevs[0]].node() - for n in oldnodes: - self.addobsolete(newnode, n) - - def _turn_extinct_secret(self): - """ensure all extinct changeset are secret""" - self._clearobsoletecache() - # this is mainly for safety purpose - # both pull and push - query = '(obsolete() - obsolete()::(unstable() - secret())) - secret()' - expobs = [c.node() for c in repo.set(query)] - phases.retractboundary(repo, 2, expobs) - - ### Disk IO + 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 - def lock(self, *args, **kwargs): - l = olock(*args, **kwargs) - if not getattr(l.releasefn, 'obspatched', False): - oreleasefn = l.releasefn - def releasefn(*args, **kwargs): - if 'obsstore' in vars(self) and self.obsstore._new: - f = self.sopener('obsstore', 'wb', atomictemp=True) - try: - self.obsstore.flushmarkers(f) - f.close() - except: # re-raises - f.discard() - raise - oreleasefn(*args, **kwargs) - releasefn.obspatched = True - l.releasefn = releasefn - return l - - - ### pull // push support - - def pull(self, remote, *args, **kwargs): - """wrapper around push that push obsolete relation""" - l = repo.lock() - try: - result = opull(remote, *args, **kwargs) - remoteobs = remote.listkeys('obsolete') - if 'dump' in remoteobs: - remoteobs['dump0'] = remoteobs.pop('dump') - if 'dump0' in remoteobs: - for key, values in remoteobs.iteritems(): - if key.startswith('dump'): - data = base85.b85decode(remoteobs['dump0']) - self.obsstore.mergemarkers(data) - self._clearobsoletecache() - self._turn_extinct_secret() - return result - finally: - l.release() - - def push(self, remote, *args, **opts): - """wrapper around pull that pull obsolete relation""" - self._turn_extinct_secret() - try: - result = opush(remote, *args, **opts) - except util.Abort, ex: - hint = _("use 'hg stabilize' to get a stable history (or --force to proceed)") - if (len(ex.args) >= 1 - and ex.args[0].startswith('push includes ') - and ex.hint is None): - ex.hint = hint - raise - if 'obsolete' in remote.listkeys('namespaces') and self.obsstore: - data = self.obsstore._writemarkers() - r = remote.pushkey('obsolete', 'dump0', '', - base85.b85encode(data)) - if not r: - self.ui.warn(_('failed to push obsolete markers!\n')) - self._turn_extinct_secret() - - return result - - - ### rollback support - - # /!\ api change in Hg 2.2 (97efd26eb9576f39590812ea9) /!\ - if util.safehasattr(repo, '_journalfiles'): # Hg 2.2 - def _journalfiles(self): - return o_journalfiles() + (self.sjoin('journal.obsstore'),) - - def _writejournal(self, desc): - """wrapped version of _writejournal that save obsolete data""" - o_writejournal(desc) - filename = 'obsstore' - filepath = self.sjoin(filename) - if os.path.exists(filepath): - journalname = 'journal.' + filename - journalpath = self.sjoin(journalname) - util.copyfile(filepath, journalpath) - - else: # XXX removing this bloc will break Hg 2.1 support - def _writejournal(self, desc): - """wrapped version of _writejournal that save obsolete data""" - entries = list(o_writejournal(desc)) - filename = 'obsstore' - filepath = self.sjoin(filename) - if os.path.exists(filepath): - journalname = 'journal.' + filename - journalpath = self.sjoin(journalname) - util.copyfile(filepath, journalpath) - entries.append(journalpath) - return tuple(entries) - - def _rollback(self, dryrun, force): - """wrapped version of _rollback that restore obsolete data""" - ret = o_rollback(dryrun, force) - if not (ret or dryrun): #rollback did not failed - src = self.sjoin('undo.obsstore') - dst = self.sjoin('obsstore') - if os.path.exists(src): - util.rename(src, dst) - elif os.path.exists(dst): - # If no state was saved because the file did not existed before. - os.unlink(dst) - # invalidate cache - self.__dict__.pop('obsstore', None) - return ret - - @storecache('00changelog.i') - def changelog(self): - # << copy pasted from mercurial source - c = changelog.changelog(self.sopener) - if 'HG_PENDING' in os.environ: - p = os.environ['HG_PENDING'] - if p.startswith(self.root): - c.readpending('00changelog.i.a') - # >> end of the copy paste - old = c.__dict__.pop('hiddenrevs', ()) - if old: - ui.warn("old wasn't empty ? %r" % old) - def _sethidden(c, value): - assert not value - - - class hchangelog(c.__class__): - @util.propertycache - def hiddenrevs(c): - shown = ['not obsolete()', '.', 'bookmark()', 'tagged()', - 'public()'] - basicquery = 'obsolete() - (::(%s))' % (' or '.join(shown)) - # !!! self is repo not changelog - result = set(scmutil.revrange(self, [basicquery])) - return result - c.__class__ = hchangelog - return c - - repo.__class__ = obsoletingrepo + 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, 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) diff -r f17a0f801e0b -r a9c27df23129 hgext/qsync.py --- a/hgext/qsync.py Tue Aug 21 12:43:21 2012 +0200 +++ b/hgext/qsync.py Fri Aug 24 11:53:55 2012 +0200 @@ -83,8 +83,8 @@ review_list.append(patch_name) except IOError: oldnode = oldfiles[patch_name] - obsolete = extensions.find('obsolete') - newnodes = obsolete.newerversion(repo, oldnode) + evolve = extensions.find('evolve') + newnodes = evolve.newerversion(repo, oldnode) if newnodes: newnodes = [n for n in newnodes if n and n[0] in repo] # remove killing if not newnodes: @@ -166,14 +166,14 @@ currentdrafts = set(d[0] for d in newdata) usednew = set() usedold = set() - obsolete = extensions.find('obsolete') + evolve = extensions.find('evolve') for oldhex, oldname in olddata: if oldhex in usedold: continue # no duplicate usedold.add(oldhex) oldname = str(oldname) oldnode = bin(oldhex) - newnodes = obsolete.newerversion(repo, oldnode) + newnodes = evolve.newerversion(repo, oldnode) if newnodes: newnodes = [n for n in newnodes if n and n[0] in repo] # remove killing if len(newnodes) > 1: diff -r f17a0f801e0b -r a9c27df23129 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Fri Aug 24 11:53:55 2012 +0200 @@ -0,0 +1,25 @@ +# Copied from histedit setup.py +# Credit to Augie Fackler + +from distutils.core import setup + +requires = [] +try: + import mercurial +except ImportError: + requires.append('mercurial') + +setup( + name='hg-evolve', + version='0.1.0', + author='Pierre-Yves David', + maintainer='Pierre-Yves David', + maintainer_email='pierre-yves.david@logilab.fr', + url='https://bitbucket.org/marmoute/mutable-history', + description='Flexible evolution of Mercurial history.', + long_description=open('README').read(), + keywords='hg mercurial', + license='GPLv2+', + py_modules=['hgext.evolve'], + install_requires=requires, +) diff -r f17a0f801e0b -r a9c27df23129 tests/test-amend.t --- a/tests/test-amend.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-amend.t Fri Aug 24 11:53:55 2012 +0200 @@ -5,7 +5,6 @@ > hgext.rebase= > hgext.graphlog= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ glog() { @@ -24,9 +23,9 @@ marked working directory as branch foo (branches are permanent and global, did you want a bookmark?) $ hg amend - $ hg debugsuccessors - 07f494440405 a34b93d251e4 - bd19cbe78fbf a34b93d251e4 + $ hg debugobsolete + bd19cbe78fbfbd87eb33420c63986fe5f3154f2c a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob) + 07f4944404050f47db2e5c5071e0e84e7a27bba9 a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob) $ hg branch foo $ hg branches @@ -58,13 +57,24 @@ $ echo a >> a $ hg ci -m changea $ echo a > a + $ hg status + M a + $ hg pstatus + $ hg diff + diff -r 2f97fe38810f a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a * +0000 (glob) + @@ -1,2 +1,1 @@ + a + -a + $ hg pdiff $ hg ci -m reseta $ hg amend --change 2 abort: no updates found [255] - $ hg debugsuccessors - 07f494440405 a34b93d251e4 - bd19cbe78fbf a34b93d251e4 + $ hg debugobsolete + bd19cbe78fbfbd87eb33420c63986fe5f3154f2c a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob) + 07f4944404050f47db2e5c5071e0e84e7a27bba9 a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob) $ hg phase 2 2: draft $ glog @@ -88,12 +98,12 @@ $ hg amend --change 2 abort: no updates found [255] - $ hg debugsuccessors - 07f494440405 a34b93d251e4 - 7384bbcba36f 000000000000 - bd19cbe78fbf a34b93d251e4 + $ hg debugobsolete + bd19cbe78fbfbd87eb33420c63986fe5f3154f2c a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob) + 07f4944404050f47db2e5c5071e0e84e7a27bba9 a34b93d251e49c93d5685ebacad785c73a7e8605 0 {'date': '* *', 'user': 'test'} (glob) + 7384bbcba36fde1a789cd00f9cd6f9b919ab5910 0 {'date': '* *', 'user': 'test'} (glob) $ glog - @ 6@foo(secret) amends a34b93d251e49c93d5685ebacad785c73a7e8605 + @ 6@foo(draft) amends a34b93d251e49c93d5685ebacad785c73a7e8605 | o 5@default(draft) resetbranch | diff -r f17a0f801e0b -r a9c27df23129 tests/test-corrupt.t --- a/tests/test-corrupt.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-corrupt.t Fri Aug 24 11:53:55 2012 +0200 @@ -16,7 +16,6 @@ > hgext.rebase= > hgext.graphlog= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ mkcommit() { > echo "$1" >> "$1" diff -r f17a0f801e0b -r a9c27df23129 tests/test-evolve.t --- a/tests/test-evolve.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-evolve.t Fri Aug 24 11:53:55 2012 +0200 @@ -15,7 +15,6 @@ > hgext.rebase= > hgext.graphlog= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" @@ -59,7 +58,8 @@ $ hg log -r 1 --template '{rev} {phase} {obsolete}\n' 1 public stable $ hg kill 1 - cannot kill immutable changeset 7c3bad9141dc + abort: Cannot obsolete immutable changeset: 7c3bad9141dc + [255] $ hg log -r 1 --template '{rev} {phase} {obsolete}\n' 1 public stable @@ -245,7 +245,7 @@ [255] $ hg amend --note 'french looks better' - 1 new unstables changesets + 1 new unstable changesets $ hg log 6 feature-A: a nifty feature - test 4 feature-B: another feature - test @@ -255,24 +255,24 @@ $ glog --hidden o 6:23409eba69a0@default(draft) a nifty feature | - | o 5:e416e48b2742@default(secret) french looks better + | x 5:e416e48b2742@default(draft) french looks better | | | | o 4:f8111a076f09@default(draft) another feature | |/ - | | o 3:524e478d4811@default(secret) fix spelling of Zwei + | | x 3:524e478d4811@default(draft) fix spelling of Zwei | | | - | | o 2:7b36850622b2@default(secret) another feature + | | x 2:7b36850622b2@default(draft) another feature | |/ - | o 1:568a468b60fc@default(draft) a nifty feature + | x 1:568a468b60fc@default(draft) a nifty feature |/ @ 0:e55e0562ee93@default(public) base - $ hg debugsuccessors - 524e478d4811 f8111a076f09 - 568a468b60fc 23409eba69a0 - 7b36850622b2 f8111a076f09 - e416e48b2742 23409eba69a0 - $ hg stabilize + $ hg debugobsolete + 524e478d4811d405c8771e4c441de4483bdf8b33 f8111a076f0975cbecb336e2bd3411be22b673fb 0 {'date': '* *', 'user': 'test'} (glob) + 7b36850622b2fd159fa30a4fb2a1edd2043b4a14 f8111a076f0975cbecb336e2bd3411be22b673fb 0 {'date': '* *', 'user': 'test'} (glob) + e416e48b27428695d00c2a2cc4a0b9619482e63f 23409eba69a0986e90cd42252852c1e6da97af5b 0 {'date': '* *', 'user': 'test'} (glob) + 568a468b60fc99a42d5d4ddbe181caff1eef308d 23409eba69a0986e90cd42252852c1e6da97af5b 0 {'date': '* *', 'user': 'test'} (glob) + $ hg evolve move:[4] another feature atop:[6] a nifty feature merging main-file-1 @@ -299,7 +299,42 @@ phase change turning obsolete changeset public issue a latecomer warning $ hg phase --public 7 - 1 new latecomers changesets + 1 new latecomer changesets + +all solving latecomer troubled + + $ hg glog + @ 8 feature-B: another feature that rox - test + | + | o 7 : another feature - test + |/ + o 6 feature-A: a nifty feature - test + | + o 0 : base - test + + $ hg evolve --any --traceback + recreate:[8] another feature that rox + atop:[7] another feature + computing new diff + commited as 8d77fa12ab0c + $ hg glog + @ 9 feature-B: latecomer update to 5f4744038ed5: - test + | + o 7 : another feature - test + | + o 6 feature-A: a nifty feature - test + | + o 0 : base - test + + $ hg diff -r 9 -r 8 + $ hg diff -r 9^ -r 9 + diff --git a/main-file-1 b/main-file-1 + --- a/main-file-1 + +++ b/main-file-1 + @@ -3,1 +3,1 @@ + -Zwei + +deux + $ hg log -r 'latecomer()' # no more latecomer $ cd .. @@ -405,9 +440,9 @@ |/ o 0:8685c6d34325@default(draft) add 0 - $ hg graft 3 -O + $ hg graft -r3 -O grafting revision 3 - $ hg graft 1 -o 2 + $ hg graft -r1 -o 2 grafting revision 1 $ glog --hidden @ 6:acb28cd497b7@default(draft) add 1 @@ -416,17 +451,17 @@ | o 4:ce341209337f@default(draft) add 4 | - | o 3:0e84df4912da@default(secret) add 3 + | x 3:0e84df4912da@default(draft) add 3 | | - | o 2:db038628b9e5@default(secret) add 2 + | x 2:db038628b9e5@default(draft) add 2 | | | o 1:73d38bb17fd7@default(draft) add 1 |/ o 0:8685c6d34325@default(draft) add 0 - $ hg debugsuccessors - 0e84df4912da 0b9e50c35132 - db038628b9e5 acb28cd497b7 + $ hg debugobsolete + 0e84df4912da4c7cad22a3b4fcfd58ddfb7c8ae9 0b9e50c35132ff548ec0065caea6a87e1ebcef32 0 {'date': '* *', 'user': 'test'} (glob) + db038628b9e56f51a454c0da0c508df247b41748 acb28cd497b7f8767e01ef70f68697a959573c2d 0 {'date': '* *', 'user': 'test'} (glob) Test graft --continue @@ -452,7 +487,7 @@ $ glog --hidden @ 8:920e58bb443b@default(draft) conflict | - | o 7:a5bfd90a2f29@default(secret) conflict + | x 7:a5bfd90a2f29@default(draft) conflict | | o | 6:acb28cd497b7@default(draft) add 1 | | @@ -460,18 +495,87 @@ | | o | 4:ce341209337f@default(draft) add 4 |/ - | o 3:0e84df4912da@default(secret) add 3 + | x 3:0e84df4912da@default(draft) add 3 | | - | o 2:db038628b9e5@default(secret) add 2 + | x 2:db038628b9e5@default(draft) add 2 | | | o 1:73d38bb17fd7@default(draft) add 1 |/ o 0:8685c6d34325@default(draft) add 0 - $ hg debugsuccessors - 0e84df4912da 0b9e50c35132 - a5bfd90a2f29 920e58bb443b - db038628b9e5 acb28cd497b7 + $ hg debugobsolete + 0e84df4912da4c7cad22a3b4fcfd58ddfb7c8ae9 0b9e50c35132ff548ec0065caea6a87e1ebcef32 0 {'date': '* *', 'user': 'test'} (glob) + db038628b9e56f51a454c0da0c508df247b41748 acb28cd497b7f8767e01ef70f68697a959573c2d 0 {'date': '* *', 'user': 'test'} (glob) + a5bfd90a2f29c7ccb8f917ff4e5013a9053d0a04 920e58bb443b73eea9d6d65570b4241051ea3229 0 {'date': '* *', 'user': 'test'} (glob) + +Test touch - $ cd .. + $ glog + @ 8:920e58bb443b@default(draft) conflict + | + o 6:acb28cd497b7@default(draft) add 1 + | + o 5:0b9e50c35132@default(draft) add 3 + | + o 4:ce341209337f@default(draft) add 4 + | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg touch + $ glog + @ 9:*@default(draft) conflict (glob) + | + o 6:acb28cd497b7@default(draft) add 1 + | + o 5:0b9e50c35132@default(draft) add 3 + | + o 4:ce341209337f@default(draft) add 4 + | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg touch . + $ glog + @ 10:*@default(draft) conflict (glob) + | + o 6:acb28cd497b7@default(draft) add 1 + | + o 5:0b9e50c35132@default(draft) add 3 + | + o 4:ce341209337f@default(draft) add 4 + | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + +Test fold + + $ hg fold 6::10 + 2 changesets folded + $ glog + @ 11:*@default(draft) add 1 (glob) + | + o 5:0b9e50c35132@default(draft) add 3 + | + o 4:ce341209337f@default(draft) add 4 + | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg log -r 11 --template '{desc}\n' + add 1 + + *** + + conflict + +Test olog + + $ hg olog + 6 : add 1 - test + 10 : conflict - test diff -r f17a0f801e0b -r a9c27df23129 tests/test-obsolete-push.t --- a/tests/test-obsolete-push.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-obsolete-push.t Fri Aug 24 11:53:55 2012 +0200 @@ -5,7 +5,6 @@ > hgext.rebase= > hgext.graphlog= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ template='{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' @@ -28,13 +27,13 @@ $ hg ci -qAm C c $ hg phase --secret --force . $ hg kill 0 1 - 1 new unstables changesets + 1 new unstable changesets $ glog --hidden @ 2:244232c2222a@default(unstable/secret) C | - | o 1:6c81ed0049f8@default(extinct/secret) B + | x 1:6c81ed0049f8@default(extinct/draft) B |/ - o 0:1994f17a630e@default(suspended/secret) A + x 0:1994f17a630e@default(suspended/draft) A $ hg init ../clone $ cat > ../clone/.hg/hgrc < hgext.rebase= > hgext.graphlog= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ glog() { > hg glog --template '{rev}:{node|short}@{branch}({phase}) {desc|firstline}\n'\ @@ -30,6 +30,15 @@ created new head $ echo e > e $ hg ci -Am adde e + +(phase compliance) + + $ hg phase --public 3 + $ hg rebase -d 1 -r 3 + abort: can't rebase immutable changeset 98e4a024635e + (see hg help phases for details) + [255] + $ hg phase --draft --force 0 $ hg rebase -d 1 -r 3 --detach --keep $ glog @ 4:9c5494949763@default(draft) adde @@ -53,7 +62,7 @@ |/ o 0:07f494440405@default(draft) adda - $ hg debugsuccessors + $ hg debugobsolete $ hg --config extensions.hgext.mq= strip tip 0 files updated, 0 files merged, 1 files removed, 0 files unresolved saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9c5494949763-backup.hg @@ -70,7 +79,7 @@ $ glog --hidden @ 4:9c5494949763@default(draft) adde | - | o 3:98e4a024635e@default(secret) adde + | x 3:98e4a024635e@default(draft) adde | | | o 2:102a90ea7b4a@default(draft) addb | | @@ -78,8 +87,8 @@ |/ o 0:07f494440405@default(draft) adda - $ hg debugsuccessors - 98e4a024635e 9c5494949763 + $ hg debugobsolete + 98e4a024635e8c50928144c9277a4388d26bd786 9c54949497631abfb5a255d96746bbd3a42ed2ba 0 {'date': '* *', 'user': 'test'} (glob) Test rebase with deleted empty revision @@ -92,11 +101,11 @@ $ hg ci -m changea $ hg rebase -d 1 $ glog --hidden - o 5:4e322f7ce8e3@foo(secret) changea + x 5:4e322f7ce8e3@foo(draft) changea | | o 4:9c5494949763@default(draft) adde | | - | | o 3:98e4a024635e@default(secret) adde + | | x 3:98e4a024635e@default(draft) adde | | | +---o 2:102a90ea7b4a@default(draft) addb | | @@ -104,9 +113,9 @@ |/ o 0:07f494440405@default(draft) adda - $ hg debugsuccessors - 4e322f7ce8e3 000000000000 - 98e4a024635e 9c5494949763 + $ hg debugobsolete + 98e4a024635e8c50928144c9277a4388d26bd786 9c54949497631abfb5a255d96746bbd3a42ed2ba 0 {'date': '* *', 'user': 'test'} (glob) + 4e322f7ce8e3e4203950eac9ece27bf7e45ffa6c 540395c442253af3b991be882b539e7e198b5808 0 {'date': '* *', 'user': 'test'} (glob) Test rebase --collapse @@ -123,15 +132,15 @@ $ glog --hidden @ 8:a7773ffa7edc@default(draft) Collapsed revision | - | o 7:03f31481307a@default(secret) changec + | x 7:03f31481307a@default(draft) changec | | - | o 6:076e9b2ffbe1@default(secret) addc + | x 6:076e9b2ffbe1@default(draft) addc | | - | | o 5:4e322f7ce8e3@foo(secret) changea + | | x 5:4e322f7ce8e3@foo(draft) changea | |/ +---o 4:9c5494949763@default(draft) adde | | - | | o 3:98e4a024635e@default(secret) adde + | | x 3:98e4a024635e@default(draft) adde | | | | | o 2:102a90ea7b4a@default(draft) addb | |/ @@ -139,15 +148,15 @@ |/ o 0:07f494440405@default(draft) adda - $ hg debugsuccessors - 03f31481307a a7773ffa7edc - 076e9b2ffbe1 a7773ffa7edc - 4e322f7ce8e3 000000000000 - 98e4a024635e 9c5494949763 + $ hg debugobsolete + 98e4a024635e8c50928144c9277a4388d26bd786 9c54949497631abfb5a255d96746bbd3a42ed2ba 0 {'date': '* *', 'user': 'test'} (glob) + 4e322f7ce8e3e4203950eac9ece27bf7e45ffa6c 540395c442253af3b991be882b539e7e198b5808 0 {'date': '* *', 'user': 'test'} (glob) + 076e9b2ffbe11c7bcb9ee97f5c0c8b88a1a10b93 a7773ffa7edcfac27b5dcdb2d5c1036e15a49861 0 {'date': '* *', 'user': 'test'} (glob) + 03f31481307aaf5275d07ec28c1c59931759ccd2 a7773ffa7edcfac27b5dcdb2d5c1036e15a49861 0 {'date': '* *', 'user': 'test'} (glob) Test rebase --abort - $ hg debugsuccessors > ../successors.old + $ hg debugobsolete > ../successors.old $ hg up 0 1 files updated, 0 files merged, 1 files removed, 0 files unresolved $ echo d > d @@ -164,7 +173,7 @@ $ hg rebase --abort saved backup bundle to $TESTTMP/repo/.hg/strip-backup/03f165c84ea8-backup.hg rebase aborted - $ hg debugsuccessors > ../successors.new + $ hg debugobsolete > ../successors.new $ diff -u ../successors.old ../successors.new Test rebase --continue @@ -182,21 +191,21 @@ | o 11:03f165c84ea8@default(draft) addd | - | o 10:4b9d80f48523@default(secret) appendab + | x 10:4b9d80f48523@default(draft) appendab | | - | o 9:a31943eabc43@default(secret) addd + | x 9:a31943eabc43@default(draft) addd | | +---o 8:a7773ffa7edc@default(draft) Collapsed revision | | - | | o 7:03f31481307a@default(secret) changec + | | x 7:03f31481307a@default(draft) changec | | | - | | o 6:076e9b2ffbe1@default(secret) addc + | | x 6:076e9b2ffbe1@default(draft) addc | |/ - | | o 5:4e322f7ce8e3@foo(secret) changea + | | x 5:4e322f7ce8e3@foo(draft) changea | |/ +---o 4:9c5494949763@default(draft) adde | | - | | o 3:98e4a024635e@default(secret) adde + | | x 3:98e4a024635e@default(draft) adde | | | | | o 2:102a90ea7b4a@default(draft) addb | |/ @@ -204,15 +213,14 @@ |/ o 0:07f494440405@default(draft) adda - $ hg debugsuccessors > ../successors.new + $ hg debugobsolete > ../successors.new $ diff -u ../successors.old ../successors.new --- ../successors.old* (glob) +++ ../successors.new* (glob) - @@ -1,4 +1,6 @@ - 03f31481307a a7773ffa7edc - 076e9b2ffbe1 a7773ffa7edc - +4b9d80f48523 1951ead97108 - 4e322f7ce8e3 000000000000 - 98e4a024635e 9c5494949763 - +a31943eabc43 03f165c84ea8 + @@ -2,3 +2,5 @@ + 4e322f7ce8e3e4203950eac9ece27bf7e45ffa6c 540395c442253af3b991be882b539e7e198b5808 0 {'date': '* *', 'user': 'test'} (glob) + 076e9b2ffbe11c7bcb9ee97f5c0c8b88a1a10b93 a7773ffa7edcfac27b5dcdb2d5c1036e15a49861 0 {'date': '* *', 'user': 'test'} (glob) + 03f31481307aaf5275d07ec28c1c59931759ccd2 a7773ffa7edcfac27b5dcdb2d5c1036e15a49861 0 {'date': '* *', 'user': 'test'} (glob) + +a31943eabc4327df16f9eca71bf7779c32f815f7 03f165c84ea8889fc35a64a392caa7a0084dd212 0 {'date': '* *', 'user': 'test'} (glob) + +4b9d80f48523e296f4402cc8e37236b768dfb981 1951ead9710803dbf117e95901954d5ed717f80b 0 {'date': '* *', 'user': 'test'} (glob) [1] diff -r f17a0f801e0b -r a9c27df23129 tests/test-obsolete.t --- a/tests/test-obsolete.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-obsolete.t Fri Aug 24 11:53:55 2012 +0200 @@ -8,13 +8,17 @@ > odiff=diff --rev 'limit(precursors(.),1)' --rev . > [extensions] > hgext.graphlog= + > hgext.rebase= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" > hg ci -m "add $1" > } + $ getid() { + > hg id --debug -ir "$1" + > } $ alias qlog="hg log --template='{rev}\n- {node|short}\n'" $ hg init local @@ -27,9 +31,21 @@ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ mkcommit obsol_c # 3 created new head - $ hg debugobsolete 3 2 + $ getid 2 + 4538525df7e2b9f09423636c61ef63a4cb872a2d + $ getid 3 + 0d3f46688ccc6e756c7e96cf64c391c411309597 + $ hg debugobsolete 4538525df7e2b9f09423636c61ef63a4cb872a2d 0d3f46688ccc6e756c7e96cf64c391c411309597 + $ hg debugobsolete + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0d3f46688ccc6e756c7e96cf64c391c411309597 0 {'date': '', 'user': 'test'} (glob) +Test hidden() revset + + $ qlog -r 'hidden()' --hidden + 2 + - 4538525df7e2 + Test that obsolete changeset are hidden $ qlog @@ -82,7 +98,7 @@ $ hg up 1 -q $ mkcommit "obsol_c'" # 4 (on 1) created new head - $ hg debugobsolete 4 3 + $ hg debugobsolete `getid 3` `getid 4` $ qlog 4 - 725c380fe99b @@ -108,7 +124,7 @@ $ hg up 3 -q Working directory parent is obsolete $ mkcommit d # 5 (on 3) - 1 new unstables changesets + 1 new unstable changesets $ qlog -r 'obsolete()' 3 - 0d3f46688ccc @@ -127,13 +143,13 @@ $ hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' \ > --hidden - @ 5:a7a6f2b5d8a5@default(unstable/secret) add d + @ 5:a7a6f2b5d8a5@default(unstable/draft) add d | | o 4:725c380fe99b@default(stable/draft) add obsol_c' | | - o | 3:0d3f46688ccc@default(suspended/secret) add obsol_c + x | 3:0d3f46688ccc@default(suspended/draft) add obsol_c |/ - | o 2:4538525df7e2@default(extinct/secret) add c + | x 2:4538525df7e2@default(extinct/draft) add c |/ o 1:7c3bad9141dc@default(stable/draft) add b | @@ -144,11 +160,13 @@ $ hg init ../other-new $ hg phase --draft 'secret() - extinct()' # until we fix exclusion + abort: empty revision set + [255] $ hg push ../other-new pushing to ../other-new searching for changes abort: push includes an unstable changeset: a7a6f2b5d8a5! - (use 'hg stabilize' to get a stable history (or --force to proceed)) + (use 'hg evolve' to get a stable history or --force to ignore warnings) [255] $ hg push -f ../other-new pushing to ../other-new @@ -181,8 +199,8 @@ Working directory parent is obsolete $ mkcommit obsol_d # 6 created new head - 1 new unstables changesets - $ hg debugobsolete 6 5 + 1 new unstable changesets + $ hg debugobsolete `getid 5` `getid 6` $ qlog 6 - 95de7fc6918d @@ -201,7 +219,7 @@ pushing to ../other-new searching for changes abort: push includes an unstable changeset: 95de7fc6918d! - (use 'hg stabilize' to get a stable history (or --force to proceed)) + (use 'hg evolve' to get a stable history or --force to ignore warnings) [255] $ hg push ../other-new -f # use f because there is unstability pushing to ../other-new @@ -230,15 +248,15 @@ $ hg push ../other-new pushing to ../other-new searching for changes - no changes found (ignored 0 secret changesets) + no changes found [1] $ hg up -q .^ # 3 Working directory parent is obsolete $ mkcommit "obsol_d'" # 7 created new head - 1 new unstables changesets - $ hg debugobsolete 7 6 + 1 new unstable changesets + $ hg debugobsolete `getid 6` `getid 7` $ hg pull -R ../other-new . pulling from . searching for changes @@ -261,55 +279,58 @@ pushing to stuff that doesn't support obsolete - $ hg init ../other-old - > # XXX I don't like this but changeset get published otherwise - > # remove it when we will get a --keep-state flag for push - $ echo '[extensions]' > ../other-old/.hg/hgrc - $ echo "obsolete=!$(echo $(dirname $TESTDIR))/obsolete.py" >> ../other-old/.hg/hgrc - $ hg push ../other-old - pushing to ../other-old - searching for changes - abort: push includes an unstable changeset: 909a0fb57e5d! - (use 'hg stabilize' to get a stable history (or --force to proceed)) - [255] - $ hg push -f ../other-old - pushing to ../other-old - searching for changes - adding changesets - adding manifests - adding file changes - added 5 changesets with 5 changes to 5 files (+1 heads) - $ qlog -R ../other-old - 4 - - 909a0fb57e5d - 3 - - 725c380fe99b - 2 - - 0d3f46688ccc - 1 - - 7c3bad9141dc - 0 - - 1f0dee641bb7 +DISABLED. the _enable switch it global :-/ + +.. $ hg init ../other-old +.. > # XXX I don't like this but changeset get published otherwise +.. > # remove it when we will get a --keep-state flag for push +.. $ echo '[extensions]' > ../other-old/.hg/hgrc +.. $ echo "obsolete=!$(echo $(dirname $TESTDIR))/obsolete.py" >> ../other-old/.hg/hgrc +.. $ hg push ../other-old +.. pushing to ../other-old +.. searching for changes +.. abort: push includes an unstable changeset: 909a0fb57e5d! +.. (use 'hg evolve' to get a stable history or --force to ignore warnings) +.. [255] +.. $ hg push -f ../other-old +.. pushing to ../other-old +.. searching for changes +.. adding changesets +.. adding manifests +.. adding file changes +.. added 5 changesets with 5 changes to 5 files (+1 heads) +.. $ qlog -R ../other-ol +.. 4 +.. - 909a0fb57e5d +.. 3 +.. - 725c380fe99b +.. 2 +.. - 0d3f46688ccc +.. 1 +.. - 7c3bad9141dc +.. 0 +.. - 1f0dee641bb7 clone support $ hg clone . ../cloned > # The warning should go away once we have default value to set ready before we pull - requesting all changes - adding changesets - adding manifests - adding file changes - added 5 changesets with 5 changes to 5 files (+1 heads) updating to branch default 4 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ qlog -R ../cloned + $ qlog -R ../cloned --hidden + 7 + - 909a0fb57e5d + 6 + - 95de7fc6918d + 5 + - a7a6f2b5d8a5 4 - - 909a0fb57e5d + - 725c380fe99b 3 - - 725c380fe99b + - 0d3f46688ccc 2 - - 0d3f46688ccc + - 4538525df7e2 1 - 7c3bad9141dc 0 @@ -321,8 +342,8 @@ Working directory parent is obsolete $ mkcommit "obsol_d''" created new head - 1 new unstables changesets - $ hg debugobsolete 8 7 + 1 new unstable changesets + $ hg debugobsolete `getid 7` `getid 8` $ cd ../other-new $ hg up -q 3 $ hg pull ../local/ @@ -355,6 +376,8 @@ branch: default commit: 1 deleted, 2 unknown (clean) update: 4 new changesets, 4 branch heads (merge) + unstable: 1 changesets + conflicting: 1 changesets $ qlog 6 - 909a0fb57e5d @@ -379,8 +402,10 @@ created new head $ hg id -n 9 - $ hg debugobsolete 9 0 - 83b5778897ad try to obsolete immutable changeset 1f0dee641bb7 + $ hg debugobsolete `getid 0` `getid 9` +83b5778897ad try to obsolete immutable changeset 1f0dee641bb7 +# at core level the warning is not issued +# this is now a big issue now that we have latecomer warning $ qlog -r 'obsolete()' 3 - 0d3f46688ccc @@ -400,7 +425,7 @@ 0 - 1f0dee641bb7 - $ hg debugobsolete null 9 #kill + $ hg debugobsolete `getid 9` #kill $ hg up null -q # to be not based on 9 anymore $ qlog 8 @@ -414,6 +439,23 @@ 0 - 1f0dee641bb7 +Check that auto update ignore hidden changeset + $ hg up 0 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg up + 3 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg id -n + 8 + +Check that named update do too + + $ hg update default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg id -n + 8 + + $ hg up null -q # to be not based on 9 anymore + check rebase compat $ hg glog -r 'not extinct()' --template='{rev} - {node|short}\n' @@ -421,29 +463,29 @@ | | o 4 - 725c380fe99b | | - o | 3 - 0d3f46688ccc + x | 3 - 0d3f46688ccc |/ o 1 - 7c3bad9141dc | o 0 - 1f0dee641bb7 - $ hg glog --template='{rev} - {node|short}\n' `(hg --version | grep -q 'version 2.1') || echo '--hidden'` - o 9 - 83b5778897ad + $ hg glog --template='{rev} - {node|short}\n' --hidden + x 9 - 83b5778897ad o 8 - 159dfc9fa5d3 | - | o 7 - 909a0fb57e5d + | x 7 - 909a0fb57e5d |/ - | o 6 - 95de7fc6918d + | x 6 - 95de7fc6918d |/ - | o 5 - a7a6f2b5d8a5 + | x 5 - a7a6f2b5d8a5 |/ | o 4 - 725c380fe99b | | - o | 3 - 0d3f46688ccc + x | 3 - 0d3f46688ccc |/ - | o 2 - 4538525df7e2 + | x 2 - 4538525df7e2 |/ o 1 - 7c3bad9141dc | @@ -452,6 +494,10 @@ should not rebase extinct changeset + $ hg --config extensions.hgext.rebase= rebase -s 7 -d 4 + whole rebase set is extinct and ignored. + nothing to rebase + [1] $ hg --config extensions.hgext.rebase= rebase -b 3 -d 4 --traceback $ hg --config extensions.graphlog= glog -r 'not extinct()' --template='{rev} - {node|short}\n' @ 11 - 9468a5f5d8b2 @@ -477,7 +523,7 @@ $ hg up -q 10 $ mkcommit "obsol_d'''" created new head - $ hg debugobsolete 12 11 + $ hg debugobsolete `getid 11` `getid 12` $ hg push ../other-new --traceback pushing to ../other-new searching for changes @@ -492,7 +538,7 @@ $ cd local $ hg phase --public 11 - 1 new latecomers changesets + 1 new latecomer changesets $ hg --config extensions.graphlog=glog glog --template='{rev} - ({phase}) {node|short} {desc}\n' @ 12 - (draft) 6db5e282cb91 add obsol_d''' | @@ -517,8 +563,8 @@ $ hg push ../other-new/ pushing to ../other-new/ searching for changes - abort: push includes an latecomer changeset: 6db5e282cb91! - (use 'hg stabilize' to get a stable history (or --force to proceed)) + abort: push includes a latecomer changeset: 6db5e282cb91! + (use 'hg evolve' to get a stable history or --force to ignore warnings) [255] Check hg commit --amend compat @@ -571,18 +617,18 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: add a - $ hg debugsuccessors - 0b1b6dd009c0 3734a65252e6 - 0d3f46688ccc 2033b4e49474 - 0d3f46688ccc 725c380fe99b - 159dfc9fa5d3 9468a5f5d8b2 - 1f0dee641bb7 83b5778897ad - 4538525df7e2 0d3f46688ccc - 83b5778897ad 000000000000 - 909a0fb57e5d 159dfc9fa5d3 - 9468a5f5d8b2 6db5e282cb91 - 95de7fc6918d 909a0fb57e5d - a7a6f2b5d8a5 95de7fc6918d + $ hg debugobsolete + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0d3f46688ccc6e756c7e96cf64c391c411309597 0 {'date': '', 'user': 'test'} (glob) + 0d3f46688ccc6e756c7e96cf64c391c411309597 725c380fe99b5e76613493f0903e8d11ddc70d54 0 {'date': '', 'user': 'test'} (glob) + a7a6f2b5d8a54b81bc7aa2fba2934ad6d700a79e 95de7fc6918dea4c9c8d5382f50649794b474c4a 0 {'date': '', 'user': 'test'} (glob) + 95de7fc6918dea4c9c8d5382f50649794b474c4a 909a0fb57e5d909f353d89e394ffd7e0890fec88 0 {'date': '', 'user': 'test'} (glob) + 909a0fb57e5d909f353d89e394ffd7e0890fec88 159dfc9fa5d334d7e03a0aecfc7f7ab4c3431fea 0 {'date': '', 'user': 'test'} (glob) + 1f0dee641bb7258c56bd60e93edfa2405381c41e 83b5778897adafb967ef2f75be3aaa4fce49a4cc 0 {'date': '', 'user': 'test'} (glob) + 83b5778897adafb967ef2f75be3aaa4fce49a4cc 0 {'date': '', 'user': 'test'} (glob) + 0d3f46688ccc6e756c7e96cf64c391c411309597 2033b4e494742365851fac84d276640cbf52833e 0 {'date': '* *', 'user': 'test'} (glob) + 159dfc9fa5d334d7e03a0aecfc7f7ab4c3431fea 9468a5f5d8b2c5d91e17474e95ae4791e9718fdf 0 {'date': '* *', 'user': 'test'} (glob) + 9468a5f5d8b2c5d91e17474e95ae4791e9718fdf 6db5e282cb91df5c43ff1f1287c119ff83230d42 0 {'date': '', 'user': 'test'} (glob) + 0b1b6dd009c037985363e2290a0b579819f659db 3734a65252e69ddcced85901647a4f335d40de1e 0 {'date': '* *', 'user': 'test'} (glob) Check conflict detection @@ -595,7 +641,8 @@ branch: default commit: (clean) update: 9 new changesets, 9 branch heads (merge) - $ hg debugobsolete 50f11e5e3a63 a7a6f2b5d8a5 + latecomer: 1 changesets + $ hg debugobsolete `getid a7a6f2b5d8a5` `getid 50f11e5e3a63` $ hg log -r 'conflicting()' changeset: 14:50f11e5e3a63 tag: tip diff -r f17a0f801e0b -r a9c27df23129 tests/test-oldconvert.t --- a/tests/test-oldconvert.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-oldconvert.t Fri Aug 24 11:53:55 2012 +0200 @@ -53,8 +53,8 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: add a - $ hg debugsuccessors - 7c3bad9141dc d67cd0334eee + $ hg debugobsolete + 7c3bad9141dcb46ff89abf5f61856facd56e476c d67cd0334eeecfded222fed9009f0db4beb57585 0 {'date': '* *', 'user': 'test'} (glob) $ hg debugconvertobsolete nothing to do 0 obsolete marker converted @@ -107,8 +107,8 @@ [255] $ hg debugconvertobsolete --traceback 3 obsolete marker converted - $ hg debugsuccessors - 2c3784e102bb - 3e03d82708d4 3218406b50ed - 5c722672795c - 7c3bad9141dc d67cd0334eee + $ hg debugobsolete + 7c3bad9141dcb46ff89abf5f61856facd56e476c d67cd0334eeecfded222fed9009f0db4beb57585 0 {'date': '* *', 'user': 'test'} (glob) + 3e03d82708d4da97a92158558dd13386d8f09ad5 3218406b50ed13480765e7c260669620f37fba6e 0 {'date': '* *', 'user': 'Pierre-Yves David '} (glob) + 5c722672795c3a2cb94d0cc9a821c394c1475f87 0 {'date': '* *', 'user': 'Pierre-Yves David '} (glob) + 2c3784e102bb34ccc93862af5bd6d609ee30c577 0 {'date': '* *', 'user': 'Pierre-Yves David '} (glob) diff -r f17a0f801e0b -r a9c27df23129 tests/test-qsync.t --- a/tests/test-qsync.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-qsync.t Fri Aug 24 11:53:55 2012 +0200 @@ -17,7 +17,6 @@ > hgext.graphlog= > hgext.mq= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ echo "qsync=$(echo $(dirname $TESTDIR))/hgext/qsync.py" >> $HGRCPATH $ mkcommit() { @@ -84,7 +83,7 @@ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ echo "a" >> a $ hg amend - 1 new unstables changesets + 1 new unstable changesets $ hg graft -O 3 grafting revision 3 $ hg qsync -a @@ -182,7 +181,6 @@ pulling from ../local2 searching for changes no changes found - (run 'hg update' to get a working copy) $ hg pull --mq ../local2/.hg/patches pulling from ../local2/.hg/patches searching for changes diff -r f17a0f801e0b -r a9c27df23129 tests/test-stabilize-order.t --- a/tests/test-stabilize-order.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-stabilize-order.t Fri Aug 24 11:53:55 2012 +0200 @@ -5,7 +5,6 @@ > hgext.rebase= > hgext.graphlog= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ glog() { @@ -40,13 +39,13 @@ [2] addb $ echo b >> b $ hg amend - 1 new unstables changesets + 1 new unstable changesets $ hg gdown 0 files updated, 0 files merged, 1 files removed, 0 files unresolved [1] adda $ echo a >> a $ hg amend - 1 new unstables changesets + 1 new unstable changesets $ glog @ 7:f5ff10856e5a@default(draft) adda | @@ -54,16 +53,16 @@ | | | | o 3:7a7552255fb5@default(draft) addc | | | - | | o 2:ef23d6ef94d6@default(draft) addb + | | x 2:ef23d6ef94d6@default(draft) addb | |/ - | o 1:93418d2c0979@default(draft) adda + | x 1:93418d2c0979@default(draft) adda |/ o 0:c471ef929e6a@default(draft) addroot Test stabilizing a predecessor child - $ hg stabilize -v + $ hg evolve -v move:[5] addb atop:[7] adda hg rebase -Dr ab8cbb6d87ff -d f5ff10856e5a @@ -77,9 +76,9 @@ | | o 3:7a7552255fb5@default(draft) addc | | - | o 2:ef23d6ef94d6@default(draft) addb + | x 2:ef23d6ef94d6@default(draft) addb | | - | o 1:93418d2c0979@default(draft) adda + | x 1:93418d2c0979@default(draft) adda |/ o 0:c471ef929e6a@default(draft) addroot @@ -88,8 +87,8 @@ $ hg up 7 0 files updated, 0 files merged, 1 files removed, 0 files unresolved - $ hg debugsuccessors > successors.old - $ hg stabilize -v + $ hg debugobsolete > successors.old + $ hg evolve -v move:[3] addc atop:[8] addb hg rebase -Dr 7a7552255fb5 -d 6bf44048e43f @@ -98,17 +97,15 @@ resolving manifests getting c c - $ hg debugsuccessors > successors.new + $ hg debugobsolete > successors.new $ diff -u successors.old successors.new --- successors.old* (glob) +++ successors.new* (glob) - @@ -1,5 +1,6 @@ - 3a4a591493f8 f5ff10856e5a - 3ca0ded0dc50 ab8cbb6d87ff - +7a7552255fb5 5e819fbb0d27 - 93418d2c0979 f5ff10856e5a - ab8cbb6d87ff 6bf44048e43f - ef23d6ef94d6 ab8cbb6d87ff + @@ -3,3 +3,4 @@ + 3a4a591493f80708e46f2bf6d3b4debfad8ff91e f5ff10856e5ab3c8dc420b9c11460e6832a3b78c 0 {'date': '* *', 'user': 'test'} (glob) + 93418d2c0979643ad446f621195e78720edb05b4 f5ff10856e5ab3c8dc420b9c11460e6832a3b78c 0 {'date': '* *', 'user': 'test'} (glob) + ab8cbb6d87ff3ab5526735a051cba6b63f3d6775 6bf44048e43f830accbf7d2bd7bc252ad7a3b99c 0 {'date': '* *', 'user': 'test'} (glob) + +7a7552255fb5f8bd745e46fba6f0ca633a4dd716 5e819fbb0d278117c0a83b7f6f6486689732cfb2 0 {'date': '* *', 'user': 'test'} (glob) [1] $ glog @ 9:5e819fbb0d27@default(draft) addc @@ -119,8 +116,8 @@ | o 0:c471ef929e6a@default(draft) addroot - $ hg stabilize -v - no unstable changeset + $ hg evolve -v + no troubled changeset [1] Test behaviour with --any @@ -129,13 +126,13 @@ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ echo b >> b $ hg amend - 1 new unstables changesets + 1 new unstable changesets $ glog @ 11:4e7cec6b4afe@default(draft) addb | | o 9:5e819fbb0d27@default(draft) addc | | - | o 8:6bf44048e43f@default(draft) addb + | x 8:6bf44048e43f@default(draft) addb |/ o 7:f5ff10856e5a@default(draft) adda | @@ -143,11 +140,11 @@ $ hg up 9 2 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ hg stabilize -v - nothing to stabilize here - (1 unstable changesets, do you want --any ?) + $ hg evolve -v + nothing to evolve here + (1 troubled changesets, do you want --any ?) [2] - $ hg stabilize --any -v + $ hg evolve --any -v move:[9] addc atop:[11] addb hg rebase -Dr 5e819fbb0d27 -d 4e7cec6b4afe @@ -166,6 +163,6 @@ | o 0:c471ef929e6a@default(draft) addroot - $ hg stabilize --any -v - no unstable changeset + $ hg evolve --any -v + no troubled changeset [1] diff -r f17a0f801e0b -r a9c27df23129 tests/test-stabilize-result.t --- a/tests/test-stabilize-result.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-stabilize-result.t Fri Aug 24 11:53:55 2012 +0200 @@ -5,7 +5,6 @@ > hgext.rebase= > hgext.graphlog= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ glog() { @@ -13,7 +12,7 @@ > '{rev}:{node|short}@{branch}({phase}) bk:[{bookmarks}] {desc|firstline}\n' "$@" > } -Test stabilize removing the changeset being stabilized +Test evolve removing the changeset being evolved $ hg init empty $ cd empty @@ -28,8 +27,8 @@ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ echo a >> a $ hg amend -m changea - 1 new unstables changesets - $ hg stabilize -v + 1 new unstable changesets + $ hg evolve -v move:[2] changea atop:[4] changea hg rebase -Dr cce2c55b8965 -d 1447e1c4828d @@ -37,15 +36,255 @@ $ glog --hidden @ 4:1447e1c4828d@default(draft) bk:[changea] changea | - | o 3:41ad4fe8c795@default(secret) bk:[] amends 102a90ea7b4a3361e4082ed620918c261189a36a + | x 3:41ad4fe8c795@default(draft) bk:[] amends 102a90ea7b4a3361e4082ed620918c261189a36a | | - | | o 2:cce2c55b8965@default(secret) bk:[] changea + | | x 2:cce2c55b8965@default(draft) bk:[] changea | |/ - | o 1:102a90ea7b4a@default(secret) bk:[] addb + | x 1:102a90ea7b4a@default(draft) bk:[] addb |/ o 0:07f494440405@default(draft) bk:[] adda - $ hg debugsuccessors - 102a90ea7b4a 1447e1c4828d - 41ad4fe8c795 1447e1c4828d - cce2c55b8965 000000000000 + $ hg debugobsolete + 41ad4fe8c79565a06c89f032ef0937b3cbd68a04 1447e1c4828d2347df8f858aa041305fa4cf7db1 0 {'date': '* *', 'user': 'test'} (glob) + 102a90ea7b4a3361e4082ed620918c261189a36a 1447e1c4828d2347df8f858aa041305fa4cf7db1 0 {'date': '* *', 'user': 'test'} (glob) + cce2c55b896511e0b6e04173c9450ba822ebc740 0 {'date': '* *', 'user': 'test'} (glob) + +Test evolve with conflict + + $ ls + a + b + $ hg pdiff a + diff -r 07f494440405 a + --- a/a * (glob) + +++ b/a * (glob) + @@ -1,1 +1,2 @@ + a + +a + $ echo 'newer a' >> a + $ hg ci -m 'newer a' + $ hg gdown + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + [4] changea + $ echo 'a' > a + $ hg amend + 1 new unstable changesets + $ hg evolve + move:[5] newer a + atop:[7] changea + merging a + warning: conflicts during merge. + merging a incomplete! (edit conflicts, then use 'hg resolve --mark') + evolve failed! + fix conflict and run "hg evolve --continue" + abort: unresolved merge conflicts (see hg help resolve) + [255] + $ hg revert -r 'unstable()' a + $ hg diff + diff -r e8cc1b534401 a + --- a/a * (glob) + +++ b/a * (glob) + @@ -1,1 +1,3 @@ + a + +a + +newer a + $ hg evolve --continue + grafting revision 5 + abort: unresolved merge conflicts (see hg help resolve) + [255] + $ hg resolve -m a + $ hg evolve --continue + grafting revision 5 + +Stabilize of late comer with different parent +================================================== + +(the with same parent is handled in test-evolve.t) + + $ glog + @ 8:e3183e9c0961@default(draft) bk:[] newer a + | + o 7:e8cc1b534401@default(draft) bk:[changea] changea + | + o 0:07f494440405@default(draft) bk:[] adda + +Add another commit + + $ hg gdown + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + [7] changea + $ echo 'c' > c + $ hg add c + $ hg commit -m 'add c' + created new head + +Get a successors of 8 on it + + $ hg graft -O 8 + grafting revision 8 + +Add real change to the successors + + $ echo 'babar' >> a + $ hg amend + +Make precursors public + + $ hg phase --public 8 + 1 new latecomer changesets + $ glog + @ 12:15c83af6f3a3@default(draft) bk:[] newer a + | + o 9:355c5cda4de1@default(draft) bk:[] add c + | + | o 8:e3183e9c0961@default(public) bk:[] newer a + |/ + o 7:e8cc1b534401@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + +Stabilize ! + + $ hg evolve --any --dry-run + recreate:[12] newer a + atop:[8] newer a + hg rebase --rev 15c83af6f3a3 --detach e8cc1b534401; + hg update e3183e9c0961; + hg revert --all --rev 15c83af6f3a3; + hg commit --msg "latecomer update to %s" (no-eol) + $ hg evolve --any + recreate:[12] newer a + atop:[8] newer a + rebasing to destination parent: e8cc1b534401 + computing new diff + commited as 1d94fef80e85 + $ glog + @ 14:1d94fef80e85@default(draft) bk:[] latecomer update to e3183e9c0961: + | + | o 9:355c5cda4de1@default(draft) bk:[] add c + | | + o | 8:e3183e9c0961@default(public) bk:[] newer a + |/ + o 7:e8cc1b534401@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + +Stabilize of conflicting changeset with same parent +==================================================== + + $ rm a.orig + $ hg up 9 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat << EOF >> a + > flore + > arthur + > zephir + > some + > less + > conflict + > EOF + $ hg ci -m 'More addition' + created new head + $ glog + @ 15:7391601a4bfa@default(draft) bk:[] More addition + | + | o 14:1d94fef80e85@default(draft) bk:[] latecomer update to e3183e9c0961: + | | + o | 9:355c5cda4de1@default(draft) bk:[] add c + | | + | o 8:e3183e9c0961@default(public) bk:[] newer a + |/ + o 7:e8cc1b534401@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + $ echo 'babar' >> a + $ hg amend + $ hg up 15 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + Working directory parent is obsolete + $ mv a a.old + $ echo 'jungle' > a + $ cat a.old >> a + $ rm a.old + $ hg amend + 2 new conflicting changesets + $ glog + @ 19:3883461cc228@default(draft) bk:[] More addition + | + | o 17:4754d61bc2db@default(draft) bk:[] More addition + |/ + | o 14:1d94fef80e85@default(draft) bk:[] latecomer update to e3183e9c0961: + | | + o | 9:355c5cda4de1@default(draft) bk:[] add c + | | + | o 8:e3183e9c0961@default(public) bk:[] newer a + |/ + o 7:e8cc1b534401@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + +Stabilize It + + $ hg evolve -qn + hg update -c 3883461cc228 && + hg merge 4754d61bc2db && + hg commit -m "auto merge resolving conflict between 3883461cc228 and 4754d61bc2db"&& + hg up -C 7391601a4bfa && + hg revert --all --rev tip && + hg commit -m "`hg log -r 3883461cc228 --template={desc}`"; + $ hg evolve -v + merge:[19] More addition + with: [17] More addition + base: [15] More addition + merging conflicting changeset + resolving manifests + merging a + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + a + a + $ hg st + $ hg amend -d '0 0' -m 'More addition' # kill date variation XXX should be done in evolve + $ glog + @ 22:ac6d600735a4@default(draft) bk:[] More addition + | + | o 14:1d94fef80e85@default(draft) bk:[] latecomer update to e3183e9c0961: + | | + o | 9:355c5cda4de1@default(draft) bk:[] add c + | | + | o 8:e3183e9c0961@default(public) bk:[] newer a + |/ + o 7:e8cc1b534401@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + $ hg summary + parent: 22:ac6d600735a4 tip + More addition + branch: default + commit: (clean) + update: 19 new changesets, 14 branch heads (merge) + $ hg export . + # HG changeset patch + # User test + # Date 0 0 + # Node ID ac6d600735a49ee377e29d1f74a0576e8c972e7b + # Parent 355c5cda4de162658ed9f961a98a73a10b3167b1 + More addition + + diff -r 355c5cda4de1 -r ac6d600735a4 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,1 +1,9 @@ + +jungle + a + +flore + +arthur + +zephir + +some + +less + +conflict + +babar diff -r f17a0f801e0b -r a9c27df23129 tests/test-tutorial.t --- a/tests/test-tutorial.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-tutorial.t Fri Aug 24 11:53:55 2012 +0200 @@ -1,1 +1,772 @@ -../docs/tutorials/tutorial.t \ No newline at end of file + +Initial setup +------------- + +This Mercurial configuration example is used for testing. +.. Various setup + + $ cat >> $HGRCPATH << EOF + > [ui] + > logtemplate ="{node|short} ({phase}): {desc}\n" + > [diff] + > git = 1 + > [alias] + > # "-d '0 0'" means that the new commit will be at January 1st 1970. + > # This is used for stable hash during test + > amend = amend -d '0 0' + > [extensions] + > hgext.graphlog= + > EOF + + $ hg init local + $ cat >> local/.hg/hgrc << EOF + > [paths] + > remote = ../remote + > other = ../other + > [ui] + > user = Babar the King + > EOF + + $ hg init remote + $ cat >> remote/.hg/hgrc << EOF + > [paths] + > local = ../local + > [ui] + > user = Celestine the Queen + > EOF + + $ hg init other + $ cat >> other/.hg/hgrc << EOF + > [ui] + > user = Princess Flore + > EOF + + +This tutorial use the following configuration for Mercurial: + +A compact log template with phase data: + + $ hg showconfig ui + ui.slash=True + ui.logtemplate="{node|short} ({phase}): {desc}\n" + +Improved git format diff: + + $ hg showconfig diff + diff.git=1 + +And the graphlog extension + $ hg showconfig extensions + extensions.hgext.graphlog= + +And of course, we anabled the experimental extensions for mutable history: + + $ $(dirname $TESTDIR)/enable.sh >> $HGRCPATH 2> /dev/null + + +----------------------- +Single Developer Usage +----------------------- + +This tutorial shows how to use evolution to rewrite history locally. + + +Fixing mistake with `hg amend` +-------------------------------- + +We are versionning a shopping list + + $ cd local + $ cat >> shopping << EOF + > Spam + > Whizzo butter + > Albatross + > Rat (rather a lot) + > Jugged fish + > Blancmange + > Salmon mousse + > EOF + $ hg commit -A -m "Monthy Python Shopping list" + adding shopping + +Its first version is shared with the outside. + + $ hg push remote + pushing to $TESTTMP/remote + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + +Later I add additional item to my list + + $ cat >> shopping << EOF + > Egg + > Suggar + > Vinegar + > Oil + > EOF + $ hg commit -m "adding condiment" + $ cat >> shopping << EOF + > Bananos + > Pear + > Apple + > EOF + $ hg commit -m "adding fruit" + +This history is very linear + + $ hg glog + @ d85de4546133 (draft): adding fruit + | + o 4d5dc8187023 (draft): adding condiment + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +But a typo was made in Babanas! + + $ hg export tip + # HG changeset patch + # User test + # Date 0 0 + # Node ID d85de4546133030c82d257bbcdd9b1b416d0c31c + # Parent 4d5dc81870237d492284826e21840b2ca00e26d1 + adding fruit + + diff --git a/shopping b/shopping + --- a/shopping + +++ b/shopping + @@ -9,3 +9,6 @@ + Suggar + Vinegar + Oil + +Bananos + +Pear + +Apple + +The faulty changeset is in the "draft" phase because he was not exchanged with +the outside. The first one have been exchanged and is an immutable public +changeset. + + $ hg glog + @ d85de4546133 (draft): adding fruit + | + o 4d5dc8187023 (draft): adding condiment + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +hopefully. I can use hg amend to rewrite my faulty changeset! + + $ sed -i'' -e s/Bananos/Banana/ shopping + $ hg diff + diff --git a/shopping b/shopping + --- a/shopping + +++ b/shopping + @@ -9,6 +9,6 @@ + Suggar + Vinegar + Oil + -Bananos + +Banana + Pear + Apple + $ hg amend + +A new changeset with the right diff replace the wrong one. + + $ hg glog + @ 0cacb48f4482 (draft): adding fruit + | + o 4d5dc8187023 (draft): adding condiment + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + $ hg export tip + # HG changeset patch + # User test + # Date 0 0 + # Node ID 0cacb48f44828d2fd31c4e45e18fde32a5b2f07b + # Parent 4d5dc81870237d492284826e21840b2ca00e26d1 + adding fruit + + diff --git a/shopping b/shopping + --- a/shopping + +++ b/shopping + @@ -9,3 +9,6 @@ + Suggar + Vinegar + Oil + +Banana + +Pear + +Apple + +Getting Ride of branchy history +---------------------------------- + +While I was working on my list. someone help made a change remotly. + + $ cd ../remote + $ hg up -q + $ sed -i'' -e 's/Spam/Spam Spam Spam/' shopping + $ hg ci -m 'SPAM' + $ cd ../local + +I'll get this remote changeset when pulling + + $ hg pull remote + pulling from $TESTTMP/remote + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + +I now have a new heads. Note that this remote head is immutable + + $ hg log -G + o 9ca060c80d74 (public): SPAM + | + | @ 0cacb48f4482 (draft): adding fruit + | | + | o 4d5dc8187023 (draft): adding condiment + |/ + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +instead of merging my head with the new one. I'm going to rebase my work + + $ hg diff + $ hg rebase -d 9ca060c80d74 -s 4d5dc8187023 + merging shopping + merging shopping + + +My local work is now rebased on the remote one. + + $ hg log -G + @ 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Removing changeset +------------------------ + +I add new item to my list + + $ cat >> shopping << EOF + > car + > bus + > plane + > boat + > EOF + $ hg ci -m 'transport' + $ hg log -G + @ d58c77aa15d7 (draft): transport + | + o 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +I have a new commit but I realize that don't want it. (transport shop list does +not fit well in my standard shopping list) + + $ hg prune . # . is for working directory parent + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory now at 387187ad9bd9 + +The silly changeset is gone. + + $ hg log -G + @ 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Reordering changeset +------------------------ + + +We create two changesets. + + + $ cat >> shopping << EOF + > Shampoo + > Toothbrush + > ... More bathroom stuff to come + > Towel + > Soap + > EOF + $ hg ci -m 'bathroom stuff' -q # XXX remove the -q + + $ sed -i'' -e 's/Spam/Spam Spam Spam/g' shopping + $ hg ci -m 'SPAM SPAM' + $ hg log -G + @ c48f32fb1787 (draft): SPAM SPAM + | + o 8d39a843582d (draft): bathroom stuff + | + o 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +.. note: don't amend changeset 7e82d3f3c2cb or 9ca060c80d74 as they are immutable. + +I now want to push to remote all my change but the bathroom one that i'm not +totally happy with yet. To be able to push "SPAM SPAM" I need a version of "SPAM SPAM" not children of +"bathroom stuff" + +You can use 'rebase -r' or 'graft -O' for that: + + $ hg up 'p1(8d39a843582d)' # going on "bathroom stuff" parent + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg graft -O c48f32fb1787 # moving "SPAM SPAM" to the working directory parent + grafting revision 10 + merging shopping + $ hg log -G + @ a2fccc2e7b08 (draft): SPAM SPAM + | + | o 8d39a843582d (draft): bathroom stuff + |/ + o 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +We have a new SPAM SPAM version without the bathroom stuff + + $ grep Spam shopping # enouth spam + Spam Spam Spam Spam Spam Spam Spam Spam Spam + $ grep Toothbrush shopping # no Toothbrush + [1] + $ hg export . + # HG changeset patch + # User test + # Date 0 0 + # Node ID a2fccc2e7b08bbce6af7255b989453f7089e4cf0 + # Parent 387187ad9bd9d8f9a00a9fa804a26231db547429 + SPAM SPAM + + diff --git a/shopping b/shopping + --- a/shopping + +++ b/shopping + @@ -1,4 +1,4 @@ + -Spam Spam Spam + +Spam Spam Spam Spam Spam Spam Spam Spam Spam + Whizzo butter + Albatross + Rat (rather a lot) + +To make sure I do not push unready changeset by mistake I set the "bathroom +stuff" changeset in the secret phase. + + $ hg phase --force --secret 8d39a843582d + +we can now push our change: + + $ hg push remote + pushing to $TESTTMP/remote + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 1 files + +for simplicity shake we get the bathroom change in line again + + $ hg rebase -Dr 8d39a843582d -d a2fccc2e7b08 + merging shopping + $ hg phase --draft . + $ hg log -G + @ 8a79ae8b029e (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + + +Splitting change +------------------ + +To be done (currently achieve with "two commit + debugobsolete") + +Collapsing change +------------------ + +To be done (currently achieve with "revert + debugobsolete" or "rebase --collapse") + + + + + + +----------------------- +Collaboration +----------------------- + + +sharing mutable changeset +---------------------------- + +To share mutable changeset with other just check that the repo you interact +with is "not publishing". Otherwise you will get the previously observe +behavior where exchanged changeset are automatically published. + + $ cd ../remote + $ hg -R ../local/ showconfig phases + +the localrepo does not have any specific configuration for `phases.publish`. It +is ``true`` by default. + + $ hg pull local + pulling from $TESTTMP/local + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg log -G + o 8a79ae8b029e (public): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + @ 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + + +We do not want to publish the "bathroom changeset". Let's rollback the last transaction + + $ hg rollback + repository tip rolled back to revision 4 (undo pull) + $ hg log -G + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + @ 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Let's make the local repo "non publishing" + + $ echo '[phases]' >> ../local/.hg/hgrc + $ echo 'publish=false' >> ../local/.hg/hgrc + $ echo '[phases]' >> .hg/hgrc + $ echo 'publish=false' >> .hg/hgrc + $ hg showconfig phases + phases.publish=false + $ hg -R ../local/ showconfig phases + phases.publish=false + + +I can now exchange mutable changeset between "remote" and "local" repository. + + $ hg pull local + pulling from $TESTTMP/local + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg log -G + o 8a79ae8b029e (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + @ 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Rebasing unstable change after pull +---------------------------------------------- + +Remotely someone add a new changeset on top of the mutable "bathroom" on. + + $ hg up 8a79ae8b029e -q + $ cat >> shopping << EOF + > Giraffe + > Rhino + > Lion + > Bear + > EOF + $ hg ci -m 'animals' + +But at the same time, locally, this same "bathroom changeset" was updated. + + $ cd ../local + $ hg up 8a79ae8b029e -q + $ sed -i'' -e 's/... More bathroom stuff to come/Bath Robe/' shopping + $ hg amend + $ hg log -G + @ ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + +When we pull from remote again we get an unstable state! + + + $ hg pull remote + pulling from $TESTTMP/remote + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + 1 new unstable changesets + + +The new changeset "animal" is based one an old changeset of "bathroom". You can +see both version showing up in the log. + + $ hg log -G + o 9ac5d0e790a2 (draft): animals + | + | @ ffa278c50818 (draft): bathroom stuff + | | + x | 8a79ae8b029e (draft): bathroom stuff + |/ + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +The older version 8a79ae8b029e never ceased to exist in the local repo. It was +jsut hidden and excluded from pull and push. + +.. note:: In hgview there is a nice doted relation highlighting ffa278c50818 as a new version of 8a79ae8b029e. this is not yet ported to graphlog. + +Their is **unstable** changeset in this history now. Mercurial will refuse to +share it with the outside: + + $ hg push other + pushing to $TESTTMP/other + searching for changes + abort: push includes an unstable changeset: 9ac5d0e790a2! + (use 'hg evolve' to get a stable history or --force to ignore warnings) + [255] + + + + +To resolve this unstable state, you need to rebase 9ac5d0e790a2 onto +ffa278c50818 the "hg evolve" command will make this for you. + +It has a --dry-run option to only suggest the next move. + + $ hg evolve --dry-run + move:[15] animals + atop:[14] bathroom stuff + hg rebase -Dr 9ac5d0e790a2 -d ffa278c50818 + +Let's do it + + $ hg rebase -Dr 9ac5d0e790a2 -d ffa278c50818 + merging shopping + +The old version of bathroom is hidden again. + + $ hg log -G + @ 437efbcaf700 (draft): animals + | + o ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + +We can push this evolution to remote + + $ hg push remote + pushing to $TESTTMP/remote + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 1 files (+1 heads) + +remote get a warning that current working directory is based on an obsolete changeset + + $ cd ../remote + $ hg pull local # we up again to trigger the warning. it was displayed during the push + pulling from $TESTTMP/local + searching for changes + no changes found + Working directory parent is obsolete + + $ hg up 437efbcaf700 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Relocating unstable change after prune +---------------------------------------------- + +The remote guy keep working + + $ sed -i'' -e 's/Spam/Spam Spam Spam Spam/g' shopping + $ hg commit -m "SPAM SPAM SPAM" + +I'm pulling its work locally. + + $ cd ../local + $ hg pull remote + pulling from $TESTTMP/remote + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg log -G + o ae45c0c3092a (draft): SPAM SPAM SPAM + | + @ 437efbcaf700 (draft): animals + | + o ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +In the mean time I noticed you can't buy animals in a super market and I prune the animal changeset: + + $ hg prune 437efbcaf700 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory now at ffa278c50818 + 1 new unstable changesets + + +The animals changeset is still displayed because the "SPAM SPAM SPAM" changeset +is neither dead or obsolete. My repository is in an unstable state again. + + $ hg log -G + o ae45c0c3092a (draft): SPAM SPAM SPAM + | + x 437efbcaf700 (draft): animals + | + @ ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + $ hg log -r 'unstable()' + ae45c0c3092a (draft): SPAM SPAM SPAM + + $ hg evolve --any + move:[17] SPAM SPAM SPAM + atop:[14] bathroom stuff + merging shopping + + $ hg log -G + @ d6717f710962 (draft): SPAM SPAM SPAM + | + o ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + +Handling Conflicting amend +---------------------------------------------- + +We can detect that multiple diverging//conflicting amend have been made. There +will be a "evol-merge" command to merge conflicting amend + +This command is not ready yet. diff -r f17a0f801e0b -r a9c27df23129 tests/test-uncommit.t --- a/tests/test-uncommit.t Tue Aug 21 12:43:21 2012 +0200 +++ b/tests/test-uncommit.t Fri Aug 24 11:53:55 2012 +0200 @@ -3,7 +3,6 @@ > hgext.rebase= > hgext.graphlog= > EOF - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH $ glog() { @@ -220,7 +219,7 @@ $ glog --hidden @ 4:e8db4aa611f6@bar(stable/draft) touncommit | - | o 3:5eb72dbe0cb4@bar(extinct/secret) touncommit + | x 3:5eb72dbe0cb4@bar(extinct/draft) touncommit |/ o 2:f63b90038565@default(stable/draft) merge |\ @@ -232,8 +231,8 @@ * touncommit-bm 4:e8db4aa611f6 touncommit-bm-inactive 4:e8db4aa611f6 unrelated 2:f63b90038565 - $ hg debugsuccessors - 5eb72dbe0cb4 e8db4aa611f6 + $ hg debugobsolete + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob) Test phase is preserved, no local changes @@ -242,7 +241,7 @@ Working directory parent is obsolete $ hg --config extensions.purge= purge $ hg uncommit -I 'set:added() and e' - 2 new conflictings changesets + 2 new conflicting changesets $ hg st --copies A e $ hg st --copies --change . @@ -263,11 +262,11 @@ R m R n $ glog --hidden - @ 5:c706fe2c12f8@bar(stable/secret) touncommit + @ 5:c706fe2c12f8@bar(stable/draft) touncommit | | o 4:e8db4aa611f6@bar(stable/draft) touncommit |/ - | o 3:5eb72dbe0cb4@bar(extinct/secret) touncommit + | x 3:5eb72dbe0cb4@bar(extinct/draft) touncommit |/ o 2:f63b90038565@default(stable/draft) merge |\ @@ -275,9 +274,9 @@ | o 0:07f494440405@default(stable/draft) adda - $ hg debugsuccessors - 5eb72dbe0cb4 c706fe2c12f8 - 5eb72dbe0cb4 e8db4aa611f6 + $ hg debugobsolete + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c706fe2c12f83ba5010cb60ea6af3bd1f0c2d6d3 0 {'date': '* *', 'user': 'test'} (glob) Test --all @@ -286,7 +285,7 @@ Working directory parent is obsolete $ hg --config extensions.purge= purge $ hg uncommit --all -X e - 1 new conflictings changesets + 1 new conflicting changesets $ hg st --copies M b M d @@ -308,21 +307,21 @@ $ hg st --copies --change . A e - $ hg debugsuccessors - 5eb72dbe0cb4 c4cbebac3751 - 5eb72dbe0cb4 c706fe2c12f8 - 5eb72dbe0cb4 e8db4aa611f6 + $ hg debugobsolete + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c706fe2c12f83ba5010cb60ea6af3bd1f0c2d6d3 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c4cbebac3751269bdf12d1466deabcc78521d272 0 {'date': '* *', 'user': 'test'} (glob) Display a warning if nothing left $ hg uncommit e new changeset is empty (use "hg kill ." to remove it) - $ hg debugsuccessors - 5eb72dbe0cb4 c4cbebac3751 - 5eb72dbe0cb4 c706fe2c12f8 - 5eb72dbe0cb4 e8db4aa611f6 - c4cbebac3751 4f1c269eab68 + $ hg debugobsolete + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c706fe2c12f83ba5010cb60ea6af3bd1f0c2d6d3 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c4cbebac3751269bdf12d1466deabcc78521d272 0 {'date': '* *', 'user': 'test'} (glob) + c4cbebac3751269bdf12d1466deabcc78521d272 4f1c269eab68720f54e88ce3c1dc02b2858b6b89 0 {'date': '* *', 'user': 'test'} (glob) Test instability warning @@ -333,4 +332,4 @@ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved [8] touncommit $ hg uncommit aa - 1 new unstables changesets + 1 new unstable changesets