Patchwork D6484: states: unified api for checking unfinished states(gsoc-19)

login
register
mail settings
Submitter phabricator
Date June 6, 2019, 3:26 a.m.
Message ID <differential-rev-PHID-DREV-up42oa5g6tg7hpmwt427-req@phab.mercurial-scm.org>
Download mbox | patch
Permalink /patch/40333/
State Superseded
Headers show

Comments

phabricator - June 6, 2019, 3:26 a.m.
taapas1128 created this revision.
Herald added a reviewer: durin42.
Herald added subscribers: mercurial-devel, Kwan, mjpieters.
Herald added a reviewer: martinvonz.
Herald added a reviewer: hg-reviewers.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6484

AFFECTED FILES
  hgext/absorb.py
  hgext/fix.py
  hgext/histedit.py
  hgext/phabricator.py
  hgext/rebase.py
  hgext/record.py
  hgext/shelve.py
  hgext/strip.py
  hgext/transplant.py
  mercurial/cmdutil.py
  mercurial/commands.py
  mercurial/statecheck.py
  tests/test-absorb-unfinished.t
  tests/test-bisect.t
  tests/test-conflict.t
  tests/test-fix.t
  tests/test-graft.t
  tests/test-histedit-arguments.t
  tests/test-histedit-edit.t
  tests/test-histedit-fold.t
  tests/test-histedit-no-change.t
  tests/test-qrecord.t
  tests/test-rebase-abort.t
  tests/test-rebase-conflicts.t
  tests/test-rebase-obsolete.t
  tests/test-rebase-pull.t
  tests/test-shelve.t
  tests/test-transplant.t

CHANGE DETAILS




To: taapas1128, durin42, martinvonz, #hg-reviewers
Cc: mjpieters, Kwan, mercurial-devel
phabricator - June 6, 2019, 4:18 a.m.
martinvonz requested changes to this revision.
martinvonz added a comment.
This revision now requires changes to proceed.


  Thanks for working on this!
  
  I think this patch does too much at once. At least the help message change should be moved to a separate patch. Ideally, the other changes would also be split up. Perhaps something like this:
  
  1. Move `unfinishedstates`, `checkunfinished`, and `clearunfinished` (without changing them) to the new module.
  2. Create and use the `statecheck` class, but only let it handle what's needed for `unfinishedstates` so far.
  3. Add the functionality from `STATES` to the new module and update the already existing users, but leave the cases that are handled by `STATES` unchanged for now.
  4. Switch existing users of `STATES` over to the new module (and delete `STATES`)
  5. Change messages to the new "To continue: ..." form. I'm not sure we want that change, or we'll probably at least want to make some, so this one is extra important to keep separate.
  
  You don't have to follow those steps exactly, of course. You know the code better, so maybe some of what I suggested doesn't make sense.

INLINE COMMENTS

> rebase.py:1951
> +    statecheckmod.unfinishedstates.append(statecheckmod.statecheck('rebase',
> +    'rebasestate', False, False, False))
>      cmdutil.afterresolvedstates.append(

Pass at least the boolean arguments as named arguments (`clearable=False` etc)

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6484

To: taapas1128, durin42, martinvonz, #hg-reviewers
Cc: mjpieters, Kwan, mercurial-devel
phabricator - June 6, 2019, 7:20 a.m.
taapas1128 added a comment.


  @martinvonz I have completed steps 1 to 5 as you stated in the sequence above in `statecheck.py`. For the purpose of showing that it works without flaw, I redirected the API calls to the new API for both STATES and unfinished state and removed those from `cmdutil.py`. All that remains is a minor bug that I am facing with `hg bisect`. I will clear that soon enough. Do you want me to send the API integration and tests as a separate patch and API as another?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6484

To: taapas1128, durin42, martinvonz, #hg-reviewers
Cc: mjpieters, Kwan, mercurial-devel
phabricator - June 6, 2019, 11:56 a.m.
pulkit added a comment.


  In https://phab.mercurial-scm.org/D6484#94131, @taapas1128 wrote:
  
  > @martinvonz I have completed steps 1 to 5 as you stated in the sequence above in `statecheck.py`. For the purpose of showing that it works without flaw, I redirected the API calls to the new API for both STATES and unfinished state and removed those from `cmdutil.py`. All that remains is a minor bug that I am facing with `hg bisect`. I will clear that soon enough. Do you want me to send the API integration and tests as a separate patch and API as another?
  
  
  I think @martinvonz wants to say that we should split the whole work into nice small commits. This will help us to review it better and understand the whole change much better. The 1 to 5 are like, one commit should do one of those things.
  
  I also agree with him, what do you think?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6484

To: taapas1128, durin42, martinvonz, #hg-reviewers
Cc: pulkit, mjpieters, Kwan, mercurial-devel
phabricator - June 7, 2019, 6:40 a.m.
taapas1128 added a comment.


  @pulkit okay I will split this into 5 commits.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6484

To: taapas1128, durin42, martinvonz, #hg-reviewers
Cc: pulkit, mjpieters, Kwan, mercurial-devel
phabricator - June 8, 2019, 9 p.m.
taapas1128 added a comment.


  @martinvonz have a look at this stack now . It is strictly according to your guidelines.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6484

To: taapas1128, durin42, martinvonz, #hg-reviewers
Cc: pulkit, mjpieters, Kwan, mercurial-devel
phabricator - June 8, 2019, 9:47 p.m.
martinvonz accepted this revision.
martinvonz added a comment.
This revision is now accepted and ready to land.


  Thanks for splitting this out! Sorry about being so particular about splitting up the patch. I think it's an important part of writing patches that others will review. Hopefully it wasn't just frustrating and you can also see some benefit with it. The benefit is mostly to reviewers and future code archeologists and not to the author, so it can feel like wasting time. Remember that more people will read the patches than the people writing them (which is just you).

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6484

To: taapas1128, durin42, martinvonz, #hg-reviewers
Cc: pulkit, mjpieters, Kwan, mercurial-devel
phabricator - June 9, 2019, 10:05 a.m.
taapas1128 added a comment.


  @martinvonz At first I didn't seem to realize why the patches need to be split that way but later I realized that I wasn't looking from the perspective of a reviewer but was just getting the job done as an author. Thanks for making me work this out :)

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6484

