[PATCH 0/2] Support updating working trees when pushing into non-bare repos

STALE3737d

15 messages, 4 authors, 2016-06-15 · open the first message on its own page

[PATCH 0/2] Support updating working trees when pushing into non-bare repos

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:02:51

A few years ago, this developer was convinced that it was a bad idea to
auto-update working directories when pushing into the current branch, and
that an excellent way to prove this was to implement that feature. To his
surprise, it turned out to be the one thing he misses most in upstream Git.

So here goes: this patch series adds support for two new
receive.denyCurrentBranch settings: one to update the working directory
(which must be clean, i.e. there must not be any uncommitted changes) when
pushing into the current branch, the other setting detaches the HEAD
instead.

The scenario in which in particular the 'updateInstead' setting became a
boon in this developer's daily work is a multi-laptop one, where working
directories need to be updated between computers without a hassle.

Johannes Schindelin (2):
  Add a few more values for receive.denyCurrentBranch
  Let deny.currentBranch=updateInstead ignore submodules

 Documentation/config.txt |  5 +++++
 builtin/receive-pack.c   | 58 ++++++++++++++++++++++++++++++++++++++++++++++--
 t/t5516-fetch-push.sh    | 36 ++++++++++++++++++++++++++++++
 3 files changed, 97 insertions(+), 2 deletions(-)

-- 
2.0.0.rc3.9669.g840d1f9

[PATCH 1/2] Add a few more values for receive.denyCurrentBranch

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:02:51

Under certain circumstances, it makes a *lot* of sense to allow pushing
into the current branch. For example, when two machines with different
Operating Systems are required for testing, it makes much more sense to
synchronize between working directories than having to go via a third
server.

Under different circumstances, the working directory needs to be left
untouched, for example when a bunch of VMs need to be shut down to save
RAM and one needs to push everything out into the host's non-bare
repositories quickly.

This change supports both workflows by offering two new values for the
denyCurrentBranch setting:

'updateInstead':
	Update the working tree accordingly, but refuse to do so if there
	are any uncommitted changes.

'detachInstead':
	Detach the HEAD, thereby keeping currently checked-out revision,
	index and working directory unchanged.

Signed-off-by: Johannes Schindelin <redacted>
---
 Documentation/config.txt |  5 +++++
 builtin/receive-pack.c   | 58 ++++++++++++++++++++++++++++++++++++++++++++++--
 t/t5516-fetch-push.sh    | 36 ++++++++++++++++++++++++++++++
 3 files changed, 97 insertions(+), 2 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index e8dd76d..6fc0d6d 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2129,6 +2129,11 @@ receive.denyCurrentBranch::
 	print a warning of such a push to stderr, but allow the push to
 	proceed. If set to false or "ignore", allow such pushes with no
 	message. Defaults to "refuse".
++
+There are two more options: "updateInstead" which will update the working
+directory (must be clean) if pushing into the current branch, and
+"detachInstead" which will leave the working directory untouched, detaching
+the HEAD so it does not need to change.
 
 receive.denyNonFastForwards::
 	If set to true, git-receive-pack will deny a ref update which is
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 32fc540..be4172f 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -26,7 +26,9 @@ enum deny_action {
 	DENY_UNCONFIGURED,
 	DENY_IGNORE,
 	DENY_WARN,
-	DENY_REFUSE
+	DENY_REFUSE,
+	DENY_UPDATE_INSTEAD,
+	DENY_DETACH_INSTEAD,
 };
 
 static int deny_deletes;
@@ -120,7 +122,12 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 	}
 
 	if (!strcmp(var, "receive.denycurrentbranch")) {
-		deny_current_branch = parse_deny_action(var, value);
+		if (value && !strcasecmp(value, "updateinstead"))
+			deny_current_branch = DENY_UPDATE_INSTEAD;
+		else if (value && !strcasecmp(value, "detachinstead"))
+			deny_current_branch = DENY_DETACH_INSTEAD;
+		else
+			deny_current_branch = parse_deny_action(var, value);
 		return 0;
 	}
 
@@ -730,6 +737,44 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 	return 0;
 }
 
+static void merge_worktree(unsigned char *sha1)
+{
+	const char *update_refresh[] = {
+		"update-index", "--refresh", NULL
+	};
+	const char *read_tree[] = {
+		"read-tree", "-u", "-m", sha1_to_hex(sha1), NULL
+	};
+	struct child_process child;
+	struct strbuf git_env = STRBUF_INIT;
+	const char *env[2];
+
+	if (is_bare_repository())
+		die ("denyCurrentBranch = updateInstead needs a worktree");
+
+	strbuf_addf(&git_env, "GIT_DIR=%s", absolute_path(get_git_dir()));
+	env[0] = git_env.buf;
+	env[1] = NULL;
+
+	memset(&child, 0, sizeof(child));
+	child.argv = update_refresh;
+	child.env = env;
+	child.dir = git_work_tree_cfg ? git_work_tree_cfg : "..";
+	child.stdout_to_stderr = 1;
+	child.git_cmd = 1;
+	if (run_command(&child))
+		die ("Could not refresh the index");
+
+	child.argv = read_tree;
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.stdout_to_stderr = 0;
+	if (run_command(&child))
+		die ("Could not merge working tree with new HEAD.  Good luck.");
+
+	strbuf_release(&git_env);
+}
+
 static const char *update(struct command *cmd, struct shallow_info *si)
 {
 	const char *name = cmd->ref_name;
@@ -760,6 +805,13 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 			if (deny_current_branch == DENY_UNCONFIGURED)
 				refuse_unconfigured_deny();
 			return "branch is currently checked out";
+		case DENY_UPDATE_INSTEAD:
+			merge_worktree(new_sha1);
+			break;
+		case DENY_DETACH_INSTEAD:
+			update_ref("push into current branch (detach)", "HEAD",
+				old_sha1, NULL, REF_NODEREF, UPDATE_REFS_DIE_ON_ERR);
+			break;
 		}
 	}
 
@@ -788,6 +840,8 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 					refuse_unconfigured_deny_delete_current();
 				rp_error("refusing to delete the current branch: %s", name);
 				return "deletion of the current branch prohibited";
+			default:
+				die ("Invalid denyDeleteCurrent setting");
 			}
 		}
 	}
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index f4da20a..3981d1b 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1330,4 +1330,40 @@ test_expect_success 'fetch into bare respects core.logallrefupdates' '
 	)
 '
 
+test_expect_success 'receive.denyCurrentBranch = updateInstead' '
+	git push testrepo master &&
+	(cd testrepo &&
+		git reset --hard &&
+		git config receive.denyCurrentBranch updateInstead
+	) &&
+	test_commit third path2 &&
+	git push testrepo master &&
+	test $(git rev-parse HEAD) = $(cd testrepo && git rev-parse HEAD) &&
+	test third = "$(cat testrepo/path2)" &&
+	(cd testrepo &&
+		git update-index --refresh &&
+		git diff-files --quiet &&
+		git diff-index --cached HEAD --
+	)
+'
+
+test_expect_success 'receive.denyCurrentBranch = detachInstead' '
+	(cd testrepo &&
+		git reset --hard &&
+		git config receive.denyCurrentBranch detachInstead
+	) &&
+	OLDHEAD=$(cd testrepo && git rev-parse HEAD) &&
+	test_commit fourth path2 &&
+	test fourth = "$(cat path2)" &&
+	git push testrepo master &&
+	test $OLDHEAD = $(cd testrepo && git rev-parse HEAD) &&
+	test fourth != "$(cat testrepo/path2)" &&
+	(cd testrepo &&
+		test_must_fail git symbolic-ref HEAD &&
+		git update-index --refresh &&
+		git diff-files --quiet &&
+		git diff-index --cached HEAD --
+	)
+'
+
 test_done
