Patchwork D11680: push: add option to abort on dirty working copy if parent is pushed

login
register
mail settings
Submitter phabricator
Date Oct. 15, 2021, 11:53 p.m.
Message ID <differential-rev-PHID-DREV-l3ldh7vkkoybvemb6m5f-req@mercurial-scm.org>
Download mbox | patch
Permalink /patch/50004/
State New
Headers show

Comments

phabricator - Oct. 15, 2021, 11:53 p.m.
martinvonz created this revision.
Herald added a reviewer: hg-reviewers.
Herald added a subscriber: mercurial-patches.

REVISION SUMMARY
  This patch adds a config option to make it an error to push the parent
  of the working copy when the working copy is dirty. We have had this
  feature enabled by default internally for a few years. I think most
  users have found it helpful.

REPOSITORY
  rHG Mercurial

BRANCH
  default

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

AFFECTED FILES
  mercurial/cmdutil.py
  mercurial/commands.py
  mercurial/configitems.py
  tests/test-completion.t
  tests/test-push.t

CHANGE DETAILS




To: martinvonz, #hg-reviewers
Cc: mercurial-patches, mercurial-devel

Patch

diff --git a/tests/test-push.t b/tests/test-push.t
--- a/tests/test-push.t
+++ b/tests/test-push.t
@@ -400,3 +400,73 @@ 
   searching for changes
   no changes found
   [1]
+
+
+Test `push.on-dirty-working-copy`
+---------------------------------
+
+  $ cat >> $HGRCPATH << EOF
+  > [extensions]
+  > drawdag=$TESTDIR/drawdag.py
+  > EOF
+  $ cd $TESTTMP
+  $ mkdir dirty-working-copy
+  $ cd dirty-working-copy
+  $ hg init source
+  $ cat >> source/.hg/hgrc << EOF
+  > [phases]
+  > publish=false
+  > EOF
+  $ hg clone source dest
+  updating to branch default
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd dest
+  $ hg debugdrawdag << 'EOF'
+  >   D
+  >   |
+  > F C
+  > | |
+  > E B
+  > |/
+  > A
+  > EOF
+  $ cat >> .hg/hgrc << EOF
+  > [push]
+  > on-dirty-working-copy=abort
+  > EOF
+# Push all commits just to make the output from further pushes consistently
+# "no changes found".
+  $ hg push -q -r 'head()'
+  $ hg co C
+  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo modified >> A
+# Cannot push the parent commit with a dirty working copy
+  $ hg push -q -r .
+  abort: won't push with dirty working directory
+  (maybe you meant to commit or amend the changes; if you want to push
+  anyway, use --allow-dirty, or set on-dirty-working-copy=ignore in
+  the [push] section of your ~/.hgrc)
+  [20]
+# Cannot push a descendant either
+  $ hg push -q -r D
+  abort: won't push with dirty working directory
+  (maybe you meant to commit or amend the changes; if you want to push
+  anyway, use --allow-dirty, or set on-dirty-working-copy=ignore in
+  the [push] section of your ~/.hgrc)
+  [20]
+# Typo in config value results in default behavior (which is to allow push)
+  $ hg push --config push.on-dirty-working-copy=abirt -q -r .
+  [1]
+# Can override config
+  $ hg push -q -r . --allow-dirty
+  [1]
+# Can push an ancestor
+  $ hg push -q -r B
+  [1]
+# Can push a sibling
+  $ hg push -q -r F
+  [1]
+# Can push descendant if the working copy parent is public
+  $ hg phase -p
+  $ hg push -q -r D
+  [1]
diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -362,7 +362,7 @@ 
   phase: public, draft, secret, force, rev
   pull: update, force, confirm, rev, bookmark, branch, ssh, remotecmd, insecure
   purge: abort-on-err, all, ignored, dirs, files, print, print0, confirm, include, exclude