To: taapas1128, durin42, martinvonz, #hg-reviewers
Cc: pulkit, mjpieters, Kwan, mercurial-devel

Patch

diff --git a/tests/test-transplant.t b/tests/test-transplant.t
--- a/tests/test-transplant.t
+++ b/tests/test-transplant.t
@@ -479,7 +479,8 @@ 
   [255]
   $ hg transplant 1:3
   abort: transplant in progress
-  (use 'hg transplant --continue' or 'hg update' to abort)
+  (To continue:    hg transplant --continue
+  To abort:       hg transplant --abort)
   [255]
   $ echo fixed > baz
   $ hg transplant --continue
diff --git a/tests/test-shelve.t b/tests/test-shelve.t
--- a/tests/test-shelve.t
+++ b/tests/test-shelve.t
@@ -375,9 +375,8 @@ 
   # 
   # To mark files as resolved:  hg resolve --mark FILE
   
-  # To continue:    hg unshelve --continue
-  # To abort:       hg unshelve --abort
-  
+  To continue:    hg unshelve --continue
+  To abort:       hg unshelve --abort
 
 ensure that we have a merge with unresolved conflicts
 
@@ -426,8 +425,9 @@ 
   U a/a
 
   $ hg shelve
-  abort: unshelve already in progress
-  (use 'hg unshelve --continue' or 'hg unshelve --abort')
+  abort: unshelve in progress
+  (To continue:    hg unshelve --continue
+  To abort:       hg unshelve --abort)
   [255]
 
 abort the unshelve and be happy
@@ -484,8 +484,9 @@ 
   continue: hg unshelve --continue
 
   $ hg commit -m 'commit while unshelve in progress'
-  abort: unshelve already in progress
-  (use 'hg unshelve --continue' or 'hg unshelve --abort')
+  abort: unshelve in progress
+  (To continue:    hg unshelve --continue
+  To abort:       hg unshelve --abort)
   [255]
 
   $ hg graft --continue
diff --git a/tests/test-rebase-pull.t b/tests/test-rebase-pull.t
--- a/tests/test-rebase-pull.t
+++ b/tests/test-rebase-pull.t
@@ -93,7 +93,8 @@ 
   [1]
   $ hg pull --rebase
   abort: histedit in progress
-  (use 'hg histedit --continue' or 'hg histedit --abort')
+  (To continue:    hg histedit --continue
+  To abort:       hg histedit --abort)
   [255]
   $ hg histedit --abort --quiet
 
diff --git a/tests/test-rebase-obsolete.t b/tests/test-rebase-obsolete.t
--- a/tests/test-rebase-obsolete.t
+++ b/tests/test-rebase-obsolete.t
@@ -2054,7 +2054,8 @@ 
 
   $ hg rebase -s 3 -d 5
   abort: rebase in progress
-  (use 'hg rebase --continue' or 'hg rebase --abort')
+  (To continue:    hg rebase --continue
+  To abort:       hg rebase --abort)
   [255]
   $ hg rebase --stop --continue
   abort: cannot use --stop with --continue
diff --git a/tests/test-rebase-conflicts.t b/tests/test-rebase-conflicts.t
--- a/tests/test-rebase-conflicts.t
+++ b/tests/test-rebase-conflicts.t
@@ -80,9 +80,8 @@ 
   # 
   # To mark files as resolved:  hg resolve --mark FILE
   
-  # To continue:    hg rebase --continue
-  # To abort:       hg rebase --abort
-  
+  To continue:    hg rebase --continue
+  To abort:       hg rebase --abort
 
 Try to continue without solving the conflict:
 
