Merge branch 'jc/push-to-checkout'
authorJunio C Hamano <gitster@pobox.com>
Wed, 11 Feb 2015 21:43:56 +0000 (13:43 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 11 Feb 2015 21:43:56 +0000 (13:43 -0800)
Extending the js/push-to-deploy topic, the behaviour of "git push"
when updating the working tree and the index with an update to the
branch that is checked out can be tweaked by push-to-checkout hook.

* jc/push-to-checkout:
receive-pack: support push-to-checkout hook
receive-pack: refactor updateInstead codepath

Documentation/config.txt
Documentation/githooks.txt
builtin/receive-pack.c
t/t5516-fetch-push.sh
index 1a54eae8f82da26f8b9190d2ce5530bacf09f771..ae6791db128130080915bf127cfa66b850921f43 100644 (file)
@@ -2158,11 +2158,15 @@ receive.denyCurrentBranch::
        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
+tree 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.
++
+By default, "updateInstead" will refuse the push if the working tree or
+the index have any difference from the HEAD, but the `push-to-checkout`
+hook can be used to customize this.  See linkgit:githooks[5].
 
 receive.denyNonFastForwards::
        If set to true, git-receive-pack will deny a ref update which is
index 9ef2469373a5d7ddc511d9569ed57ca587cc2c6d..7ba0ac965dd6266404d4b3fad74d59a4e79be0dd 100644 (file)
@@ -341,6 +341,36 @@ Both standard output and standard error output are forwarded to
 'git send-pack' on the other end, so you can simply `echo` messages
 for the user.
 
+push-to-checkout
+~~~~~~~~~~~~~~~~
+
+This hook is invoked by 'git-receive-pack' on the remote repository,
+which happens when a 'git push' is done on a local repository, when
+the push tries to update the branch that is currently checked out
+and the `receive.denyCurrentBranch` configuration variable is set to
+`updateInstead`.  Such a push by default is refused if the working
+tree and the index of the remote repository has any difference from
+the currently checked out commit; when both the working tree and the
+index match the current commit, they are updated to match the newly
+pushed tip of the branch.  This hook is to be used to override the
+default behaviour.
+
+The hook receives the commit with which the tip of the current
+branch is going to be updated.  It can exit with a non-zero status
+to refuse the push (when it does so, it must not modify the index or
+the working tree).  Or it can make any necessary changes to the
+working tree and to the index to bring them to the desired state
+when the tip of the current branch is updated to the new commit, and
+exit with a zero status.
+
+For example, the hook can simply run `git read-tree -u -m HEAD "$1"`
+in order to emulate 'git fetch' that is run in the reverse direction
+with `git push`, as the two-tree form of `read-tree -u -m` is
+essentially the same as `git checkout` that switches branches while
+keeping the local changes in the working tree that do not interfere
+with the difference between the branches.
+
+
 pre-auto-gc
 ~~~~~~~~~~~
 
index 4e85e25d0f3cd3108bd4ad1c3be4dba53a30c1a2..e0ce78e5a069670b00da6f3d67b991525c6dd29c 100644 (file)
@@ -743,7 +743,9 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
        return 0;
 }
 
-static const char *update_worktree(unsigned char *sha1)
+static const char *push_to_deploy(unsigned char *sha1,
+                                 struct argv_array *env,
+                                 const char *work_tree)
 {
        const char *update_refresh[] = {
                "update-index", "-q", "--ignore-submodules", "--refresh", NULL
@@ -758,69 +760,87 @@ static const char *update_worktree(unsigned char *sha1)
        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.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);
+       if (run_command(&child))
                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.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);
+       if (run_command(&child))
                return "Working directory has unstaged changes";
-       }
 
        child_process_init(&child);
        child.argv = diff_index;
-       child.env = env.argv;
+       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);
+       if (run_command(&child))
                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.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);
+       if (run_command(&child))
                return "Could not update working tree to new HEAD";
-       }
 
-       argv_array_clear(&env);
        return NULL;
 }
 
+static const char *push_to_checkout_hook = "push-to-checkout";
+
+static const char *push_to_checkout(unsigned char *sha1,
+                                   struct argv_array *env,
+                                   const char *work_tree)
+{
+       argv_array_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
+       if (run_hook_le(env->argv, push_to_checkout_hook,
+                       sha1_to_hex(sha1), NULL))
+               return "push-to-checkout hook declined";
+       else
+               return NULL;
+}
+
+static const char *update_worktree(unsigned char *sha1)
+{
+       const char *retval;
+       const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
+       struct argv_array env = ARGV_ARRAY_INIT;
+
+       if (is_bare_repository())
+               return "denyCurrentBranch = updateInstead needs a worktree";
+
+       argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
+
+       if (!find_hook(push_to_checkout_hook))
+               retval = push_to_deploy(sha1, &env, work_tree);
+       else
+               retval = push_to_checkout(sha1, &env, work_tree);
+
+       argv_array_clear(&env);
+       return retval;
+}
+
 static const char *update(struct command *cmd, struct shallow_info *si)
 {
        const char *name = cmd->ref_name;
index 85c7fecd22a37d06001398883954180a2b1a9868..e4436c170088a69b9efc5712a55bbe1d4bb8113f 100755 (executable)
@@ -1434,4 +1434,67 @@ test_expect_success 'receive.denyCurrentBranch = updateInstead' '
 
 '
 
+test_expect_success 'updateInstead with push-to-checkout hook' '
+       rm -fr testrepo &&
+       git init testrepo &&
+       (
+               cd testrepo &&
+               git pull .. master &&
+               git reset --hard HEAD^^ &&
+               git tag initial &&
+               git config receive.denyCurrentBranch updateInstead &&
+               write_script .git/hooks/push-to-checkout <<-\EOF
+               echo >&2 updating from $(git rev-parse HEAD)
+               echo >&2 updating to "$1"
+
+               git update-index -q --refresh &&
+               git read-tree -u -m HEAD "$1" || {
+                       status=$?
+                       echo >&2 read-tree failed
+                       exit $status
+               }
+               EOF
+       ) &&
+
+       # Try pushing into a pristine
+       git push testrepo master &&
+       (
+               cd testrepo &&
+               git diff --quiet &&
+               git diff HEAD --quiet &&
+               test $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD)
+       ) &&
+
+       # Try pushing into a repository with conflicting change
+       (
+               cd testrepo &&
+               git reset --hard initial &&
+               echo conflicting >path2
+       ) &&
+       test_must_fail git push testrepo master &&
+       (
+               cd testrepo &&
+               test $(git rev-parse initial) = $(git rev-parse HEAD) &&
+               test conflicting = "$(cat path2)" &&
+               git diff-index --quiet --cached HEAD
+       ) &&
+
+       # Try pushing into a repository with unrelated change
+       (
+               cd testrepo &&
+               git reset --hard initial &&
+               echo unrelated >path1 &&
+               echo irrelevant >path5 &&
+               git add path5
+       ) &&
+       git push testrepo master &&
+       (
+               cd testrepo &&
+               test "$(cat path1)" = unrelated &&
+               test "$(cat path5)" = irrelevant &&
+               test "$(git diff --name-only --cached HEAD)" = path5 &&
+               test $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD)
+       )
+'
+
 test_done