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.
For developers who are uncomfortable with letting pushes update the
working directory, but who are equally uncomfortable with the idea of
pushing into a temporary ref that will be readily forgotten, there is now
also an option to detach the HEAD if a push wants to update the current
branch (no working directory update is required in such a case because the
branch is no longer current after detaching the HEAD).
The new options are:
'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 <[email protected]>
---
Documentation/config.txt | 9 +++++++
builtin/receive-pack.c | 61 +++++++++++++++++++++++++++++++++++++++++++++---
t/t5516-fetch-push.sh | 36 ++++++++++++++++++++++++++++
3 files changed, 103 insertions(+), 3 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index e8dd76d..fc9b8db 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2129,6 +2129,15 @@ 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).
++
+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 32fc540..4534e88 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,11 +737,44 @@ static int update_shallow_ref(struct command *cmd, struct
shallow_info *si)
return 0;
}
+static const char *merge_worktree(unsigned char *sha1)
+{
+ const char *update_refresh[] = {
+ "update-index", "--ignore-submodules", "--refresh", NULL
+ };
+ const char *read_tree[] = {
+ "read-tree", "-u", "-m", sha1_to_hex(sha1), NULL
+ };
+ 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()));
+ child.argv = update_refresh;
+ 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");
+
+ /* 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.");
+
+ 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 +800,19 @@ 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 = 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;
+ if (ret)
+ return ret;
+ break;
}
}
@@ -788,6 +841,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