diff --git a/tests/test-rebase-abort.t b/tests/test-rebase-abort.t
--- a/tests/test-rebase-abort.t
+++ b/tests/test-rebase-abort.t
@@ -319,7 +319,8 @@ 
   $ echo new > a
   $ hg up 1               # user gets an error saying to run hg rebase --abort
   abort: rebase in progress
-  (use 'hg rebase --continue' or 'hg rebase --abort')
+  (To continue:    hg rebase --continue
+  To abort:       hg rebase --abort)
   [255]
 
   $ cat a
@@ -389,20 +390,24 @@ 
 
   $ hg rebase -s 3 -d tip
   abort: rebase in progress
-  (use 'hg rebase --continue' or 'hg rebase --abort')
+  (To continue:    hg rebase --continue
+  To abort:       hg rebase --abort)
   [255]
   $ hg up .
   abort: rebase in progress
-  (use 'hg rebase --continue' or 'hg rebase --abort')
+  (To continue:    hg rebase --continue
+  To abort:       hg rebase --abort)
   [255]
   $ hg up -C .
   abort: rebase in progress
-  (use 'hg rebase --continue' or 'hg rebase --abort')
+  (To continue:    hg rebase --continue
+  To abort:       hg rebase --abort)
   [255]
 
   $ hg graft 3
   abort: rebase in progress
-  (use 'hg rebase --continue' or 'hg rebase --abort')
+  (To continue:    hg rebase --continue
+  To abort:       hg rebase --abort)
   [255]
 
   $ hg rebase --abort
diff --git a/tests/test-qrecord.t b/tests/test-qrecord.t
--- a/tests/test-qrecord.t
+++ b/tests/test-qrecord.t
@@ -459,5 +459,6 @@ 
   > n
   > EOF
   abort: histedit in progress
-  (use 'hg histedit --continue' or 'hg histedit --abort')
-  [255]
+  (To continue:    hg histedit --continue
+  To abort:       hg histedit --abort)
+  [255]
diff --git a/tests/test-histedit-no-change.t b/tests/test-histedit-no-change.t
--- a/tests/test-histedit-no-change.t
+++ b/tests/test-histedit-no-change.t
@@ -168,7 +168,8 @@ 
 abort editing session, after first forcibly updating away
   $ hg up 0
   abort: histedit in progress
-  (use 'hg histedit --continue' or 'hg histedit --abort')
+  (To continue:    hg histedit --continue
+  To abort:       hg histedit --abort)
   [255]
   $ mv .hg/histedit-state .hg/histedit-state-ignore
   $ hg up 0
diff --git a/tests/test-histedit-fold.t b/tests/test-histedit-fold.t
--- a/tests/test-histedit-fold.t
+++ b/tests/test-histedit-fold.t
@@ -306,9 +306,9 @@ 
   # 
   # To mark files as resolved:  hg resolve --mark FILE
   
-  # To continue:    hg histedit --continue
-  # To abort:       hg histedit --abort
-  
+  To continue:    hg histedit --continue
+  To abort:       hg histedit --abort
+  To stop:        hg histedit --stop
   $ hg resolve -l
   U file
   $ hg revert -r 'p1()' file
diff --git a/tests/test-histedit-edit.t b/tests/test-histedit-edit.t
--- a/tests/test-histedit-edit.t
+++ b/tests/test-histedit-edit.t
@@ -81,7 +81,8 @@ 
 try to update and get an error
   $ hg update tip
   abort: histedit in progress
-  (use 'hg histedit --continue' or 'hg histedit --abort')
+  (To continue:    hg histedit --continue
+  To abort:       hg histedit --abort)
   [255]
 
 edit the plan via the editor
@@ -135,7 +136,8 @@ 
   3+
   $ hg up 0
   abort: histedit in progress
-  (use 'hg histedit --continue' or 'hg histedit --abort')
+  (To continue:    hg histedit --continue
+  To abort:       hg histedit --abort)
   [255]
 
 Try to delete necessary commit
@@ -152,7 +154,8 @@ 
 
   $ hg --config extensions.mq= qnew please-fail
   abort: histedit in progress
-  (use 'hg histedit --continue' or 'hg histedit --abort')
+  (To continue:    hg histedit --continue
+  To abort:       hg histedit --abort)
   [255]
   $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
 
diff --git a/tests/test-histedit-arguments.t b/tests/test-histedit-arguments.t
--- a/tests/test-histedit-arguments.t
+++ b/tests/test-histedit-arguments.t
@@ -494,7 +494,8 @@ 
   continue: hg histedit --continue
   $ hg commit --amend -m 'reject this fold'
   abort: histedit in progress
-  (use 'hg histedit --continue' or 'hg histedit --abort')
+  (To continue:    hg histedit --continue
+  To abort:       hg histedit --abort)
   [255]
 
 With markers enabled, histedit does not get confused, and
