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

Reply via email to