This is an archive of the discontinued Mercurial Phabricator instance.

rebase: add --stop option to stop rebase at any point (issue5206)
ClosedPublic

Authored by khanchi97 on Jul 17 2018, 3:43 PM.

Details

Summary

Before this patch, during a rebase if you get a point where you can't
figure out what to do next then either you had to complete your series
or abandon all the work you have done during this rebase.

Now, with this feature you can stop at any point by keeping the rebased
csets and mark original csets as obsolete. And if you don't have evolution
extension enabled then you can use --keep option as an alternative which
will keep original csets too, instead of marking them obsolete.

Diff Detail

Repository
rHG Mercurial
Lint
Automatic diff as part of commit; lint not applicable.
Unit
Automatic diff as part of commit; unit tests not applicable.

Event Timeline

khanchi97 created this revision.Jul 17 2018, 3:43 PM
yuja added a subscriber: yuja.Jul 18 2018, 9:01 AM

+ def _stoprebase(self):
+ """stop the interrupted rebase"""
+ self.restorestatus()
+ if not self.stateobj.exists():
+ raise error.Abort(_("no interrupted rebase found"))
+ allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
+ if not (self.keepf or allowunstable):
+ raise error.Abort(_("can't remove original changesets with"
+ " unrebased descendants"),
+ hint=_('either enable evolve extension to allow unstable '
+ 'revisions or use --keep to keep original changesets'))
+
+ # update to the last rebased node if any
+ ctx = self.repo[None]
+ pars = [p.node() for p in ctx.parents()]
+ p1 = pars[0]
+ hg.updaterepo(self.repo, p1, overwrite=True)

Perhaps this differs from what a successful rebase would do. Unlike graft,
hg rebase goes back to the original working directory.

+ # either mark obsolete or keep rebased revisions
+ repo, ui, opts = self.repo, self.ui, self.opts
+ fm = ui.formatter('rebase', opts)
+ fm.startitem()
+ clearrebased(ui, repo, self.destmap, self.state, self.skipped,
+ collapsedas=None, keepf=self.keepf, fm=fm)
+ clearstatus(self.repo)
+ fm.end()
+ return 0

I doubt if this would work with --collapse. Can you try writing some tests?

I'm not pretty sure, but it might be possible to utilize _finishrebase()
to implement --stop, roughly by:

  1. restore status
  2. drop revisions that aren't rebased yet
  3. finish it

+ elif stop:
+ #todo: raise error for conflicting options

Yes!

In D3959#61670, @yuja wrote:

+ def _stoprebase(self):
+ """stop the interrupted rebase"""
+ self.restorestatus()
+ if not self.stateobj.exists():
+ raise error.Abort(_("no interrupted rebase found"))
+ allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
+ if not (self.keepf or allowunstable):
+ raise error.Abort(_("can't remove original changesets with"
+ " unrebased descendants"),
+ hint=_('either enable evolve extension to allow unstable '
+ 'revisions or use --keep to keep original changesets'))
+
+ # update to the last rebased node if any
+ ctx = self.repo[None]
+ pars = [p.node() for p in ctx.parents()]
+ p1 = pars[0]
+ hg.updaterepo(self.repo, p1, overwrite=True)

Perhaps this differs from what a successful rebase would do. Unlike graft,
hg rebase goes back to the original working directory.

right, will change it.

+ # either mark obsolete or keep rebased revisions
+ repo, ui, opts = self.repo, self.ui, self.opts
+ fm = ui.formatter('rebase', opts)
+ fm.startitem()
+ clearrebased(ui, repo, self.destmap, self.state, self.skipped,
+ collapsedas=None, keepf=self.keepf, fm=fm)
+ clearstatus(self.repo)
+ fm.end()
+ return 0

I doubt if this would work with --collapse. Can you try writing some tests?

What is the expected behaviour in case of --collapse? Should it collapse only those revisions which are rebased, then we may have to ask user to change commit message accordingly.

I'm not pretty sure, but it might be possible to utilize _finishrebase()
to implement --stop, roughly by:

  1. restore status
  2. drop revisions that aren't rebased yet
  3. finish it

yeah, trying to do it in _finishrebase()

+ elif stop:
+ #todo: raise error for conflicting options

Yes!

