Submitter | Mads Kiilerich |
---|---|
Date | Oct. 14, 2018, 3:15 p.m. |
Message ID | <258029c642d97ef66339.1539530157@xps> |
Download | mbox | patch |
Permalink | /patch/36003/ |
State | Accepted |
Headers | show |
Comments
> On Oct 14, 2018, at 11:15, Mads Kiilerich <mads@kiilerich.com> wrote: > > # HG changeset patch > # User Mads Kiilerich <mads@kiilerich.com> > # Date 1539529698 -7200 > # Sun Oct 14 17:08:18 2018 +0200 > # Node ID 258029c642d97ef663396476c63ce34dbef89b13 > # Parent 38ac525b44c93fcadb3680d4ded56f1e5a0029b2 > graft: introduce --base option for using custom base revision while merging queued At the sprint you had mentioned maybe being able to use this to help backing out a merge - if you know the incantation for that maybe send that as a follow-up for the verbose help? > > 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 --git a/mercurial/commands.py b/mercurial/commands.py > --- a/mercurial/commands.py > +++ b/mercurial/commands.py > @@ -2223,6 +2223,8 @@ def forget(ui, repo, *pats, **opts): > @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')), > @@ -2267,6 +2269,35 @@ def graft(ui, repo, *revs, **opts): > > .. 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:: > @@ -2290,6 +2321,15 @@ def graft(ui, repo, *revs, **opts): > > 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. > @@ -2305,6 +2345,9 @@ def _dograft(ui, repo, *revs, **opts): > > 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 > @@ -2384,13 +2427,16 @@ def _dograft(ui, repo, *revs, **opts): > 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 > @@ -2398,7 +2444,7 @@ def _dograft(ui, repo, *revs, **opts): > # 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) > @@ -2494,8 +2540,9 @@ def _dograft(ui, repo, *revs, **opts): > 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 --git a/tests/test-completion.t b/tests/test-completion.t > --- a/tests/test-completion.t > +++ b/tests/test-completion.t > @@ -318,7 +318,7 @@ Show all commands + options > debugwireargs: three, four, five, ssh, remotecmd, insecure > debugwireproto: localssh, peer, noreadstderr, nologhandshake, ssh, remotecmd, insecure > files: rev, print0, include, exclude, template, subrepos > - 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 --git a/tests/test-graft.t b/tests/test-graft.t > --- a/tests/test-graft.t > +++ b/tests/test-graft.t > @@ -25,7 +25,7 @@ Create a repo with some stuff in it: > $ 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 @@ Create a repo with some stuff in it: > | > 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 > _______________________________________________ > Mercurial-devel mailing list > Mercurial-devel@mercurial-scm.org > https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
On 10/23/18 4:59 PM, Augie Fackler wrote: >> On Oct 14, 2018, at 11:15, Mads Kiilerich <mads@kiilerich.com> wrote: >> >> # HG changeset patch >> # User Mads Kiilerich <mads@kiilerich.com> >> # Date 1539529698 -7200 >> # Sun Oct 14 17:08:18 2018 +0200 >> # Node ID 258029c642d97ef663396476c63ce34dbef89b13 >> # Parent 38ac525b44c93fcadb3680d4ded56f1e5a0029b2 >> graft: introduce --base option for using custom base revision while merging > queued Thanks. > At the sprint you had mentioned maybe being able to use this to help backing out a merge - if you know the incantation for that maybe send that as a follow-up for the verbose help? IIRC, graft has other safety mechanisms that prevent using a descendant as base. The primary use of this is to "graft merges". Backout of merges should be possible with existing backout functionality. The change help hinted in that direction: >> + >> + For using non-ancestors as the base to backout changes, see the backout >> + command and the hidden --parent option. (But ok, backout could use more helpful help too.) /Mads
Patch
diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -2223,6 +2223,8 @@ def forget(ui, repo, *pats, **opts): @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')), @@ -2267,6 +2269,35 @@ def graft(ui, repo, *revs, **opts): .. 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:: @@ -2290,6 +2321,15 @@ def graft(ui, repo, *revs, **opts): 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. @@ -2305,6 +2345,9 @@ def _dograft(ui, repo, *revs, **opts): 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 @@ -2384,13 +2427,16 @@ def _dograft(ui, repo, *revs, **opts): 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 @@ -2398,7 +2444,7 @@ def _dograft(ui, repo, *revs, **opts): # 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) @@ -2494,8 +2540,9 @@ def _dograft(ui, repo, *revs, **opts): 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 --git a/tests/test-completion.t b/tests/test-completion.t --- a/tests/test-completion.t +++ b/tests/test-completion.t @@ -318,7 +318,7 @@ Show all commands + options debugwireargs: three, four, five, ssh, remotecmd, insecure debugwireproto: localssh, peer, noreadstderr, nologhandshake, ssh, remotecmd, insecure files: rev, print0, include, exclude, template, subrepos - 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 --git a/tests/test-graft.t b/tests/test-graft.t --- a/tests/test-graft.t +++ b/tests/test-graft.t @@ -25,7 +25,7 @@ Create a repo with some stuff in it: $ 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 @@ Create a repo with some stuff in it: | 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