# HG changeset patch # User Mads Kiilerich # Date 1539529698 -7200 # Node ID 3c0d5016b2beca95c0ff13c8c5e56f39554bc3ef # Parent 808b762679cd5b54e5af2331c18a62b5f0b65ac9 graft: introduce --base option for using custom base revision while merging The graft command usually performs an internal merge of the current parent revision with the graft revision, using p1 of the grafted revision as base for the merge. As a trivial extension of this, we introduce the --base option to allow for using another base revision. This can be used as a building block for grafting and collapsing multiple changesets at once, or for grafting the resulting change from a merge as a single simple change. (This is kind of similar to backout --parent ... only different: this graft base must be an ancestor, but is usually *not* a parent.) This is probably an advanced use case, and we do thus not show it in the non-verbose help. diff -r 808b762679cd -r 3c0d5016b2be mercurial/commands.py --- a/mercurial/commands.py Thu Oct 18 12:31:06 2018 +0200 +++ b/mercurial/commands.py Sun Oct 14 17:08:18 2018 +0200 @@ -2249,6 +2249,8 @@ @command( 'graft', [('r', 'rev', [], _('revisions to graft'), _('REV')), + ('', 'base', '', + _('base revision when doing the graft merge (ADVANCED)'), _('REV')), ('c', 'continue', False, _('resume interrupted graft')), ('', 'stop', False, _('stop interrupted graft')), ('', 'abort', False, _('abort interrupted graft')), @@ -2294,6 +2296,35 @@ .. container:: verbose + The --base option exposes more of how graft internally uses merge with a + custom base revision. --base can be used to specify another ancestor than + the first and only parent. + + The command:: + + hg graft -r 345 --base 234 + + is thus pretty much the same as:: + + hg diff -r 234 -r 345 | hg import + + but using merge to resolve conflicts and track moved files. + + The result of a merge can thus be backported as a single commit by + specifying one of the merge parents as base, and thus effectively + grafting the changes from the other side. + + It is also possible to collapse multiple changesets and clean up history + by specifying another ancestor as base, much like rebase --collapse + --keep. + + The commit message can be tweaked after the fact using commit --amend . + + For using non-ancestors as the base to backout changes, see the backout + command and the hidden --parent option. + + .. container:: verbose + Examples: - copy a single change to the stable branch and edit its description:: @@ -2317,6 +2348,15 @@ hg log -r "sort(all(), date)" + - backport the result of a merge as a single commit:: + + hg graft -r 123 --base 123^ + + - land a feature branch as one changeset:: + + hg up -cr default + hg graft -r featureX --base "ancestor('featureX', 'default')" + See :hg:`help revisions` for more about specifying revisions. Returns 0 on successful completion. @@ -2332,6 +2372,9 @@ revs = list(revs) revs.extend(opts.get('rev')) + basectx = None + if opts.get('base'): + basectx = scmutil.revsingle(repo, opts['base'], None) # a dict of data to be stored in state file statedata = {} # list of new nodes created by ongoing graft @@ -2411,13 +2454,16 @@ revs = scmutil.revrange(repo, revs) skipped = set() - # check for merges - for rev in repo.revs('%ld and merge()', revs): - ui.warn(_('skipping ungraftable merge revision %d\n') % rev) - skipped.add(rev) + if basectx is None: + # check for merges + for rev in repo.revs('%ld and merge()', revs): + ui.warn(_('skipping ungraftable merge revision %d\n') % rev) + skipped.add(rev) revs = [r for r in revs if r not in skipped] if not revs: return -1 + if basectx is not None and len(revs) != 1: + raise error.Abort(_('only one revision allowed with --base ')) # Don't check in the --continue case, in effect retaining --force across # --continues. That's because without --force, any revisions we decided to @@ -2425,7 +2471,7 @@ # way to the graftstate. With --force, any revisions we would have otherwise # skipped would not have been filtered out, and if they hadn't been applied # already, they'd have been in the graftstate. - if not (cont or opts.get('force')): + if not (cont or opts.get('force')) and basectx is None: # check for ancestors of dest branch crev = repo['.'].rev() ancestors = repo.changelog.ancestors([crev], inclusive=True) @@ -2521,8 +2567,9 @@ if not cont: # perform the graft merge with p1(rev) as 'ancestor' overrides = {('ui', 'forcemerge'): opts.get('tool', '')} + base = ctx.p1() if basectx is None else basectx with ui.configoverride(overrides, 'graft'): - stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'graft']) + stats = mergemod.graft(repo, ctx, base, ['local', 'graft']) # report any conflicts if stats.unresolvedcount > 0: # write out state for --continue diff -r 808b762679cd -r 3c0d5016b2be tests/test-completion.t --- a/tests/test-completion.t Thu Oct 18 12:31:06 2018 +0200 +++ b/tests/test-completion.t Sun Oct 14 17:08:18 2018 +0200 @@ -308,7 +308,7 @@ export: bookmark, output, switch-parent, rev, text, git, binary, nodates, template files: rev, print0, include, exclude, template, subrepos forget: interactive, include, exclude, dry-run - graft: rev, continue, stop, abort, edit, log, no-commit, force, currentdate, currentuser, date, user, tool, dry-run + graft: rev, base, continue, stop, abort, edit, log, no-commit, force, currentdate, currentuser, date, user, tool, dry-run grep: print0, all, diff, text, follow, ignore-case, files-with-matches, line-number, rev, all-files, user, date, template, include, exclude heads: rev, topo, active, closed, style, template help: extension, command, keyword, system diff -r 808b762679cd -r 3c0d5016b2be tests/test-graft.t --- a/tests/test-graft.t Thu Oct 18 12:31:06 2018 +0200 +++ b/tests/test-graft.t Sun Oct 14 17:08:18 2018 +0200 @@ -25,7 +25,7 @@ $ echo b > e $ hg branch -q stable $ hg ci -m5 - $ hg merge -q default --tool internal:local + $ hg merge -q default --tool internal:local # for conflicts in e, choose 5 and ignore 4 $ hg branch -q default $ hg ci -m6 $ hg phase --public 3 @@ -46,8 +46,40 @@ | o test@0.public: 0 +Test --base for grafting the merge of 4 from the perspective of 5, thus only getting the change to d + + $ hg up -cqr 3 + $ hg graft -r 6 --base 5 + grafting 6:25a2b029d3ae "6" (tip) + merging e + $ hg st --change . + M d + + $ hg -q strip . --config extensions.strip= + +Test --base for collapsing changesets 2 and 3, thus getting both b and c + + $ hg up -cqr 0 + $ hg graft -r 3 --base 1 + grafting 3:4c60f11aa304 "3" + merging a and b to b + merging a and c to c + $ hg st --change . + A b + A c + R a + + $ hg -q strip . --config extensions.strip= + +Specifying child as --base revision fails safely (perhaps slightly confusing, but consistent) + + $ hg graft -r 2 --base 3 + grafting 2:5c095ad7e90f "2" + note: graft of 2:5c095ad7e90f created no changes to commit + Can't continue without starting: + $ hg -q up -cr tip $ hg rm -q e $ hg graft --continue abort: no graft in progress