-- 
2.0.0.rc3.9669.g840d1f9

[PATCH 2/2] Let deny.currentBranch=updateInstead ignore submodules

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:02:51

They are not affected by the update anyway.

Signed-off-by: Johannes Schindelin <redacted>
---
 builtin/receive-pack.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index be4172f..4ba51df 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -740,7 +740,7 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 static void merge_worktree(unsigned char *sha1)
 {
 	const char *update_refresh[] = {
-		"update-index", "--refresh", NULL
+		"update-index", "--ignore-submodules", "--refresh", NULL
 	};
 	const char *read_tree[] = {
 		"read-tree", "-u", "-m", sha1_to_hex(sha1), NULL
-- 
2.0.0.rc3.9669.g840d1f9

Re: [PATCH 1/2] Add a few more values for receive.denyCurrentBranch

From: Jeff King <hidden>
Date: 2016-06-15 23:02:53

On Fri, Nov 07, 2014 at 02:58:17PM +0100, Johannes Schindelin wrote:
Under certain circumstances, it makes a *lot* of sense to allow pushing
into the current branch. For example, when two machines with different
Operating Systems are required for testing, it makes much more sense to
synchronize between working directories than having to go via a third
server.
FWIW, I do this without a third server (and without resorting to pull),
with:

  host1$ git push host2 master:refs/remotes/host1/master
  host2$ git merge host1/master

You can even set up a push refspec to make "git push host2" do the right
thing.

That being said, I do like the premise of your patch, as it eliminates
the extra step on the remote side (which is not that big a deal in
itself, but when you realize that host2 _did_ have some changes on it,
then you end up doing the merge there, when in general I'd prefer to do
all the work on host1 via "git pull").

-Peff

Re: [PATCH 1/2] Add a few more values for receive.denyCurrentBranch

From: brian m. carlson <hidden>
Date: 2016-06-15 23:02:53

On Sat, Nov 08, 2014 at 06:18:55AM -0500, Jeff King wrote:
On Fri, Nov 07, 2014 at 02:58:17PM +0100, Johannes Schindelin wrote:
quoted
Under certain circumstances, it makes a *lot* of sense to allow pushing
into the current branch. For example, when two machines with different
Operating Systems are required for testing, it makes much more sense to
synchronize between working directories than having to go via a third
server.
FWIW, I do this without a third server (and without resorting to pull),
with:

  host1$ git push host2 master:refs/remotes/host1/master
  host2$ git merge host1/master

You can even set up a push refspec to make "git push host2" do the right
thing.
I do something similar, but it's inconvenient when the repo you're
pushing into is $HOME, since you have to type something like "exec zsh
-l" in order to fix things up.
That being said, I do like the premise of your patch, as it eliminates
the extra step on the remote side (which is not that big a deal in
itself, but when you realize that host2 _did_ have some changes on it,
then you end up doing the merge there, when in general I'd prefer to do
all the work on host1 via "git pull").
I agree.  This is very useful.
-- 
brian m. carlson / brian with sandals: Houston, Texas, US
+1 832 623 2791 | http://www.crustytoothpaste.net/~bmc | My opinion only
OpenPGP: RSA v4 4096b: 88AC E9B2 9196 305B A994 7552 F1BA 225C 0223 B187

Re: [PATCH 1/2] Add a few more values for receive.denyCurrentBranch

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:02:54

Hi Peff,

On Sat, 8 Nov 2014, Jeff King wrote:
On Fri, Nov 07, 2014 at 02:58:17PM +0100, Johannes Schindelin wrote:
quoted
Under certain circumstances, it makes a *lot* of sense to allow pushing
into the current branch. For example, when two machines with different
Operating Systems are required for testing, it makes much more sense to
synchronize between working directories than having to go via a third
server.
FWIW, I do this without a third server (and without resorting to pull),
with:

  host1$ git push host2 master:refs/remotes/host1/master
  host2$ git merge host1/master

You can even set up a push refspec to make "git push host2" do the right
thing.

That being said, I do like the premise of your patch, as it eliminates
the extra step on the remote side (which is not that big a deal in
itself, but when you realize that host2 _did_ have some changes on it,
then you end up doing the merge there, when in general I'd prefer to do
all the work on host1 via "git pull").
Plus: you have the luxury of working on an OS that makes ssh'ing from
another machine relatively easy. At least if you have the root password on
your machine. Which, I hate to point it out, is not too common a
commodity.

Ciao,
Dscho

[PATCH v2 0/2] Support updating working trees when pushing into non-bare repos

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:02:54

This patch series adds support for two new receive.denyCurrentBranch settings:
one to update the working directory (which must be clean, i.e. there must not
be any uncommitted changes) when pushing into the current branch, the other
setting detaches the HEAD instead.

The scenario in which in particular the 'updateInstead' setting became a
boon in this developer's daily work is when trying to get a bug fix from a
Windows computer, a virtual machine or a user's machine onto his main
machine (in all of those cases it is only possible to connect via ssh in one
direction, but not in the reverse direction).

Interdiff vs v1 below the diffstat.

Johannes Schindelin (2):
  Clean stale environment pointer in finish_command()
  Add a few more options for receive.denyCurrentBranch

 Documentation/config.txt |  9 ++++++++
 builtin/receive-pack.c   | 60 ++++++++++++++++++++++++++++++++++++++++++++++--
 run-command.c            |  3 +++
 t/t5516-fetch-push.sh    | 36 +++++++++++++++++++++++++++++
 4 files changed, 106 insertions(+), 2 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 6fc0d6d..fc9b8db 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2130,10 +2130,14 @@ receive.denyCurrentBranch::
 	proceed. If set to false or "ignore", allow such pushes with no
 	message. Defaults to "refuse".
 +
-There are two more options: "updateInstead" which will update the working
-directory (must be clean) if pushing into the current branch, and
-"detachInstead" which will leave the working directory untouched, detaching
-the HEAD so it does not need to change.
+Another option is "updateInstead" which will update the working
+directory (must be clean) if pushing into the current branch. This option is
+intended for synchronizing working directories when one side is not easily
+accessible via ssh (e.g. inside a VM).
++
+Yet another option is "detachInstead" which will detach the HEAD if updates
+are pushed into the current branch; That way, the current revision, the
+index and the working directory are always left untouched by pushes.
 
 receive.denyNonFastForwards::
 	If set to true, git-receive-pack will deny a ref update which is
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
index 2b2d797..7db860f 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 GVF=GIT-VERSION-FILE
-DEF_VER=v2.2.0-rc0
+DEF_VER=v2.2.0-rc1
 
 LF='
 '
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 4ba51df..4534e88 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -28,7 +28,7 @@ enum deny_action {
 	DENY_WARN,
 	DENY_REFUSE,
 	DENY_UPDATE_INSTEAD,
-	DENY_DETACH_INSTEAD,
+	DENY_DETACH_INSTEAD
 };
 
 static int deny_deletes;
@@ -737,7 +737,7 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 	return 0;
 }
 
-static void merge_worktree(unsigned char *sha1)
+static const char *merge_worktree(unsigned char *sha1)
 {
 	const char *update_refresh[] = {
 		"update-index", "--ignore-submodules", "--refresh", NULL
@@ -745,41 +745,36 @@ static void merge_worktree(unsigned char *sha1)
 	const char *read_tree[] = {
 		"read-tree", "-u", "-m", sha1_to_hex(sha1), NULL
 	};
-	struct child_process child;
-	struct strbuf git_env = STRBUF_INIT;
-	const char *env[2];
+	struct child_process child = CHILD_PROCESS_INIT;
 
 	if (is_bare_repository())
-		die ("denyCurrentBranch = updateInstead needs a worktree");
-
-	strbuf_addf(&git_env, "GIT_DIR=%s", absolute_path(get_git_dir()));
-	env[0] = git_env.buf;
-	env[1] = NULL;
+		return "denyCurrentBranch = updateInstead needs a worktree";
 
-	memset(&child, 0, sizeof(child));
+	argv_array_pushf(&child.env_array, "GIT_DIR=%s", absolute_path(get_git_dir()));
 	child.argv = update_refresh;
-	child.env = env;
 	child.dir = git_work_tree_cfg ? git_work_tree_cfg : "..";
 	child.stdout_to_stderr = 1;
 	child.git_cmd = 1;
 	if (run_command(&child))
-		die ("Could not refresh the index");
+		die("Could not refresh the index");
 
+	/* finish_command cleared the environment; reinitialize */
+	argv_array_pushf(&child.env_array, "GIT_DIR=%s", absolute_path(get_git_dir()));
 	child.argv = read_tree;
 	child.no_stdin = 1;
 	child.no_stdout = 1;
 	child.stdout_to_stderr = 0;
 	if (run_command(&child))
-		die ("Could not merge working tree with new HEAD.  Good luck.");
+		die("Could not merge working tree with new HEAD.");
 
-	strbuf_release(&git_env);
+	return NULL;
 }
 
 static const char *update(struct command *cmd, struct shallow_info *si)
 {
 	const char *name = cmd->ref_name;
 	struct strbuf namespaced_name_buf = STRBUF_INIT;
-	const char *namespaced_name;
+	const char *namespaced_name, *ret;
 	unsigned char *old_sha1 = cmd->old_sha1;
 	unsigned char *new_sha1 = cmd->new_sha1;
 
@@ -806,11 +801,17 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 				refuse_unconfigured_deny();
 			return "branch is currently checked out";
 		case DENY_UPDATE_INSTEAD:
-			merge_worktree(new_sha1);
+			ret = merge_worktree(new_sha1);
+			if (ret)
+				return ret;
 			break;
 		case DENY_DETACH_INSTEAD:
-			update_ref("push into current branch (detach)", "HEAD",
-				old_sha1, NULL, REF_NODEREF, UPDATE_REFS_DIE_ON_ERR);
+			ret = update_ref("push into current branch (detach)",
+				"HEAD", old_sha1, NULL, REF_NODEREF,
+				UPDATE_REFS_DIE_ON_ERR) ?
+				"Could not detach HEAD" : NULL;
+			if (ret)
+				return ret;
 			break;
 		}
 	}
diff --git a/run-command.c b/run-command.c
index 79a0a76..85578da 100644
--- a/run-command.c
+++ b/run-command.c
@@ -555,6 +555,9 @@ int finish_command(struct child_process *cmd)
 {
 	int ret = wait_or_whine(cmd->pid, cmd->argv[0]);
 	argv_array_clear(&cmd->args);
+	/* Avoid pointing to a stale environment */
+	if (cmd->env == cmd->env_array.argv)
+		cmd->env = NULL;
 	argv_array_clear(&cmd->env_array);
 	return ret;
 }
-- 
2.0.0.rc3.9669.g840d1f9

[PATCH v3 0/1] Support updating working trees when pushing into non-bare repos

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:02:55

This patch series adds support for a new receive.denyCurrentBranch setting
to update the working directory (which must be clean, i.e. there must not be
any uncommitted changes) when pushing into the current branch.

The scenario in which the 'updateInstead' setting became a boon in this
developer's daily work is when trying to get a bug fix from a Windows
computer, a virtual machine or a user's machine onto his main machine (in
all of those cases it is only possible to connect via ssh in one direction,
but not in the reverse direction).

