Mercurial > evolve
changeset 775:0961a7eb82c4
merge with stable
author | Pierre-Yves David <pierre-yves.david@fb.com> |
---|---|
date | Thu, 09 Jan 2014 21:46:23 -0800 |
parents | 966e2659e989 (diff) 03844e1fbfe6 (current diff) |
children | 7eaad1101242 |
files | |
diffstat | 5 files changed, 82 insertions(+), 518 deletions(-) [+] |
line wrap: on
line diff
--- a/README Thu Jan 09 21:37:52 2014 -0800 +++ b/README Thu Jan 09 21:46:23 2014 -0800 @@ -42,6 +42,15 @@ Changelog ========= +3.3.0 -- + +- add verbose hint about how to handle corner case by hand. + This should help people until evolve is able to to it itself. +- removed the qsync extension. The only user I knew about (logilab) is not + using it anymore. It not compatible with coming Mercurial version 2.9. +- add progress indicator for long evolve command +- report troubles creation from `hg import` + 3.2.0 -- 2013-11-15 - conform to the Mercurial custom of lowercase messages
--- a/hgext/evolve.py Thu Jan 09 21:37:52 2014 -0800 +++ b/hgext/evolve.py Thu Jan 09 21:46:23 2014 -0800 @@ -569,6 +569,7 @@ # XXX this could wrap transaction code # XXX (but this is a bit a layer violation) @eh.wrapcommand("commit") +@eh.wrapcommand("import") @eh.wrapcommand("push") @eh.wrapcommand("pull") @eh.wrapcommand("graft") @@ -925,31 +926,53 @@ ui.write_err(_('no troubled changesets\n')) return 1 + def progresscb(): + if allopt: + ui.progress('evolve', seen, unit='changesets', total=count) + seen = 1 + count = allopt and _counttroubled(ui, repo) or 1 + while tr is not None: - result = _evolveany(ui, repo, tr, dryrunopt) + progresscb() + result = _evolveany(ui, repo, tr, dryrunopt, progresscb=progresscb) + progresscb() + seen += 1 if not allopt: return result + progresscb() tr = _picknexttroubled(ui, repo, anyopt or allopt) + if allopt: + ui.progress('evolve', None) -def _evolveany(ui, repo, tr, dryrunopt): + +def _evolveany(ui, repo, tr, dryrunopt, progresscb): repo = repo.unfiltered() tr = repo[tr.rev()] cmdutil.bailifchanged(repo) troubles = tr.troubles() if 'unstable' in troubles: - return _solveunstable(ui, repo, tr, dryrunopt) + return _solveunstable(ui, repo, tr, dryrunopt, progresscb) elif 'bumped' in troubles: - return _solvebumped(ui, repo, tr, dryrunopt) + return _solvebumped(ui, repo, tr, dryrunopt, progresscb) elif 'divergent' in troubles: repo = repo.unfiltered() tr = repo[tr.rev()] - return _solvedivergent(ui, repo, tr, dryrunopt) + return _solvedivergent(ui, repo, tr, dryrunopt, progresscb) else: assert False # WHAT? unknown troubles -def _picknexttroubled(ui, repo, pickany=False): +def _counttroubled(ui, repo): + """Count the amount of troubled changesets""" + troubled = set() + troubled.update(getrevs(repo, 'unstable')) + troubled.update(getrevs(repo, 'bumped')) + troubled.update(getrevs(repo, 'divergent')) + return len(troubled) + +def _picknexttroubled(ui, repo, pickany=False, progresscb=None): """Pick a the next trouble changeset to solve""" + if progresscb: progresscb() tr = _stabilizableunstable(repo, repo['.']) if tr is None: wdp = repo['.'] @@ -988,7 +1011,7 @@ return child return None -def _solveunstable(ui, repo, orig, dryrun=False): +def _solveunstable(ui, repo, orig, dryrun=False, progresscb=None): """Stabilize a unstable changeset""" obs = orig.parents()[0] if not obs.obsolete(): @@ -1019,11 +1042,13 @@ repo.ui.status(_('atop:')) if not ui.quiet: displayer.show(target) + if progresscb: progresscb() todo = 'hg rebase -r %s -d %s\n' % (orig, target) if dryrun: repo.ui.write(todo) else: repo.ui.note(todo) + if progresscb: progresscb() lock = repo.lock() try: relocate(repo, orig, target) @@ -1035,7 +1060,7 @@ finally: lock.release() -def _solvebumped(ui, repo, bumped, dryrun=False): +def _solvebumped(ui, repo, bumped, dryrun=False, progresscb=None): """Stabilize a bumped changeset""" # For now we deny bumped merge if len(bumped.parents()) > 1: @@ -1061,6 +1086,7 @@ repo.ui.write('hg revert --all --rev %s;\n' % bumped) repo.ui.write('hg commit --msg "bumped update to %s"') return 0 + if progresscb: progresscb() wlock = repo.wlock() try: newid = tmpctx = None @@ -1137,17 +1163,40 @@ finally: wlock.release() -def _solvedivergent(ui, repo, divergent, dryrun=False): +def _solvedivergent(ui, repo, divergent, dryrun=False, progresscb=None): base, others = divergentdata(divergent) if len(others) > 1: - raise util.Abort("We do not handle split yet") + othersstr = "[%s]" % (','.join([str(i) for i in others])) + hint = ("changeset %d is divergent with a changeset that got splitted " + "| into multiple ones:\n[%s]\n" + "| This is not handled by automatic evolution yet\n" + "| You have to fallback to manual handling with commands as:\n" + "| - hg touch -D\n" + "| - hg prune\n" + "| \n" + "| You should contact your local evolution Guru for help.\n" + % (divergent, othersstr)) + raise util.Abort("We do not handle divergence with split yet", + hint='') other = others[0] if divergent.phase() <= phases.public: - raise util.Abort("We can't resolve this conflict from the public side") + raise util.Abort("We can't resolve this conflict from the public side", + hint="%s is public, try from %s" % (divergent, other)) if len(other.parents()) > 1: - raise util.Abort("divergent changeset can't be a merge (yet)") + raise util.Abort("divergent changeset can't be a merge (yet)", + hint="You have to fallback to solving this by hand...\n" + "| This probably mean to redo the merge and use " + "| `hg prune` to kill older version.") if other.p1() not in divergent.parents(): - raise util.Abort("parents are not common (not handled yet)") + raise util.Abort("parents are not common (not handled yet)", + hint="| %(d)s, %(o)s are not based on the same changeset." + "| With the current state of its implementation, " + "| evolve does not work in that case.\n" + "| rebase one of them next to the other and run " + "| this command again.\n" + "| - either: hg rebase -dest 'p1(%(d)s)' -r %(o)s" + "| - or: hg rebase -dest 'p1(%(d)s)' -r %(o)s" + % {'d': divergent, 'o': other}) displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) ui.status(_('merge:')) @@ -1177,6 +1226,7 @@ repo.ui.status(_('updating to "local" conflict\n')) hg.update(repo, divergent.rev()) repo.ui.note(_('merging divergent changeset\n')) + if progresscb: progresscb() stats = merge.update(repo, other.node(), branchmerge=True, @@ -1197,6 +1247,7 @@ /!\ * hg ci -m "same message as the amended changeset" => new cset Y /!\ * hg kill -n Y W Z """) + if progresscb: progresscb() tr = repo.transaction('stabilize-divergent') try: repo.dirstate.setparents(divergent.node(), node.nullid)
--- a/hgext/qsync.py Thu Jan 09 21:37:52 2014 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,266 +0,0 @@ -# Copyright 2011 Logilab SA <contact@logilab.fr> -"""synchronize patches queues and evolving changesets""" - -import re -from cStringIO import StringIO -import json - -from mercurial.i18n import _ -from mercurial import commands -from mercurial import patch -from mercurial import util -from mercurial.node import nullid, hex, short, bin -from mercurial import cmdutil -from mercurial import hg -from mercurial import scmutil -from mercurial import error -from mercurial import extensions -from mercurial import phases -from mercurial import obsolete - -### old compat code -############################# - -BRANCHNAME="qsubmit2" - -### new command -############################# -cmdtable = {} -command = cmdutil.command(cmdtable) - -@command('^qsync|sync', - [ - ('a', 'review-all', False, _('mark all touched patches ready for review (no editor)')), - ], - '') -def cmdsync(ui, repo, **opts): - '''Export draft changeset as mq patch in a mq patches repository commit. - - This command get all changesets in draft phase and create an mq changeset: - - * on a "qsubmit2" branch (based on the last changeset) - - * one patch per draft changeset - - * a series files listing all generated patch - - * qsubmitdata holding useful information - - It does use obsolete relation to update patches that already existing in the qsubmit2 branch. - - Already existing patch which became public, draft or got killed are remove from the mq repo. - - Patch name are generated using the summary line for changeset description. - - .. warning:: Series files is ordered topologically. So two series with - interleaved changeset will appear interleaved. - ''' - - review = 'edit' - if opts['review_all']: - review = 'all' - mqrepo = repo.mq.qrepo() - if mqrepo is None: - raise util.Abort('No patches repository') - - try: - parent = mqrepo[BRANCHNAME] - except error.RepoLookupError: - parent = initqsubmit(mqrepo) - store, data, touched = fillstore(repo, parent) - try: - if not touched: - raise util.Abort('Nothing changed') - files = ['qsubmitdata', 'series'] + touched - # mark some as ready for review - message = 'qsubmit commit\n\n' - review_list = [] - applied_list = [] - if review: - olddata = get_old_data(parent) - oldfiles = dict([(name, bin(ctxhex)) for ctxhex, name in olddata]) - - for patch_name in touched: - try: - store.getfile(patch_name) - review_list.append(patch_name) - except IOError: - oldnode = oldfiles[patch_name] - newnodes = obsolete.successorssets(repo, oldnode) - if newnodes: - newnodes = [n for n in newnodes if n and n[0] in repo] # remove killing - if not newnodes: - # changeset has been killed (eg. reject) - pass - else: - assert len(newnodes) == 1 # conflict!!! - newnode = newnodes[0] - assert len(newnode) == 1 # split unsupported for now - newnode = list(newnode)[0] - # XXX unmanaged case where a cs is obsoleted by an unavailable one - #if newnode.node() not in repo.changelog.nodemap: - # raise util.Abort('%s is obsoleted by an unknown node %s'% (oldnode, newnode)) - ctx = repo[newnode] - if ctx.phase() == phases.public: - # applied - applied_list.append(patch_name) - elif ctx.phase() == phases.secret: - # already exported changeset is now secret - repo.ui.warn("An already exported changeset is now secret!!!") - else: - # draft - assert False, "Should be exported" - - if review: - if applied_list: - message += '\n'.join('* applied %s' % x for x in applied_list) + '\n' - if review_list: - message += '\n'.join('* %s ready for review' % x for x in review_list) + '\n' - memctx = patch.makememctx(mqrepo, (parent.node(), nullid), - message, - None, - None, - parent.branch(), files, store, - editor=None) - if review == 'edit': - memctx._text = cmdutil.commitforceeditor(mqrepo, memctx, []) - mqrepo.savecommitmessage(memctx.description()) - n = memctx.commit() - finally: - store.close() - return 0 - - -def makename(ctx): - """create a patch name form a changeset""" - descsummary = ctx.description().splitlines()[0] - descsummary = re.sub(r'\s+', '_', descsummary) - descsummary = re.sub(r'\W+', '', descsummary) - if len(descsummary) > 45: - descsummary = descsummary[:42] + '.' - return '%s-%s.diff' % (ctx.branch().upper(), descsummary) - - -def get_old_data(mqctx): - """read qsubmit data to fetch previous export data - - get old data from the content of an mq commit""" - try: - old_data = mqctx['qsubmitdata'] - return json.loads(old_data.data()) - except error.LookupError: - return [] - -def get_current_data(repo): - """Return what would be exported if no previous data exists""" - data = [] - for ctx in repo.set('draft() - (obsolete() + merge())'): - name = makename(ctx) - data.append([ctx.hex(), makename(ctx)]) - merges = repo.revs('draft() and merge()') - if merges: - repo.ui.warn('ignoring %i merge\n' % len(merges)) - return data - - -def patchmq(repo, store, olddata, newdata): - """export the mq patches and return all useful data to be exported""" - finaldata = [] - touched = set() - currentdrafts = set(d[0] for d in newdata) - usednew = set() - usedold = set() - 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.successorssets(repo, oldnode) - if newnodes: - newnodes = [n for n in newnodes if n and n[0] in repo] # remove killing - if len(newnodes) > 1: - newnodes = [short(nodes[0]) for nodes in newnodes] - raise util.Abort('%s have more than one newer version: %s'% (oldname, newnodes)) - if newnodes: - # else, changeset have been killed - newnode = list(newnodes)[0][0] - ctx = repo[newnode] - if ctx.hex() != oldhex and ctx.phase(): - fp = StringIO() - cmdutil.export(repo, [ctx.rev()], fp=fp) - data = fp.getvalue() - store.setfile(oldname, data, (None, None)) - finaldata.append([ctx.hex(), oldname]) - usednew.add(ctx.hex()) - touched.add(oldname) - continue - if oldhex in currentdrafts: - # else changeset is now public or secret - finaldata.append([oldhex, oldname]) - usednew.add(ctx.hex()) - continue - touched.add(oldname) - - for newhex, newname in newdata: - if newhex in usednew: - continue - newnode = bin(newhex) - ctx = repo[newnode] - fp = StringIO() - cmdutil.export(repo, [ctx.rev()], fp=fp) - data = fp.getvalue() - store.setfile(newname, data, (None, None)) - finaldata.append([ctx.hex(), newname]) - touched.add(newname) - # sort by branchrev number - finaldata.sort(key=lambda x: sort_key(repo[x[0]])) - # sort touched too (ease review list) - stouched = [f[1] for f in finaldata if f[1] in touched] - stouched += [x for x in touched if x not in stouched] - return finaldata, stouched - -def sort_key(ctx): - """ctx sort key: (branch, rev)""" - return (ctx.branch(), ctx.rev()) - - -def fillstore(repo, basemqctx): - """fill store with patch data""" - olddata = get_old_data(basemqctx) - newdata = get_current_data(repo) - store = patch.filestore() - try: - data, touched = patchmq(repo, store, olddata, newdata) - # put all name in the series - series ='\n'.join(d[1] for d in data) + '\n' - store.setfile('series', series, (False, False)) - - # export data to ease futur work - store.setfile('qsubmitdata', json.dumps(data, indent=True), - (False, False)) - except: - store.close() - raise - return store, data, touched - - -def initqsubmit(mqrepo): - """create initial qsubmit branch""" - store = patch.filestore() - try: - files = set() - store.setfile('DO-NOT-EDIT-THIS-WORKING-COPY-BY-HAND', 'WE WARNED YOU!', (False, False)) - store.setfile('.hgignore', '^status$\n', (False, False)) - memctx = patch.makememctx(mqrepo, (nullid, nullid), - 'qsubmit init', - None, - None, - BRANCHNAME, ('.hgignore',), store, - editor=None) - mqrepo.savecommitmessage(memctx.description()) - n = memctx.commit() - finally: - store.close() - return mqrepo[n]
--- a/tests/test-obsolete.t Thu Jan 09 21:37:52 2014 -0800 +++ b/tests/test-obsolete.t Thu Jan 09 21:46:23 2014 -0800 @@ -678,3 +678,12 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: add c + +Check import reports new unstable changeset: + + $ hg up --hidden 2 + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory parent is obsolete! + $ hg export 9468a5f5d8b2 | hg import - + applying patch from stdin + 1 new unstable changesets
--- a/tests/test-qsync.t Thu Jan 09 21:37:52 2014 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,239 +0,0 @@ - $ cat >> $HGRCPATH <<EOF - > [defaults] - > amend=-d "0 0" - > [web] - > push_ssl = false - > allow_push = * - > [phases] - > publish = False - > [alias] - > qlog = log --template='{rev} - {node|short} {desc} ({phase})\n' - > mqlog = log --mq --template='{rev} - {desc}\n' - > [diff] - > git = 1 - > unified = 0 - > [extensions] - > hgext.rebase= - > hgext.graphlog= - > hgext.mq= - > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH - $ echo "qsync=$(echo $(dirname $TESTDIR))/hgext/qsync.py" >> $HGRCPATH - $ mkcommit() { - > echo "$1" > "$1" - > hg add "$1" - > hg ci -m "add $1" - > } - -basic sync - - $ hg init local - $ cd local - $ hg qinit -c - $ hg qci -m "initial commit" - $ mkcommit a - $ mkcommit b - $ hg qlog - 1 - 7c3bad9141dc add b (draft) - 0 - 1f0dee641bb7 add a (draft) - $ hg qsync -a - $ hg mqlog - 2 - qsubmit commit - - * DEFAULT-add_a.diff ready for review - * DEFAULT-add_b.diff ready for review - 1 - qsubmit init - 0 - initial commit - -basic sync II - - $ hg init local - $ cd local - $ hg qinit -c - $ hg qci -m "initial commit" - $ mkcommit a - $ mkcommit b - $ hg qlog - 1 - 7c3bad9141dc add b (draft) - 0 - 1f0dee641bb7 add a (draft) - $ hg qsync -a - $ hg mqlog - 2 - qsubmit commit - - * DEFAULT-add_a.diff ready for review - * DEFAULT-add_b.diff ready for review - 1 - qsubmit init - 0 - initial commit - - $ echo "b" >> b - $ hg amend - $ hg qsync -a - $ hg mqlog - 3 - qsubmit commit - - * DEFAULT-add_b.diff ready for review - 2 - qsubmit commit - - * DEFAULT-add_a.diff ready for review - * DEFAULT-add_b.diff ready for review - 1 - qsubmit init - 0 - initial commit - - $ hg up -r 0 - 0 files updated, 0 files merged, 1 files removed, 0 files unresolved - $ echo "a" >> a - $ hg amend - 1 new unstable changesets - $ hg graft -O 3 - grafting revision 3 - $ hg qsync -a - $ hg mqlog - 4 - qsubmit commit - - * DEFAULT-add_a.diff ready for review - * DEFAULT-add_b.diff ready for review - 3 - qsubmit commit - - * DEFAULT-add_b.diff ready for review - 2 - qsubmit commit - - * DEFAULT-add_a.diff ready for review - * DEFAULT-add_b.diff ready for review - 1 - qsubmit init - 0 - initial commit - -sync with published changeset - - $ hg init local - $ cd local - $ hg qinit -c - $ hg qci -m "initial commit" - $ mkcommit a - $ mkcommit b - $ hg qlog - 1 - 7c3bad9141dc add b (draft) - 0 - 1f0dee641bb7 add a (draft) - $ hg qsync -a - $ hg mqlog - 2 - qsubmit commit - - * DEFAULT-add_a.diff ready for review - * DEFAULT-add_b.diff ready for review - 1 - qsubmit init - 0 - initial commit - - $ hg phase -p 0 - $ hg qsync -a - $ hg mqlog - 3 - qsubmit commit - - * applied DEFAULT-add_a.diff - 2 - qsubmit commit - - * DEFAULT-add_a.diff ready for review - * DEFAULT-add_b.diff ready for review - 1 - qsubmit init - 0 - initial commit - - $ mkcommit c - $ mkcommit d - $ hg qsync -a - $ hg mqlog - 4 - qsubmit commit - - * DEFAULT-add_c.diff ready for review - * DEFAULT-add_d.diff ready for review - 3 - qsubmit commit - - * applied DEFAULT-add_a.diff - 2 - qsubmit commit - - * DEFAULT-add_a.diff ready for review - * DEFAULT-add_b.diff ready for review - 1 - qsubmit init - 0 - initial commit - - $ cd .. - $ hg qclone -U local local2 - $ cd local2 - $ hg qlog - 3 - 47d2a3944de8 add d (draft) - 2 - 4538525df7e2 add c (draft) - 1 - 7c3bad9141dc add b (draft) - 0 - 1f0dee641bb7 add a (public) - $ hg strip -n 1 --no-backup - $ hg up - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ hg up --mq 4 - 6 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ hg qseries - DEFAULT-add_b.diff - DEFAULT-add_c.diff - DEFAULT-add_d.diff - $ hg qpush - applying DEFAULT-add_b.diff - now at: DEFAULT-add_b.diff - $ hg qfinish -a - $ hg phase -p . - $ hg qci -m "applied DEFAULT-add_b.diff" - $ cd ../local - $ hg pull ../local2 - pulling from ../local2 - searching for changes - no changes found - $ hg pull --mq ../local2/.hg/patches - pulling from ../local2/.hg/patches - 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 qlog - 3 - 47d2a3944de8 add d (draft) - 2 - 4538525df7e2 add c (draft) - 1 - 7c3bad9141dc add b (public) - 0 - 1f0dee641bb7 add a (public) - $ hg mqlog -l 1 - 5 - applied DEFAULT-add_b.diff - $ hg status --mq --rev tip:-2 - M series - A DEFAULT-add_b.diff - $ hg qsync -a - $ hg status --mq --rev tip:-2 - M qsubmitdata - $ hg mqlog -l 1 - 6 - qsubmit commit - - * applied DEFAULT-add_b.diff - $ hg qsync -a - abort: Nothing changed - [255] - -mixed sync - - $ hg init local - $ cd local - $ hg qinit -c - $ mkcommit a - $ mkcommit b - $ hg qlog - 1 - 7c3bad9141dc add b (draft) - 0 - 1f0dee641bb7 add a (draft) - $ hg qsync -a - $ hg mqlog - 1 - qsubmit commit - - * DEFAULT-add_a.diff ready for review - * DEFAULT-add_b.diff ready for review - 0 - qsubmit init - $ hg phase -p 0 - $ echo "b" >> b - $ hg amend - $ hg qsync -a - $ hg mqlog -l 1 - 2 - qsubmit commit - - * applied DEFAULT-add_a.diff - * DEFAULT-add_b.diff ready for review -