yuja added a comment.Jul 22 2018, 1:36 AM
> I doubt if this would work with --collapse. Can you try writing some tests?
What is the expected behaviour in case of --collapse? Should it collapse only those revisions which are rebased, then we may have to ask user to change commit message accordingly.

No idea. Perhaps we can revisit it later by making hg rebase --stop just
abort if it was a --collapse session.

khanchi97 edited the summary of this revision. (Show Details)Jul 22 2018, 12:55 PM
khanchi97 updated this revision to Diff 9642.
pulkit added a subscriber: pulkit.Jul 23 2018, 2:59 PM

Excellent! I like the feature and this has been on my TODO for long. Thanks for coding this up. You also need to take care of bookmark movements. Bookmark from changesets which have successfully rebased should be moved to their successors when we call --stop.

hgext/rebase.py
598

nit: 'cannot stop in --collapse session'

Also, if we cannot stop, we should not fallback to aborting.

607

nit: we don't mention about evolve or any out of tree extension from core. How about making --stop only work when obsmarkers are enabled and error out if they are not enabled?

676

Can we prevent having s and S evaluate to different options i.e. leave 'S' for now?

@pulkit I will send a follow up which will take care of bookmarks movement. Thanks for review!

hgext/rebase.py
598

Ah, right. We should only print that message and keep everything as it is.

607

okay, will remove mentioning of evolve.

How about making --stop only work when obsmarkers are enabled and error out if they are not enabled?

allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)

Don't ^ line check that obsmarker are enabled or not?

676

okay, will remove 'S'.

khanchi97 updated this revision to Diff 9654.Jul 24 2018, 2:04 PM
In D3959#61803, @pulkit wrote:

You also need to take care of bookmark movements. Bookmark from changesets which have successfully rebased should be moved to their successors when we call --stop.

@pulkit
I think bookmarks movement is already handled by clearrebased() function. I going to send a patch to reflect this behaviour. Correct me if I am missing something.

yuja added a comment.Aug 8 2018, 9:06 AM
  • def _finishrebase(self):

+ def _finishrebase(self, stoprebase=False):

repo, ui, opts = self.repo, self.ui, self.opts
fm = ui.formatter('rebase', opts)
fm.startitem()

+ if stoprebase:
+ self.restorestatus()
+ if self.collapsef:
+ ui.status(_("cannot stop in --collapse session\n"))
+ return

raise Abort because it's an error?

+
+ allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
+ if not (self.keepf or allowunstable):
+ raise error.Abort(_("can't remove original changesets with"
+ " unrebased descendants"),
+ hint=_('either enable obsmarkers to allow unstable '
+ 'revisions or use --keep to keep original '
+ 'changesets'))

It will be nice if we can move the pre-process out from _finishrebase(),
and get rid of the stoprebase flag somehow.

if self.collapsef:
    p1, p2, _base = defineparents(repo, min(self.state), self.destmap,
                                  self.state, self.skipped,

@@ -626,7 +640,10 @@

    newwd = self.originalwd
if newwd not in [c.rev() for c in repo[None].parents()]:
    ui.note(_("update back to initial working directory parent\n"))
  • hg.updaterepo(repo, newwd, overwrite=False)

+ if stoprebase:
+ hg.updaterepo(repo, newwd, overwrite=True)
+ else:
+ hg.updaterepo(repo, newwd, overwrite=False)

This implies an interrupted merge won't be cleared if no update is required.
(try hg up 3 && hg rebase -s 1 -d 5 in the first test you've added.)

Perhaps the rebase --stop flow can be processed as follows?

rbsrt = rebaseruntime(repo, ui)
rbsrt.restorestatus()
... check unsupported options ...
with repo.wlock(), repo.lock():
    if needupdate(repo, state):
        .. update to the current working revision with overwrite=True
        .. to clear interrupted merge
    rbsrt._finishrebase()
khanchi97 updated this revision to Diff 10286.Aug 10 2018, 7:15 AM
yuja added a comment.Aug 14 2018, 8:47 PM

Queued, many thanks.

+ elif stop:
+ rbsrt = rebaseruntime(repo, ui)
+ rbsrt.restorestatus()

Perhaps restorestatus() needs to be covered by the lock to prevent it from
being updated by another process. Can you send a follow up?

This revision was automatically updated to reflect the committed changes.