Interdiff vs v2 below the diffstat.

Johannes Schindelin (1):
  Add another option for receive.denyCurrentBranch

 Documentation/config.txt |  5 ++++
 builtin/receive-pack.c   | 78 ++++++++++++++++++++++++++++++++++++++++++++++--
 t/t5516-fetch-push.sh    | 17 +++++++++++
 3 files changed, 98 insertions(+), 2 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 4f9fe81..c384515 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2134,10 +2134,6 @@ Another option is "updateInstead" which will update the working
 directory (must be clean) if pushing into the current branch. This option is
 intended for synchronizing working directories when one side is not easily
 accessible via ssh (e.g. inside a VM).
-+
-Yet another option is "detachInstead" which will detach the HEAD if updates
-are pushed into the current branch; That way, the current revision, the
-index and the working directory are always left untouched by pushes.
 
 receive.denyNonFastForwards::
 	If set to true, git-receive-pack will deny a ref update which is
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 4534e88..836720f 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -27,8 +27,7 @@ enum deny_action {
 	DENY_IGNORE,
 	DENY_WARN,
 	DENY_REFUSE,
-	DENY_UPDATE_INSTEAD,
-	DENY_DETACH_INSTEAD
+	DENY_UPDATE_INSTEAD
 };
 
 static int deny_deletes;
@@ -78,6 +77,8 @@ static enum deny_action parse_deny_action(const char *var, const char *value)
 			return DENY_WARN;
 		if (!strcasecmp(value, "refuse"))
 			return DENY_REFUSE;
+		if (!strcasecmp(value, "updateinstead"))
+			return DENY_UPDATE_INSTEAD;
 	}
 	if (git_config_bool(var, value))
 		return DENY_REFUSE;
@@ -122,12 +123,7 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 	}
 
 	if (!strcmp(var, "receive.denycurrentbranch")) {
-		if (value && !strcasecmp(value, "updateinstead"))
-			deny_current_branch = DENY_UPDATE_INSTEAD;
-		else if (value && !strcasecmp(value, "detachinstead"))
-			deny_current_branch = DENY_DETACH_INSTEAD;
-		else
-			deny_current_branch = parse_deny_action(var, value);
+		deny_current_branch = parse_deny_action(var, value);
 		return 0;
 	}
 
@@ -737,36 +733,66 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 	return 0;
 }
 