-  push: force, rev, bookmark, all-bookmarks, branch, new-branch, pushvars, publish, ssh, remotecmd, insecure
+  push: force, rev, bookmark, all-bookmarks, branch, new-branch, pushvars, publish, allow-dirty, ssh, remotecmd, insecure
   recover: verify
   remove: after, force, subrepos, include, exclude, dry-run
   rename: forget, after, at-rev, force, include, exclude, dry-run
diff --git a/mercurial/configitems.py b/mercurial/configitems.py
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -1869,6 +1869,12 @@ 
     default=False,
 )
 coreconfigitem(
+    b'push',
+    b'on-dirty-working-copy',
+    default=b"ignore",
+    experimental=True,
+)
+coreconfigitem(
     b'rewrite',
     b'backup-bundle',
     default=True,
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -5633,6 +5633,15 @@ 
             False,
             _(b'push the changeset as public (EXPERIMENTAL)'),
         ),
+        (
+            b'',
+            b'allow-dirty',
+            False,
+            _(
+                b'allow pushing with a dirty working copy, overriding '
+                b'push.on-dirty-working-copy=abort (EXPERIMENTAL)'
+            ),
+        ),
     ]
     + remoteopts,
     _(b'[-f] [-r REV]... [-e CMD] [--remotecmd CMD] [DEST]...'),
@@ -5735,8 +5744,10 @@ 
 
         try:
             if revs:
-                revs = [repo[r].node() for r in logcmdutil.revrange(repo, revs)]
-                if not revs:
+                nodes = [
+                    repo[r].node() for r in logcmdutil.revrange(repo, revs)
+                ]
+                if not nodes:
                     raise error.InputError(
                         _(b"specified revisions evaluate to an empty set"),
                         hint=_(b"use different revision arguments"),
@@ -5746,8 +5757,8 @@ 
                 # to DAG heads to make discovery simpler.
                 expr = revsetlang.formatspec(b'heads(%r)', path.pushrev)
                 revs = scmutil.revrange(repo, [expr])
-                revs = [repo[rev].node() for rev in revs]
-                if not revs:
+                nodes = [repo[rev].node() for rev in revs]
+                if not nodes:
                     raise error.InputError(
                         _(
                             b'default push revset for path evaluates to an empty set'
@@ -5758,6 +5769,10 @@ 
                     _(b'no revisions specified to push'),
                     hint=_(b'did you mean "hg push -r ."?'),
                 )
+            else:
+                nodes = None
+            if nodes and not opts.get(b'allow_dirty'):
+                cmdutil.check_push_dirty_wc(repo, nodes)
 
             repo._subtoppath = dest
             try:
@@ -5780,7 +5795,7 @@ 
                 repo,
                 other,
                 opts.get(b'force'),
-                revs=revs,
+                revs=nodes,
                 newbranch=opts.get(b'new_branch'),
                 bookmarks=opts.get(b'bookmark', ()),
                 publish=opts.get(b'publish'),
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -1112,6 +1112,26 @@ 
         ctx.sub(s).bailifchanged(hint=hint)
 
 
+def check_push_dirty_wc(repo, nodes):
+    """Checks that `.` is not being pushed if the working copy is dirty."""
+    if repo.ui.config(b'push', b'on-dirty-working-copy') == b'abort':
+        # We assume that commits back to the public ancestors will be pushed.
+        if repo.revs('only(%ln, public()) & parents()', nodes):
+            # Covers both pending working directory state and merges.
+            try:
+                bailifchanged(repo)
+            except error.Abort:
+                hint = (
+                    b'maybe you meant to commit or amend the changes; if '
+                    b'you want to push\nanyway, use --allow-dirty, or set '
+                    b'on-dirty-working-copy=ignore in\nthe [push] section '
+                    b'of your ~/.hgrc'
+                )
+                raise error.StateError(
+                    b"won't push with dirty working directory", hint=_(hint)
+                )
+
+
 def logmessage(ui, opts):
     """get the log message according to -m and -l option"""