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.schinde...@gmx.de>
---
 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

Reply via email to