diff --git a/tests/test-graft.t b/tests/test-graft.t
--- a/tests/test-graft.t
+++ b/tests/test-graft.t
@@ -279,15 +279,17 @@ 
   # 
   # To mark files as resolved:  hg resolve --mark FILE
   
-  # To continue:    hg graft --continue
-  # To abort:       hg graft --abort
-  
+  To continue:    hg graft --continue
+  To abort:       hg graft --abort
+  To stop:        hg graft --stop
 
 Commit while interrupted should fail:
 
   $ hg ci -m 'commit interrupted graft'
   abort: graft in progress
-  (use 'hg graft --continue' or 'hg graft --stop' to stop)
+  (To continue:    hg graft --continue
+  To abort:       hg graft --abort
+  To stop:        hg graft --stop)
   [255]
 
 Abort the graft and try committing:
diff --git a/tests/test-fix.t b/tests/test-fix.t
--- a/tests/test-fix.t
+++ b/tests/test-fix.t
@@ -831,7 +831,8 @@ 
 
   $ hg --config extensions.rebase= fix -r .
   abort: rebase in progress
-  (use 'hg rebase --continue' or 'hg rebase --abort')
+  (To continue:    hg rebase --continue
+  To abort:       hg rebase --abort)
   [255]
 
 When fixing a file that was renamed, we should diff against the source of the
diff --git a/tests/test-conflict.t b/tests/test-conflict.t
--- a/tests/test-conflict.t
+++ b/tests/test-conflict.t
@@ -57,9 +57,8 @@ 
   # 
   # To mark files as resolved:  hg resolve --mark FILE
   
