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 <johannes.schindelin@xxxxxx> --- 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