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'commands', b'push.check-dirty') == 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'push-dirty=allow in the\n[commands] section of your ' + b'~/.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""" diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -5639,6 +5639,15 @@ False, _(b'push the changeset as public (EXPERIMENTAL)'), ), + ( + b'', + b'allow-dirty', + False, + _( + b'allow pushing with a dirty working copy, overriding ' + b'commands.push.check-dirty=abort (EXPERIMENTAL)' + ), + ), ] + remoteopts, _(b'[-f] [-r REV]... [-e CMD] [--remotecmd CMD] [DEST]...'), @@ -5741,8 +5750,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"), @@ -5752,8 +5763,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' @@ -5764,6 +5775,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: @@ -5786,7 +5801,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/configitems.py b/mercurial/configitems.py --- a/mercurial/configitems.py +++ b/mercurial/configitems.py @@ -1864,6 +1864,12 @@ default=False, ) coreconfigitem( + b'commands', + b'push.check-dirty', + default=b"allow", + experimental=True, +) +coreconfigitem( b'rewrite', b'backup-bundle', default=True, 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/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 `commands.push.check-dirty` +--------------------------------- + + $ 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 + > [commands] + > push.check-dirty=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 push-dirty=allow in the + [commands] 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 push-dirty=allow in the + [commands] section of your ~/.hgrc) + [20] +# Typo in config value results in default behavior (which is to allow push) + $ hg push --config commands.push.check-dirty=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]