-static const char *merge_worktree(unsigned char *sha1)
+static const char *update_worktree(unsigned char *sha1)
 {
 	const char *update_refresh[] = {
 		"update-index", "--ignore-submodules", "--refresh", NULL
 	};
+	const char *diff_index[] = {
+		"diff-index", "--quiet", "--cached", "--ignore-submodules",
+		"HEAD", "--", NULL
+	};
 	const char *read_tree[] = {
 		"read-tree", "-u", "-m", sha1_to_hex(sha1), NULL
 	};
+	const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
+	struct argv_array env = ARGV_ARRAY_INIT;
 	struct child_process child = CHILD_PROCESS_INIT;
 
 	if (is_bare_repository())
 		return "denyCurrentBranch = updateInstead needs a worktree";
 
-	argv_array_pushf(&child.env_array, "GIT_DIR=%s", absolute_path(get_git_dir()));
+	argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
+
 	child.argv = update_refresh;
-	child.dir = git_work_tree_cfg ? git_work_tree_cfg : "..";
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
 	child.stdout_to_stderr = 1;
 	child.git_cmd = 1;
-	if (run_command(&child))
-		die("Could not refresh the index");
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Up-to-date check failed";
+	}
 
-	/* finish_command cleared the environment; reinitialize */
-	argv_array_pushf(&child.env_array, "GIT_DIR=%s", absolute_path(get_git_dir()));
+	/* run_command() does not clean up completely; reinitialize */
+	child_process_init(&child);
+	child.argv = diff_index;
+	child.env = env.argv;
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.stdout_to_stderr = 0;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Working directory not clean";
+	}
+
+	/* run_command() does not clean up completely; reinitialize */
+	child_process_init(&child);
 	child.argv = read_tree;
+	child.env = env.argv;
+	child.dir = work_tree;
 	child.no_stdin = 1;
 	child.no_stdout = 1;
 	child.stdout_to_stderr = 0;
-	if (run_command(&child))
-		die("Could not merge working tree with new HEAD.");
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Could not update working tree to new HEAD";
+	}
 
+	argv_array_clear(&env);
 	return NULL;
 }
 
@@ -801,15 +827,7 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 				refuse_unconfigured_deny();
 			return "branch is currently checked out";
 		case DENY_UPDATE_INSTEAD:
-			ret = merge_worktree(new_sha1);
-			if (ret)
-				return ret;
-			break;
-		case DENY_DETACH_INSTEAD:
-			ret = update_ref("push into current branch (detach)",
-				"HEAD", old_sha1, NULL, REF_NODEREF,
-				UPDATE_REFS_DIE_ON_ERR) ?
-				"Could not detach HEAD" : NULL;
+			ret = update_worktree(new_sha1);
 			if (ret)
 				return ret;
 			break;
@@ -837,12 +855,13 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 				break;
 			case DENY_REFUSE:
 			case DENY_UNCONFIGURED:
+			case DENY_UPDATE_INSTEAD:
 				if (deny_delete_current == DENY_UNCONFIGURED)
 					refuse_unconfigured_deny_delete_current();
 				rp_error("refusing to delete the current branch: %s", name);
 				return "deletion of the current branch prohibited";
 			default:
-				die ("Invalid denyDeleteCurrent setting");
+				return "Invalid denyDeleteCurrent setting";
 			}
 		}
 	}
diff --git a/run-command.c b/run-command.c
index 551c6c2..a476999 100644
--- a/run-command.c
+++ b/run-command.c
@@ -555,9 +555,6 @@ int finish_command(struct child_process *cmd)
 {
 	int ret = wait_or_whine(cmd->pid, cmd->argv[0]);
 	argv_array_clear(&cmd->args);
-	/* Avoid pointing to a stale environment */
-	if (cmd->env == cmd->env_array.argv)
-		cmd->env = NULL;
 	argv_array_clear(&cmd->env_array);
 	return ret;
 }
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 3981d1b..ba002a9 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1347,23 +1347,4 @@ test_expect_success 'receive.denyCurrentBranch = updateInstead' '
 	)
 '
 
-test_expect_success 'receive.denyCurrentBranch = detachInstead' '
-	(cd testrepo &&
-		git reset --hard &&
-		git config receive.denyCurrentBranch detachInstead
-	) &&
-	OLDHEAD=$(cd testrepo && git rev-parse HEAD) &&
-	test_commit fourth path2 &&
-	test fourth = "$(cat path2)" &&
-	git push testrepo master &&
-	test $OLDHEAD = $(cd testrepo && git rev-parse HEAD) &&
-	test fourth != "$(cat testrepo/path2)" &&
-	(cd testrepo &&
-		test_must_fail git symbolic-ref HEAD &&
-		git update-index --refresh &&
-		git diff-files --quiet &&
-		git diff-index --cached HEAD --
-	)
-'
-
 test_done
-- 
2.0.0.rc3.9669.g840d1f9

[PATCH v3 1/1] Add another option for receive.denyCurrentBranch

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:02:55

When synchronizing between working directories, it can be handy to update
the current branch via 'push' rather than 'pull', e.g. when pushing a fix
from inside a VM, or when pushing a fix made on a user's machine (where
the developer is not at liberty to install an ssh daemon let alone know
the user's password).

The common workaround – pushing into a temporary branch and then merging
on the other machine – is no longer necessary with this patch.

The new option is:

'updateInstead':
	Update the working tree accordingly, but refuse to do so if there
	are any uncommitted changes.

Signed-off-by: Johannes Schindelin <redacted>
---
 Documentation/config.txt |  5 ++++
 builtin/receive-pack.c   | 78 ++++++++++++++++++++++++++++++++++++++++++++++--
 t/t5516-fetch-push.sh    | 17 +++++++++++
 3 files changed, 98 insertions(+), 2 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 9220725..c384515 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2129,6 +2129,11 @@ receive.denyCurrentBranch::
 	print a warning of such a push to stderr, but allow the push to
 	proceed. If set to false or "ignore", allow such pushes with no
 	message. Defaults to "refuse".
++
+Another option is "updateInstead" which will update the working
+directory (must be clean) if pushing into the current branch. This option is
+intended for synchronizing working directories when one side is not easily
+accessible via ssh (e.g. inside a VM).
 
 receive.denyNonFastForwards::
 	If set to true, git-receive-pack will deny a ref update which is
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 32fc540..836720f 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -26,7 +26,8 @@ enum deny_action {
 	DENY_UNCONFIGURED,
 	DENY_IGNORE,
 	DENY_WARN,
-	DENY_REFUSE
+	DENY_REFUSE,
+	DENY_UPDATE_INSTEAD
 };
 
 static int deny_deletes;
@@ -76,6 +77,8 @@ static enum deny_action parse_deny_action(const char *var, const char *value)
 			return DENY_WARN;
 		if (!strcasecmp(value, "refuse"))
 			return DENY_REFUSE;
+		if (!strcasecmp(value, "updateinstead"))
+			return DENY_UPDATE_INSTEAD;
 	}
 	if (git_config_bool(var, value))
 		return DENY_REFUSE;
@@ -730,11 +733,74 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 	return 0;
 }
 
+static const char *update_worktree(unsigned char *sha1)
+{
+	const char *update_refresh[] = {
+		"update-index", "--ignore-submodules", "--refresh", NULL
+	};
+	const char *diff_index[] = {
+		"diff-index", "--quiet", "--cached", "--ignore-submodules",
+		"HEAD", "--", NULL
+	};
+	const char *read_tree[] = {
+		"read-tree", "-u", "-m", sha1_to_hex(sha1), NULL
+	};
+	const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
+	struct argv_array env = ARGV_ARRAY_INIT;
+	struct child_process child = CHILD_PROCESS_INIT;
+
+	if (is_bare_repository())
+		return "denyCurrentBranch = updateInstead needs a worktree";
+
+	argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
+
+	child.argv = update_refresh;
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
+	child.stdout_to_stderr = 1;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Up-to-date check failed";
+	}
+
+	/* run_command() does not clean up completely; reinitialize */
+	child_process_init(&child);
+	child.argv = diff_index;
+	child.env = env.argv;
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.stdout_to_stderr = 0;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Working directory not clean";
+	}
+
+	/* run_command() does not clean up completely; reinitialize */
+	child_process_init(&child);
+	child.argv = read_tree;
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.stdout_to_stderr = 0;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Could not update working tree to new HEAD";
+	}
+
+	argv_array_clear(&env);
+	return NULL;
+}
+
 static const char *update(struct command *cmd, struct shallow_info *si)
 {
 	const char *name = cmd->ref_name;
 	struct strbuf namespaced_name_buf = STRBUF_INIT;
-	const char *namespaced_name;
+	const char *namespaced_name, *ret;
 	unsigned char *old_sha1 = cmd->old_sha1;
 	unsigned char *new_sha1 = cmd->new_sha1;
 
@@ -760,6 +826,11 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 			if (deny_current_branch == DENY_UNCONFIGURED)
 				refuse_unconfigured_deny();
 			return "branch is currently checked out";
+		case DENY_UPDATE_INSTEAD:
+			ret = update_worktree(new_sha1);
+			if (ret)
+				return ret;
+			break;
 		}
 	}
 
@@ -784,10 +855,13 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 				break;
 			case DENY_REFUSE:
 			case DENY_UNCONFIGURED:
+			case DENY_UPDATE_INSTEAD:
 				if (deny_delete_current == DENY_UNCONFIGURED)
 					refuse_unconfigured_deny_delete_current();
 				rp_error("refusing to delete the current branch: %s", name);
 				return "deletion of the current branch prohibited";
+			default:
+				return "Invalid denyDeleteCurrent setting";
 			}
 		}
 	}
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index f4da20a..ba002a9 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1330,4 +1330,21 @@ test_expect_success 'fetch into bare respects core.logallrefupdates' '
 	)
 '
 
+test_expect_success 'receive.denyCurrentBranch = updateInstead' '
+	git push testrepo master &&
+	(cd testrepo &&
+		git reset --hard &&
+		git config receive.denyCurrentBranch updateInstead
+	) &&
+	test_commit third path2 &&
+	git push testrepo master &&
+	test $(git rev-parse HEAD) = $(cd testrepo && git rev-parse HEAD) &&
+	test third = "$(cat testrepo/path2)" &&
+	(cd testrepo &&
+		git update-index --refresh &&
+		git diff-files --quiet &&
+		git diff-index --cached HEAD --
+	)
+'
+
 test_done
-- 
2.0.0.rc3.9669.g840d1f9

[PATCH v4] Support updating working trees when pushing into non-bare repos

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:03:07

This patch "series" adds support for a new receive.denyCurrentBranch setting
to update the working directory (which must be clean, i.e. there must not be
any uncommitted changes) when pushing into the current branch.

The scenario in which the 'updateInstead' setting became a boon in this
developer's daily work is when trying to get a bug fix from a Windows
computer, a virtual machine, or when getting a bug fix from a user's machine
onto his main machine (in all of those cases it is only possible to connect
via ssh in one direction, but not in the reverse direction). It also comes
in handy when updating a live web site via push (in which case a clean
working directory is an absolute must).

As to the name 'updateInstead': since I do not want the option to perform
the equivalent of a checkout (where staged changes would be okay), I stuck
to the name I use in all of my $HOME/.gitconfigs. Hopefully you don't
mind.

Interdiff vs v3 below the diffstat

Johannes Schindelin (1):
  Add another option for receive.denyCurrentBranch

 Documentation/config.txt |  7 ++++
 builtin/receive-pack.c   | 94 ++++++++++++++++++++++++++++++++++++++++++++++--
 t/t5516-fetch-push.sh    | 21 +++++++++++
 3 files changed, 120 insertions(+), 2 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index c384515..0519073 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2133,7 +2133,9 @@ receive.denyCurrentBranch::
 Another option is "updateInstead" which will update the working
 directory (must be clean) if pushing into the current branch. This option is
 intended for synchronizing working directories when one side is not easily
-accessible via ssh (e.g. inside a VM).
+accessible via interactive ssh (e.g. a live web site, hence the requirement
+that the working directory be clean). This mode also comes in handy when
+developing inside a VM to test and fix code on different Operating Systems.
 
 receive.denyNonFastForwards::
 	If set to true, git-receive-pack will deny a ref update which is
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 836720f..1b05e4e 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -736,7 +736,10 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 static const char *update_worktree(unsigned char *sha1)
 {
 	const char *update_refresh[] = {
-		"update-index", "--ignore-submodules", "--refresh", NULL
+		"update-index", "--ignore-submodules", "--refresh", "-q", NULL
+	};
+	const char *diff_files[] = {
+		"diff-files", "--quiet", "--ignore-submodules", "--", NULL
 	};
 	const char *diff_index[] = {
 		"diff-index", "--quiet", "--cached", "--ignore-submodules",
@@ -767,6 +770,19 @@ static const char *update_worktree(unsigned char *sha1)
 
 	/* run_command() does not clean up completely; reinitialize */
 	child_process_init(&child);
+	child.argv = diff_files;
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
+	child.stdout_to_stderr = 1;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Working directory not clean";
+	}
+
+	/* run_command() does not clean up completely; reinitialize */
+	child_process_init(&child);
 	child.argv = diff_index;
 	child.env = env.argv;
 	child.no_stdin = 1;
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index ba002a9..b8df39c 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1343,8 +1343,12 @@ test_expect_success 'receive.denyCurrentBranch = updateInstead' '
 	(cd testrepo &&
 		git update-index --refresh &&
 		git diff-files --quiet &&
-		git diff-index --cached HEAD --
-	)
+		git diff-index --cached HEAD -- &&
+		echo changed > path2 &&
+		git add path2
+	) &&
+	test_commit fourth path2 &&
+	test_must_fail git push testrepo master
 '
 
 test_done
-- 
2.0.0.rc3.9669.g840d1f9

[PATCH v4] Add another option for receive.denyCurrentBranch

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:03:07

When synchronizing between working directories, it can be handy to update
the current branch via 'push' rather than 'pull', e.g. when pushing a fix
from inside a VM, or when pushing a fix made on a user's machine (where
the developer is not at liberty to install an ssh daemon let alone know
the user's password).

The common workaround – pushing into a temporary branch and then merging
on the other machine – is no longer necessary with this patch.

The new option is:

'updateInstead':
	Update the working tree accordingly, but refuse to do so if there
	are any uncommitted changes.

Signed-off-by: Johannes Schindelin <redacted>
---
 Documentation/config.txt |  7 ++++
 builtin/receive-pack.c   | 94 ++++++++++++++++++++++++++++++++++++++++++++++--
 t/t5516-fetch-push.sh    | 21 +++++++++++
 3 files changed, 120 insertions(+), 2 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 9220725..0519073 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2129,6 +2129,13 @@ receive.denyCurrentBranch::
 	print a warning of such a push to stderr, but allow the push to
 	proceed. If set to false or "ignore", allow such pushes with no
 	message. Defaults to "refuse".
++
+Another option is "updateInstead" which will update the working
+directory (must be clean) if pushing into the current branch. This option is
+intended for synchronizing working directories when one side is not easily
+accessible via interactive ssh (e.g. a live web site, hence the requirement
+that the working directory be clean). This mode also comes in handy when
+developing inside a VM to test and fix code on different Operating Systems.
 
 receive.denyNonFastForwards::
 	If set to true, git-receive-pack will deny a ref update which is
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e908d07..7ff8be5 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -26,7 +26,8 @@ enum deny_action {
 	DENY_UNCONFIGURED,
 	DENY_IGNORE,
 	DENY_WARN,
-	DENY_REFUSE
+	DENY_REFUSE,
+	DENY_UPDATE_INSTEAD
 };
 
 static int deny_deletes;
@@ -76,6 +77,8 @@ static enum deny_action parse_deny_action(const char *var, const char *value)
 			return DENY_WARN;
 		if (!strcasecmp(value, "refuse"))
 			return DENY_REFUSE;
+		if (!strcasecmp(value, "updateinstead"))
+			return DENY_UPDATE_INSTEAD;
 	}
 	if (git_config_bool(var, value))
 		return DENY_REFUSE;
@@ -730,11 +733,90 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 	return 0;
 }
 
+static const char *update_worktree(unsigned char *sha1)
+{
+	const char *update_refresh[] = {
+		"update-index", "--ignore-submodules", "--refresh", "-q", NULL
+	};
+	const char *diff_files[] = {
+		"diff-files", "--quiet", "--ignore-submodules", "--", NULL
+	};
+	const char *diff_index[] = {
+		"diff-index", "--quiet", "--cached", "--ignore-submodules",
+		"HEAD", "--", NULL
+	};
+	const char *read_tree[] = {
+		"read-tree", "-u", "-m", sha1_to_hex(sha1), NULL
+	};
+	const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
+	struct argv_array env = ARGV_ARRAY_INIT;
+	struct child_process child = CHILD_PROCESS_INIT;
+
+	if (is_bare_repository())
+		return "denyCurrentBranch = updateInstead needs a worktree";
+
+	argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
+
+	child.argv = update_refresh;
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
+	child.stdout_to_stderr = 1;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Up-to-date check failed";
+	}
+
+	/* run_command() does not clean up completely; reinitialize */
+	child_process_init(&child);
+	child.argv = diff_files;
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
+	child.stdout_to_stderr = 1;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Working directory not clean";
+	}
+
+	/* run_command() does not clean up completely; reinitialize */
+	child_process_init(&child);
+	child.argv = diff_index;
+	child.env = env.argv;
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.stdout_to_stderr = 0;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Working directory not clean";
+	}
+
+	/* run_command() does not clean up completely; reinitialize */
+	child_process_init(&child);
+	child.argv = read_tree;
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.stdout_to_stderr = 0;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Could not update working tree to new HEAD";
+	}
+
+	argv_array_clear(&env);
+	return NULL;
+}
+
 static const char *update(struct command *cmd, struct shallow_info *si)
 {
 	const char *name = cmd->ref_name;
 	struct strbuf namespaced_name_buf = STRBUF_INIT;
-	const char *namespaced_name;
+	const char *namespaced_name, *ret;
 	unsigned char *old_sha1 = cmd->old_sha1;
 	unsigned char *new_sha1 = cmd->new_sha1;
 
@@ -760,6 +842,11 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 			if (deny_current_branch == DENY_UNCONFIGURED)
 				refuse_unconfigured_deny();
 			return "branch is currently checked out";
+		case DENY_UPDATE_INSTEAD:
+			ret = update_worktree(new_sha1);
+			if (ret)
+				return ret;
+			break;
 		}
 	}
 
@@ -784,10 +871,13 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 				break;
 			case DENY_REFUSE:
 			case DENY_UNCONFIGURED:
+			case DENY_UPDATE_INSTEAD:
 				if (deny_delete_current == DENY_UNCONFIGURED)
 					refuse_unconfigured_deny_delete_current();
 				rp_error("refusing to delete the current branch: %s", name);
 				return "deletion of the current branch prohibited";
+			default:
+				return "Invalid denyDeleteCurrent setting";
 			}
 		}
 	}
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index f4da20a..b8df39c 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1330,4 +1330,25 @@ test_expect_success 'fetch into bare respects core.logallrefupdates' '
 	)
 '
 
+test_expect_success 'receive.denyCurrentBranch = updateInstead' '
+	git push testrepo master &&
+	(cd testrepo &&
+		git reset --hard &&
+		git config receive.denyCurrentBranch updateInstead
+	) &&
+	test_commit third path2 &&
+	git push testrepo master &&
+	test $(git rev-parse HEAD) = $(cd testrepo && git rev-parse HEAD) &&
+	test third = "$(cat testrepo/path2)" &&
+	(cd testrepo &&
+		git update-index --refresh &&
+		git diff-files --quiet &&
+		git diff-index --cached HEAD -- &&
+		echo changed > path2 &&
+		git add path2
+	) &&
+	test_commit fourth path2 &&
+	test_must_fail git push testrepo master
+'
+
 test_done
-- 
2.0.0.rc3.9669.g840d1f9

[PATCH v5] Support updating working trees when pushing into non-bare repos

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:03:07

This patch series adds support for a new receive.denyCurrentBranch setting
to update the working directory (which must be clean, i.e. there must not be
any uncommitted changes) when pushing into the current branch.

The scenario in which the 'updateInstead' setting became a boon in this
developer's daily work is when trying to get a bug fix from a Windows
computer, a virtual machine, or when getting a bug fix from a user's machine
onto his main machine (in all of those cases it is only possible to connect
via ssh in one direction, but not in the reverse direction). It also comes
in handy when updating a live web site via push (in which case a clean
working directory is an absolute must).

Interdiff vs v4 below diffstat.

Johannes Schindelin (1):
  Add another option for receive.denyCurrentBranch

 Documentation/config.txt |  7 ++++
 builtin/receive-pack.c   | 93 ++++++++++++++++++++++++++++++++++++++++++++++--
 t/t5516-fetch-push.sh    | 26 ++++++++++++++
 3 files changed, 124 insertions(+), 2 deletions(-)
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 7ff8be5..bbd9ba3 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -736,7 +736,7 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 static const char *update_worktree(unsigned char *sha1)
 {
 	const char *update_refresh[] = {
-		"update-index", "--ignore-submodules", "--refresh", "-q", NULL
+		"update-index", "-q", "--ignore-submodules", "--refresh", NULL
 	};
 	const char *diff_files[] = {
 		"diff-files", "--quiet", "--ignore-submodules", "--", NULL
@@ -746,7 +746,7 @@ static const char *update_worktree(unsigned char *sha1)
 		"HEAD", "--", NULL
 	};
 	const char *read_tree[] = {
-		"read-tree", "-u", "-m", sha1_to_hex(sha1), NULL
+		"read-tree", "-u", "-m", NULL, NULL
 	};
 	const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
 	struct argv_array env = ARGV_ARRAY_INIT;
@@ -778,10 +778,9 @@ static const char *update_worktree(unsigned char *sha1)
 	child.git_cmd = 1;
 	if (run_command(&child)) {
 		argv_array_clear(&env);
-		return "Working directory not clean";
+		return "Working directory has unstaged changes";
 	}
 
-	/* run_command() does not clean up completely; reinitialize */
 	child_process_init(&child);
 	child.argv = diff_index;
 	child.env = env.argv;
@@ -791,10 +790,10 @@ static const char *update_worktree(unsigned char *sha1)
 	child.git_cmd = 1;
 	if (run_command(&child)) {
 		argv_array_clear(&env);
-		return "Working directory not clean";
+		return "Working directory has staged changes";
 	}
 
-	/* run_command() does not clean up completely; reinitialize */
+	read_tree[3] = sha1_to_hex(sha1);
 	child_process_init(&child);
 	child.argv = read_tree;
 	child.env = env.argv;
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index b8df39c..7b353d0 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1341,14 +1341,19 @@ test_expect_success 'receive.denyCurrentBranch = updateInstead' '
 	test $(git rev-parse HEAD) = $(cd testrepo && git rev-parse HEAD) &&
 	test third = "$(cat testrepo/path2)" &&
 	(cd testrepo &&
-		git update-index --refresh &&
-		git diff-files --quiet &&
-		git diff-index --cached HEAD -- &&
-		echo changed > path2 &&
+		git update-index -q --refresh &&
+		git diff-files --quiet -- &&
+		git diff-index --quiet --cached HEAD -- &&
+		echo changed >path2 &&
 		git add path2
 	) &&
 	test_commit fourth path2 &&
-	test_must_fail git push testrepo master
+	test_must_fail git push testrepo master &&
+	test $(git rev-parse HEAD^) = $(git -C testrepo rev-parse HEAD) &&
+	(cd testrepo &&
+		git diff --quiet &&
+		test changed = "$(cat path2)"
+	)
 '
 
 test_done
-- 
2.0.0.rc3.9669.g840d1f9

[PATCH v5] Add another option for receive.denyCurrentBranch

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:03:07

When synchronizing between working directories, it can be handy to update
the current branch via 'push' rather than 'pull', e.g. when pushing a fix
from inside a VM, or when pushing a fix made on a user's machine (where
the developer is not at liberty to install an ssh daemon let alone know
the user's password).

The common workaround – pushing into a temporary branch and then merging
on the other machine – is no longer necessary with this patch.

The new option is:

'updateInstead':
	Update the working tree accordingly, but refuse to do so if there
	are any uncommitted changes.

Signed-off-by: Johannes Schindelin <redacted>
---
 Documentation/config.txt |  7 ++++
 builtin/receive-pack.c   | 93 ++++++++++++++++++++++++++++++++++++++++++++++--
 t/t5516-fetch-push.sh    | 26 ++++++++++++++
 3 files changed, 124 insertions(+), 2 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 9220725..0519073 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2129,6 +2129,13 @@ receive.denyCurrentBranch::
 	print a warning of such a push to stderr, but allow the push to
 	proceed. If set to false or "ignore", allow such pushes with no
 	message. Defaults to "refuse".
++
+Another option is "updateInstead" which will update the working
+directory (must be clean) if pushing into the current branch. This option is
+intended for synchronizing working directories when one side is not easily
+accessible via interactive ssh (e.g. a live web site, hence the requirement
+that the working directory be clean). This mode also comes in handy when
+developing inside a VM to test and fix code on different Operating Systems.
 
 receive.denyNonFastForwards::
 	If set to true, git-receive-pack will deny a ref update which is
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e908d07..bbd9ba3 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -26,7 +26,8 @@ enum deny_action {
 	DENY_UNCONFIGURED,
 	DENY_IGNORE,
 	DENY_WARN,
-	DENY_REFUSE
+	DENY_REFUSE,
+	DENY_UPDATE_INSTEAD
 };
 
 static int deny_deletes;
@@ -76,6 +77,8 @@ static enum deny_action parse_deny_action(const char *var, const char *value)
 			return DENY_WARN;
 		if (!strcasecmp(value, "refuse"))
 			return DENY_REFUSE;
+		if (!strcasecmp(value, "updateinstead"))
+			return DENY_UPDATE_INSTEAD;
 	}
 	if (git_config_bool(var, value))
 		return DENY_REFUSE;
@@ -730,11 +733,89 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
 	return 0;
 }
 
+static const char *update_worktree(unsigned char *sha1)
+{
+	const char *update_refresh[] = {
+		"update-index", "-q", "--ignore-submodules", "--refresh", NULL
+	};
+	const char *diff_files[] = {
+		"diff-files", "--quiet", "--ignore-submodules", "--", NULL
+	};
+	const char *diff_index[] = {
+		"diff-index", "--quiet", "--cached", "--ignore-submodules",
+		"HEAD", "--", NULL
+	};
+	const char *read_tree[] = {
+		"read-tree", "-u", "-m", NULL, NULL
+	};
+	const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
+	struct argv_array env = ARGV_ARRAY_INIT;
+	struct child_process child = CHILD_PROCESS_INIT;
+
+	if (is_bare_repository())
+		return "denyCurrentBranch = updateInstead needs a worktree";
+
+	argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
+
+	child.argv = update_refresh;
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
+	child.stdout_to_stderr = 1;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Up-to-date check failed";
+	}
+
+	/* run_command() does not clean up completely; reinitialize */
+	child_process_init(&child);
+	child.argv = diff_files;
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
+	child.stdout_to_stderr = 1;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Working directory has unstaged changes";
+	}
+
+	child_process_init(&child);
+	child.argv = diff_index;
+	child.env = env.argv;
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.stdout_to_stderr = 0;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Working directory has staged changes";
+	}
+
+	read_tree[3] = sha1_to_hex(sha1);
+	child_process_init(&child);
+	child.argv = read_tree;
+	child.env = env.argv;
+	child.dir = work_tree;
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.stdout_to_stderr = 0;
+	child.git_cmd = 1;
+	if (run_command(&child)) {
+		argv_array_clear(&env);
+		return "Could not update working tree to new HEAD";
+	}
+
+	argv_array_clear(&env);
+	return NULL;
+}
+
 static const char *update(struct command *cmd, struct shallow_info *si)
 {
 	const char *name = cmd->ref_name;
 	struct strbuf namespaced_name_buf = STRBUF_INIT;
-	const char *namespaced_name;
+	const char *namespaced_name, *ret;
 	unsigned char *old_sha1 = cmd->old_sha1;
 	unsigned char *new_sha1 = cmd->new_sha1;
 
@@ -760,6 +841,11 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 			if (deny_current_branch == DENY_UNCONFIGURED)
 				refuse_unconfigured_deny();
 			return "branch is currently checked out";
+		case DENY_UPDATE_INSTEAD:
+			ret = update_worktree(new_sha1);
+			if (ret)
+				return ret;
+			break;
 		}
 	}
 
@@ -784,10 +870,13 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 				break;
 			case DENY_REFUSE:
 			case DENY_UNCONFIGURED:
+			case DENY_UPDATE_INSTEAD:
 				if (deny_delete_current == DENY_UNCONFIGURED)
 					refuse_unconfigured_deny_delete_current();
 				rp_error("refusing to delete the current branch: %s", name);
 				return "deletion of the current branch prohibited";
+			default:
+				return "Invalid denyDeleteCurrent setting";
 			}
 		}
 	}
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index f4da20a..7b353d0 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1330,4 +1330,30 @@ test_expect_success 'fetch into bare respects core.logallrefupdates' '
 	)
 '
 
+test_expect_success 'receive.denyCurrentBranch = updateInstead' '
+	git push testrepo master &&
+	(cd testrepo &&
+		git reset --hard &&
+		git config receive.denyCurrentBranch updateInstead
+	) &&
+	test_commit third path2 &&
+	git push testrepo master &&
+	test $(git rev-parse HEAD) = $(cd testrepo && git rev-parse HEAD) &&
+	test third = "$(cat testrepo/path2)" &&
+	(cd testrepo &&
+		git update-index -q --refresh &&
+		git diff-files --quiet -- &&
+		git diff-index --quiet --cached HEAD -- &&
+		echo changed >path2 &&
+		git add path2
+	) &&
+	test_commit fourth path2 &&
+	test_must_fail git push testrepo master &&
+	test $(git rev-parse HEAD^) = $(git -C testrepo rev-parse HEAD) &&
+	(cd testrepo &&
+		git diff --quiet &&
+		test changed = "$(cat path2)"
+	)
+'
+
 test_done
-- 
2.0.0.rc3.9669.g840d1f9

Re: [PATCH v5] Add another option for receive.denyCurrentBranch

From: Junio C Hamano <hidden>
Date: 2016-06-15 23:03:09

Thanks, will queue.

I think we would need a bit more tests to protect the feature from
future changes, if you care about the cleanliness requirement of
this feature which is a lot stricter than that of "git checkout".

Perhaps like this one on top.

-- >8 --
From: Junio C Hamano <redacted>
Date: Sun, 30 Nov 2014 17:54:30 -0800
Subject: [PATCH] t5516: more tests for receive.denyCurrentBranch=updateInstead

The previous one tests only the case where a path to be updated by
the push-to-deploy has an incompatible change in the target's
working tree that has already been added to the index, but the
feature itself wants to require the working tree to be a lot cleaner
than what is tested.  Add a handful more tests to protect the
feature from future changes that mistakenly (from the viewpoint of
the inventor of the feature) loosens the cleanliness requirement,
namely:

 - A change only to the working tree but not to the index is still a
   change to be protected;

 - An untracked file in the working tree that would be overwritten
   by a push-to-deploy needs to be protected;

 - A change that happens to make a file identical to what is being
   pushed is still a change to be protected (i.e. the feature's
   cleanliness requirement is more strict than that of checkout).

Also, test that a stat-only change to the working tree is not a
reason to reject a push-to-deploy.

Signed-off-by: Junio C Hamano <redacted>
---
 t/t5516-fetch-push.sh | 96 ++++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 87 insertions(+), 9 deletions(-)
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 7b353d0..85c7fec 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1332,28 +1332,106 @@ test_expect_success 'fetch into bare respects core.logallrefupdates' '
 
 test_expect_success 'receive.denyCurrentBranch = updateInstead' '
 	git push testrepo master &&
-	(cd testrepo &&
+	(
+		cd testrepo &&
 		git reset --hard &&
 		git config receive.denyCurrentBranch updateInstead
 	) &&
 	test_commit third path2 &&
+
+	# Try pushing into a repository with pristine working tree
 	git push testrepo master &&
-	test $(git rev-parse HEAD) = $(cd testrepo && git rev-parse HEAD) &&
-	test third = "$(cat testrepo/path2)" &&
-	(cd testrepo &&
+	(
+		cd testrepo &&
+		git update-index -q --refresh &&
+		git diff-files --quiet -- &&
+		git diff-index --quiet --cached HEAD -- &&
+		test third = "$(cat path2)" &&
+		test $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD)
+	) &&
+
+	# Try pushing into a repository with working tree needing a refresh
+	(
+		cd testrepo &&
+		git reset --hard HEAD^ &&
+		test $(git -C .. rev-parse HEAD^) = $(git rev-parse HEAD) &&
+		test-chmtime +100 path1
+	) &&
+	git push testrepo master &&
+	(
+		cd testrepo &&
 		git update-index -q --refresh &&
 		git diff-files --quiet -- &&
 		git diff-index --quiet --cached HEAD -- &&
-		echo changed >path2 &&
-		git add path2
+		test_cmp ../path1 path1 &&
+		test third = "$(cat path2)" &&
+		test $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD)
 	) &&
+
+	# Update what is to be pushed
 	test_commit fourth path2 &&
+
+	# Try pushing into a repository with a dirty working tree
+	# (1) the working tree updated
+	(
+		cd testrepo &&
+		echo changed >path1
+	) &&
 	test_must_fail git push testrepo master &&
-	test $(git rev-parse HEAD^) = $(git -C testrepo rev-parse HEAD) &&
-	(cd testrepo &&
+	(
+		cd testrepo &&
+		test $(git -C .. rev-parse HEAD^) = $(git rev-parse HEAD) &&
+		git diff --quiet --cached &&
+		test changed = "$(cat path1)"
+	) &&
+
+	# (2) the index updated
+	(
+		cd testrepo &&
+		echo changed >path1 &&
+		git add path1
+	) &&
+	test_must_fail git push testrepo master &&
+	(
+		cd testrepo &&
+		test $(git -C .. rev-parse HEAD^) = $(git rev-parse HEAD) &&
+		git diff --quiet &&
+		test changed = "$(cat path1)"
+	) &&
+
+	# Introduce a new file in the update
+	test_commit fifth path3 &&
+
+	# (3) the working tree has an untracked file that would interfere
+	(
+		cd testrepo &&
+		git reset --hard &&
+		echo changed >path3
+	) &&
+	test_must_fail git push testrepo master &&
+	(
+		cd testrepo &&
+		test $(git -C .. rev-parse HEAD^^) = $(git rev-parse HEAD) &&
+		git diff --quiet &&
+		git diff --quiet --cached &&
+		test changed = "$(cat path3)"
+	) &&
+
+	# (4) the target changes to what gets pushed but it still is a change
+	(
+		cd testrepo &&
+		git reset --hard &&
+		echo fifth >path3 &&
+		git add path3
+	) &&
+	test_must_fail git push testrepo master &&
+	(
+		cd testrepo &&
+		test $(git -C .. rev-parse HEAD^^) = $(git rev-parse HEAD) &&
 		git diff --quiet &&
-		test changed = "$(cat path2)"
+		test fifth = "$(cat path3)"
 	)
+
 '
 
 test_done
-- 
2.2.0-117-g2c5ea17

Re: [PATCH v5] Add another option for receive.denyCurrentBranch

From: Johannes Schindelin <hidden>
Date: 2016-06-15 23:03:09

Hi Junio,

On Sun, 30 Nov 2014, Junio C Hamano wrote:
Thanks, will queue.
Thanks!
I think we would need a bit more tests to protect the feature from
future changes, if you care about the cleanliness requirement of
this feature which is a lot stricter than that of "git checkout".

Perhaps like this one on top.
Thanks again!
Dscho
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help