-  # To continue:    hg commit
-  # To abort:       hg merge --abort
-  
+  To continue:    hg merge --continue
+  To abort:       hg merge --abort
   $ hg status -Tjson
   [
    {
diff --git a/tests/test-bisect.t b/tests/test-bisect.t
--- a/tests/test-bisect.t
+++ b/tests/test-bisect.t
@@ -185,12 +185,6 @@ 
   $ hg bisect -r
   $ hg bisect -b
   $ hg status -v
-  # The repository is in an unfinished *bisect* state.
-  
-  # To mark the changeset good:    hg bisect --good
-  # To mark the changeset bad:     hg bisect --bad
-  # To abort:                      hg bisect --reset
-  
   $ hg status -v --config commands.status.skipstates=bisect
   $ hg summary
   parent: 31:58c80a7c8a40 tip
diff --git a/tests/test-absorb-unfinished.t b/tests/test-absorb-unfinished.t
--- a/tests/test-absorb-unfinished.t
+++ b/tests/test-absorb-unfinished.t
@@ -25,6 +25,7 @@ 
 
   $ hg --config extensions.rebase= absorb
   abort: rebase in progress
-  (use 'hg rebase --continue' or 'hg rebase --abort')
+  (To continue:    hg rebase --continue
+  To abort:       hg rebase --abort)
   [255]
 
diff --git a/mercurial/statecheck.py b/mercurial/statecheck.py
new file mode 100644
--- /dev/null
+++ b/mercurial/statecheck.py
@@ -0,0 +1,131 @@ 
+from __future__ import absolute_import
+
+from .i18n import _
+
+from . import (
+    error,
+    util,
+)
+
+class statecheck(object):
+    """a utility class that will to deal with multistep operations
+       like graft, histedit, bisect, update etc and check whether such commands
+       are in an unfinished conditition of not and return appropriate message
+       and hint.
+       It also has the ability to register and determine the states of any new
+       multistep operation or multistep command extension.
+    """
+
+    def __init__(self, cmdname, fname, clearable, allowcommit, stopflag):
+        """cmdname is the name the command
+
+        fname is the file name in which data should be stored in .hg directory.
+        It is None for merge command.
+
+        clearable boolean determines whether or not interrupted states can be
+        cleared by running `hg update -C .`
+
+        allowcommit boolean decides whether commit is allowed during interrupted
+        state or not.
+
+        stopflag is a boolean that determines whether or not a command supports
+        --stop flag
+
+        """
+        self._cmdname = cmdname
+        self._fname = fname
+        self._clearable = clearable
+        self._allowcommit = allowcommit
+        self._stopflag = stopflag
+
+    def _hintmessage(self):
+        """returns the hint message corresponding to the command"""
+        if self._cmdname == 'bisect':
+            msg = _('To mark the changeset good:    hg bisect --good\n'
+                    'To mark the changeset bad:     hg bisect --bad\n'
+                    'To abort:                      hg bisect --reset\n')
+
+        elif self._cmdname == 'update':
+            msg = _("use 'hg update' to get a consistent checkout")
+
+        else:
+            msg = (_('To continue:    hg %s --continue\n'
+                    'To abort:       hg %s --abort') % (self._cmdname,
+                    self._cmdname))
+            if self._stopflag:
+                msg = msg + (_('\nTo stop:        hg %s --stop') %
+                     (self._cmdname))
+        return msg
+
+    def _statusmessage(self):
+        """returns the status message corresponding to the command"""
+        if self._cmdname == 'update':
+            hint = _('last update was interrupted')
+        else:
+            hint = _('%s in progress') % (self._cmdname)
+        return hint
+
+    def _mergepredicate(self, repo):
+        """determines is a merge is in progress or not"""
+        return len(repo[None].parents()) > 1
+
+
+unfinishedstates=[]
+unfinishedstates.append(statecheck('graft', 'graftstate', True, False, True))
+unfinishedstates.append(statecheck('update', 'updatestate', True, False, False))
+unfinishedstates.append(statecheck('merge', 'None', True, False, False))
+
+def checkunfinished(repo, commit=False):
+    '''Look for an unfinished multistep operation, like graft, and abort
+    if found. It's probably good to check this right before
+    bailifchanged().
+    '''
+    # Check for non-clearable states first, so things like rebase will take
+    # precedence over update.
+    for state in unfinishedstates:
+        if (state._clearable or (commit and state._allowcommit)
+             or state._cmdname=='merge'):
+            continue
+        if repo.vfs.exists(state._fname):
+            raise error.Abort(state._statusmessage(), hint=state._hintmessage())
+
+    for s in unfinishedstates:
+        if (not s._clearable or (commit and s._allowcommit)
+            or s._cmdname=='merge'):
+            continue
+        if repo.vfs.exists(s._fname):
+            raise error.Abort(s._statusmessage(), hint=s._hintmessage())
+
+def _getrepostate(repo):
+    # experimental config: commands.status.skipstates
+    skip = set(repo.ui.configlist('commands', 'status.skipstates'))
+    for state in unfinishedstates:
+        if state._cmdname in skip or state._clearable:
+            continue
+        if state._cmdname == 'merge' and state._mergepredicate(repo):
+            return (state._cmdname, state._mergepredicate(repo),
+                     state._hintmessage())
+        elif repo.vfs.exists(state._fname):
+            return (state._cmdname, repo.vfs.exists(state._fname),
+                        state._hintmessage())
+    for state in unfinishedstates:
+        if state._cmdname in skip or not state._clearable:
+            continue
+        if state._cmdname == 'merge' and state._mergepredicate(repo):
+            return (state._cmdname, state._mergepredicate(repo),
+                     state._hintmessage())
+        elif repo.vfs.exists(state._fname):
+            return (state._cmdname, repo.vfs.exists(state._fname),
+                        state._hintmessage())
+
+def clearunfinished(repo):
+    '''Check for unfinished operations (as above), and clear the ones
+    that are clearable.
+    '''
+    for state in unfinishedstates:
+        if not state._clearable and repo.vfs.exists(state._fname):
+            raise error.Abort(state._statusmessage(), hint=state._hintmessage())
+
+    for s in unfinishedstates:
+        if s._clearable and repo.vfs.exists(s._fname):
+            util.unlink(repo.vfs.join(s._fname))
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -59,6 +59,7 @@ 
     scmutil,
     server,
     state as statemod,
+    statecheck as statecheckmod,
     streamclone,
     tags as tagsmod,
     ui as uimod,
@@ -618,7 +619,7 @@ 
     if date:
         opts['date'] = dateutil.parsedate(date)
 
-    cmdutil.checkunfinished(repo)
+    statecheckmod.checkunfinished(repo)
     cmdutil.bailifchanged(repo)
     node = scmutil.revsingle(repo, rev).node()
 
@@ -847,7 +848,7 @@ 
         """common used update sequence"""
         if noupdate:
             return
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
         cmdutil.bailifchanged(repo)
         return hg.clean(repo, node, show_stats=show_stats)
 
@@ -1664,7 +1665,7 @@ 
         # Let --subrepos on the command line override config setting.
         ui.setconfig('ui', 'commitsubrepos', True, 'commit')
 
-    cmdutil.checkunfinished(repo, commit=True)
+    statecheckmod.checkunfinished(repo, commit=True)
 
     branch = repo[None].branch()
     bheads = repo.branchheads(branch)
@@ -1696,7 +1697,7 @@ 
         # Note: eventually this guard will be removed. Please do not expect
         # this behavior to remain.
         if not obsolete.isenabled(repo, obsolete.createmarkersopt):
-            cmdutil.checkunfinished(repo)
+            statecheckmod.checkunfinished(repo)
 
         node = cmdutil.amend(ui, repo, old, extra, pats, opts)
         if node == old.node():
@@ -2479,7 +2480,7 @@ 
     else:
         if not revs:
             raise error.Abort(_('no revisions specified'))
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
         cmdutil.bailifchanged(repo)
         revs = scmutil.revrange(repo, revs)
 
@@ -3481,7 +3482,7 @@ 
 
     with repo.wlock():
         if update:
-            cmdutil.checkunfinished(repo)
+            statecheckmod.checkunfinished(repo)
             if (exact or not opts.get('force')):
                 cmdutil.bailifchanged(repo)
 
@@ -6130,7 +6131,7 @@ 
         updatecheck = 'none'
 
     with repo.wlock():
-        cmdutil.clearunfinished(repo)
+        statecheckmod.clearunfinished(repo)
 
         if date:
             rev = cmdutil.finddate(ui, repo, date)
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -42,6 +42,7 @@ 
     rewriteutil,
     scmutil,
     smartset,
+    statecheck as statecheckmod,
     subrepoutil,
     templatekw,
     templater,
@@ -265,7 +266,7 @@ 
         will be left in place, so the user can continue working.
         """
 
-        checkunfinished(repo, commit=True)
+        statecheckmod.checkunfinished(repo, commit=True)
         wctx = repo[None]
         merge = len(wctx.parents()) > 1
         if merge:
@@ -618,63 +619,8 @@ 
 
     return _commentlines(msg)
 
-def _helpmessage(continuecmd, abortcmd):
-    msg = _('To continue:    %s\n'
-            'To abort:       %s') % (continuecmd, abortcmd)
-    return _commentlines(msg)
-
-def _rebasemsg():
-    return _helpmessage('hg rebase --continue', 'hg rebase --abort')
-
-def _histeditmsg():
-    return _helpmessage('hg histedit --continue', 'hg histedit --abort')
-
-def _unshelvemsg():
-    return _helpmessage('hg unshelve --continue', 'hg unshelve --abort')
-
-def _graftmsg():
-    return _helpmessage('hg graft --continue', 'hg graft --abort')
-
-def _mergemsg():
-    return _helpmessage('hg commit', 'hg merge --abort')
-
-def _bisectmsg():
-    msg = _('To mark the changeset good:    hg bisect --good\n'
-            'To mark the changeset bad:     hg bisect --bad\n'
-            'To abort:                      hg bisect --reset\n')
-    return _commentlines(msg)
-
-def fileexistspredicate(filename):
-    return lambda repo: repo.vfs.exists(filename)
-
-def _mergepredicate(repo):
-    return len(repo[None].parents()) > 1
-
-STATES = (
-    # (state, predicate to detect states, helpful message function)
-    ('histedit', fileexistspredicate('histedit-state'), _histeditmsg),
-    ('bisect', fileexistspredicate('bisect.state'), _bisectmsg),
-    ('graft', fileexistspredicate('graftstate'), _graftmsg),
-    ('unshelve', fileexistspredicate('shelvedstate'), _unshelvemsg),
-    ('rebase', fileexistspredicate('rebasestate'), _rebasemsg),
-    # The merge state is part of a list that will be iterated over.
-    # They need to be last because some of the other unfinished states may also
-    # be in a merge or update state (eg. rebase, histedit, graft, etc).
-    # We want those to have priority.
-    ('merge', _mergepredicate, _mergemsg),
-)
-
-def _getrepostate(repo):
-    # experimental config: commands.status.skipstates
-    skip = set(repo.ui.configlist('commands', 'status.skipstates'))
-    for state, statedetectionpredicate, msgfn in STATES:
-        if state in skip:
-            continue
-        if statedetectionpredicate(repo):
-            return (state, statedetectionpredicate, msgfn)
-
 def morestatus(repo, fm):
-    statetuple = _getrepostate(repo)
+    statetuple = statecheckmod._getrepostate(repo)
     label = 'status.morestatus'
     if statetuple:
         state, statedetectionpredicate, helpfulmsg = statetuple
@@ -684,8 +630,7 @@ 
         if conmsg:
             fm.plain('%s\n' % conmsg, label=label)
         if helpfulmsg:
-            helpmsg = helpfulmsg()
-            fm.plain('%s\n' % helpmsg, label=label)
+            fm.plain('%s\n' % helpfulmsg, label=label)
 
 def findpossible(cmd, table, strict=False):
     """
@@ -3289,46 +3234,6 @@ 
 #  - (desturl,   destbranch,   destpeer,   outgoing)
 summaryremotehooks = util.hooks()
 
-# A list of state files kept by multistep operations like graft.
-# Since graft cannot be aborted, it is considered 'clearable' by update.
-# note: bisect is intentionally excluded
-# (state file, clearable, allowcommit, error, hint)
-unfinishedstates = [
-    ('graftstate', True, False, _('graft in progress'),
-     _("use 'hg graft --continue' or 'hg graft --stop' to stop")),
-    ('updatestate', True, False, _('last update was interrupted'),
-     _("use 'hg update' to get a consistent checkout"))
-    ]
-
-def checkunfinished(repo, commit=False):
-    '''Look for an unfinished multistep operation, like graft, and abort
-    if found. It's probably good to check this right before
-    bailifchanged().
-    '''
-    # Check for non-clearable states first, so things like rebase will take
-    # precedence over update.
-    for f, clearable, allowcommit, msg, hint in unfinishedstates:
-        if clearable or (commit and allowcommit):
-            continue
-        if repo.vfs.exists(f):
-            raise error.Abort(msg, hint=hint)
-
-    for f, clearable, allowcommit, msg, hint in unfinishedstates:
-        if not clearable or (commit and allowcommit):
-            continue
-        if repo.vfs.exists(f):
-            raise error.Abort(msg, hint=hint)
-
-def clearunfinished(repo):
-    '''Check for unfinished operations (as above), and clear the ones
-    that are clearable.
-    '''
-    for f, clearable, allowcommit, msg, hint in unfinishedstates:
-        if not clearable and repo.vfs.exists(f):
-            raise error.Abort(msg, hint=hint)
-    for f, clearable, allowcommit, msg, hint in unfinishedstates:
-        if clearable and repo.vfs.exists(f):
-            util.unlink(repo.vfs.join(f))
 
 afterresolvedstates = [
     ('graftstate',
diff --git a/hgext/transplant.py b/hgext/transplant.py
--- a/hgext/transplant.py
+++ b/hgext/transplant.py
@@ -35,6 +35,7 @@ 
     revset,
     scmutil,
     smartset,
+    statecheck as statecheckmod,
     util,
     vfs as vfsmod,
 )
@@ -675,7 +676,7 @@ 
         if not tp.canresume():
             raise error.Abort(_('no transplant to continue'))
     else:
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
         cmdutil.bailifchanged(repo)
 
     sourcerepo = opts.get('source')
@@ -757,9 +758,7 @@ 
     return n and nodemod.hex(n) or ''
 
 def extsetup(ui):
-    cmdutil.unfinishedstates.append(
-        ['transplant/journal', True, False, _('transplant in progress'),
-         _("use 'hg transplant --continue' or 'hg update' to abort")])
-
+    statecheckmod.unfinishedstates.append(statecheckmod.statecheck('transplant',
+    'transplant/journal', True, False, False))
 # tell hggettext to extract docstrings from these functions:
 i18nfunctions = [revsettransplanted, kwtransplanted]
diff --git a/hgext/strip.py b/hgext/strip.py
--- a/hgext/strip.py
+++ b/hgext/strip.py
@@ -18,6 +18,7 @@ 
     registrar,
     repair,
     scmutil,
+    statecheck as statecheckmod,
     util,
 )
 nullid = nodemod.nullid
@@ -47,7 +48,7 @@ 
     return inclsubs
 
 def checklocalchanges(repo, force=False, excsuffix=''):
-    cmdutil.checkunfinished(repo)
+    statecheckmod.checkunfinished(repo)
     s = repo.status()
     if not force:
         if s.modified or s.added or s.removed or s.deleted:
diff --git a/hgext/shelve.py b/hgext/shelve.py
--- a/hgext/shelve.py
+++ b/hgext/shelve.py
@@ -48,6 +48,7 @@ 
     registrar,
     repair,
     scmutil,
+    statecheck as statecheckmod,
     templatefilters,
     util,
     vfs as vfsmod,
@@ -445,7 +446,7 @@ 
 def createcmd(ui, repo, pats, opts):
     """subcommand that creates a new shelve"""
     with repo.wlock():
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
         return _docreatecmd(ui, repo, pats, opts)
 
 def _docreatecmd(ui, repo, pats, opts):
@@ -935,7 +936,7 @@ 
     abortf = opts.get('abort')
     continuef = opts.get('continue')
     if not abortf and not continuef:
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
     shelved = list(shelved)
     if opts.get("name"):
         shelved.append(opts["name"])
@@ -1139,9 +1140,7 @@ 
         return createcmd(ui, repo, pats, opts)
 
 def extsetup(ui):
-    cmdutil.unfinishedstates.append(
-        [shelvedstate._filename, False, False,
-         _('unshelve already in progress'),
-         _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
+    statecheckmod.unfinishedstates.append(statecheckmod.statecheck('unshelve',
+    shelvedstate._filename, False, False, False))
     cmdutil.afterresolvedstates.append(
         [shelvedstate._filename, _('hg unshelve --continue')])
diff --git a/hgext/record.py b/hgext/record.py
--- a/hgext/record.py
+++ b/hgext/record.py
@@ -19,6 +19,7 @@ 
     error,
     extensions,
     registrar,
+    statecheck as statecheckmod
 )
 
 cmdtable = {}
@@ -119,7 +120,7 @@ 
 
     overrides = {('experimental', 'crecord'): False}
     with ui.configoverride(overrides, 'record'):
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
         cmdutil.dorecord(ui, repo, committomq, cmdsuggest, False,
                          cmdutil.recordfilter, *pats, **opts)
 
diff --git a/hgext/rebase.py b/hgext/rebase.py
--- a/hgext/rebase.py
+++ b/hgext/rebase.py
@@ -48,6 +48,7 @@ 
     scmutil,
     smartset,
     state as statemod,
+    statecheck as statecheckmod,
     util,
 )
 
@@ -1064,7 +1065,7 @@ 
         raise error.Abort(_('cannot specify both a revision and a source'))
 
     if not inmemory:
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
         cmdutil.bailifchanged(repo)
 
     if ui.configbool('commands', 'rebase.requiredest') and not destf:
@@ -1144,7 +1145,7 @@ 
            rebase_rebasing_wcp=rebasingwcp)
     if inmemory and rebasingwcp:
         # Check these since we did not before.
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
         cmdutil.bailifchanged(repo)
 
     if not destf:
@@ -1818,8 +1819,7 @@ 
                 del opts[r'update']
                 ui.debug('--update and --rebase are not compatible, ignoring '
                          'the update flag\n')
-
-            cmdutil.checkunfinished(repo)
+            statecheckmod.checkunfinished(repo)
             cmdutil.bailifchanged(repo, hint=_('cannot pull with rebase: '
                 'please commit or shelve your changes first'))
 
@@ -1947,8 +1947,7 @@ 
     entry[1].append(('t', 'tool', '',
                      _("specify merge tool for rebase")))
     cmdutil.summaryhooks.add('rebase', summaryhook)
-    cmdutil.unfinishedstates.append(
-        ['rebasestate', False, False, _('rebase in progress'),
-         _("use 'hg rebase --continue' or 'hg rebase --abort'")])
+    statecheckmod.unfinishedstates.append(statecheckmod.statecheck('rebase',
+    'rebasestate', False, False, False))
     cmdutil.afterresolvedstates.append(
         ['rebasestate', _('hg rebase --continue')])
diff --git a/hgext/phabricator.py b/hgext/phabricator.py
--- a/hgext/phabricator.py
+++ b/hgext/phabricator.py
@@ -64,6 +64,7 @@ 
     registrar,
     scmutil,
     smartset,
+    statecheck as statecheckmod,
     tags,
     templatefilters,
     templateutil,
@@ -526,7 +527,7 @@ 
     if not revs:
         raise error.Abort(_(b'phabsend requires at least one changeset'))
     if opts.get(b'amend'):
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
 
     # {newnode: (oldnode, olddiff, olddrev}
     oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
diff --git a/hgext/histedit.py b/hgext/histedit.py
--- a/hgext/histedit.py
+++ b/hgext/histedit.py
@@ -226,6 +226,7 @@ 
     repair,
     scmutil,
     state as statemod,
+    statecheck as statecheckmod,
     util,
 )
 from mercurial.utils import (
@@ -1508,7 +1509,7 @@ 
     try:
         keep = opts.get('keep')
         revs = opts.get('rev', [])[:]
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
         cmdutil.bailifchanged(repo)
 
         if os.path.exists(os.path.join(repo.path, 'histedit-state')):
@@ -1940,7 +1941,7 @@ 
     rules = opts.get('commands', '')
     force = opts.get('force')
 
-    cmdutil.checkunfinished(repo)
+    statecheckmod.checkunfinished(repo)
     cmdutil.bailifchanged(repo)
 
     topmost = repo.dirstate.p1()
@@ -2313,8 +2314,7 @@ 
 
 def extsetup(ui):
     cmdutil.summaryhooks.add('histedit', summaryhook)
-    cmdutil.unfinishedstates.append(
-        ['histedit-state', False, True, _('histedit in progress'),
-         _("use 'hg histedit --continue' or 'hg histedit --abort'")])
+    statecheckmod.unfinishedstates.append(statecheckmod.statecheck('histedit',
+    'histedit-state', False, True, False))
     cmdutil.afterresolvedstates.append(
         ['histedit-state', _('hg histedit --continue')])
diff --git a/hgext/fix.py b/hgext/fix.py
--- a/hgext/fix.py
+++ b/hgext/fix.py
@@ -132,6 +132,7 @@ 
     pycompat,
     registrar,
     scmutil,
+    statecheck as statecheckmod,
     util,
     worker,
 )
@@ -349,7 +350,7 @@ 
     for rev in revs:
         checkfixablectx(ui, repo, repo[rev])
     if revs:
-        cmdutil.checkunfinished(repo)
+        statecheckmod.checkunfinished(repo)
         checknodescendants(repo, revs)
     if opts.get('working_dir'):
         revs.add(wdirrev)
diff --git a/hgext/absorb.py b/hgext/absorb.py
--- a/hgext/absorb.py
+++ b/hgext/absorb.py
@@ -51,6 +51,7 @@ 
     pycompat,
     registrar,
     scmutil,
+    statecheck as statecheckmod,
     util,
 )
 from mercurial.utils import (
@@ -1008,7 +1009,7 @@ 
 
     with repo.wlock(), repo.lock():
         if not opts['dry_run']:
-            cmdutil.checkunfinished(repo)
+            statecheckmod.checkunfinished(repo)
 
         state = absorb(ui, repo, pats=pats, opts=opts)
         if sum(s[0] for s in state.chunkstats.values()) == 0: