{"thread":{"id":"38319","subject":"[PATCH v2] receive-pack: support push-to-checkout hook","startedAt":"2015-01-08T23:25:47Z","lastAt":"2015-01-09T08:44:10Z","messageCount":2,"participants":["Junio C Hamano","Johannes Schindelin"],"isPatch":true,"patchVersion":2,"patchTotal":null},"messages":[{"id":"254493","messageId":"xmqqbnm8etxg.fsf@gitster.dls.corp.google.com","threadId":"38319","inReplyTo":null,"subject":"[PATCH v2] receive-pack: support push-to-checkout hook","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2015-01-08T23:25:47Z","receivedAt":"2015-01-08T23:25:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"When receive.denyCurrentBranch is set to updateInstead, a push that\ntries to update the branch that is currently checked out is accepted\nonly when the index and the working tree exactly matches the\ncurrently checked out commit, in which case the index and the\nworking tree are updated to match the pushed commit.  Otherwise the\npush is refused.\n\nThis hook can be used to customize this \"push-to-deploy\" logic.  The\nhook receives the commit with which the tip of the current branch is\ngoing to be updated, and can decide what kind of local changes are\nacceptable and how to update the index and the working tree to match\nthe updated tip of the current branch.\n\nFor example, the hook can simply run `git read-tree -u -m HEAD \"$1\"`\nin order to emulate 'git fetch' that is run in the reverse direction\nwith `git push`, as the two-tree form of `read-tree -u -m` is\nessentially the same as `git checkout` that switches branches while\nkeeping the local changes in the working tree that do not interfere\nwith the difference between the branches.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n\n * This is an update to $gmane/260527; relative to what I have been\n   keeping in 'pu', the only difference is that it comes with\n   documentation updates.\n\n Documentation/config.txt   |  6 ++++-\n Documentation/githooks.txt | 30 ++++++++++++++++++++++\n builtin/receive-pack.c     | 19 +++++++++++++-\n t/t5516-fetch-push.sh      | 63 ++++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 116 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 6a4c19e..4e558ab 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -2131,11 +2131,15 @@ receive.denyCurrentBranch::\n \tmessage. Defaults to \"refuse\".\n +\n Another option is \"updateInstead\" which will update the working\n-directory (must be clean) if pushing into the current branch. This option is\n+tree if pushing into the current branch.  This option is\n intended for synchronizing working directories when one side is not easily\n accessible via interactive ssh (e.g. a live web site, hence the requirement\n that the working directory be clean). This mode also comes in handy when\n developing inside a VM to test and fix code on different Operating Systems.\n++\n+By default, \"updateInstead\" will refuse the push if the working tree or\n+the index have any difference from the HEAD, but the `push-to-checkout`\n+hook can be used to customize this.  See linkgit:githooks[5].\n \n receive.denyNonFastForwards::\n \tIf set to true, git-receive-pack will deny a ref update which is\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 9ef2469..7ba0ac9 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -341,6 +341,36 @@ Both standard output and standard error output are forwarded to\n 'git send-pack' on the other end, so you can simply `echo` messages\n for the user.\n \n+push-to-checkout\n+~~~~~~~~~~~~~~~~\n+\n+This hook is invoked by 'git-receive-pack' on the remote repository,\n+which happens when a 'git push' is done on a local repository, when\n+the push tries to update the branch that is currently checked out\n+and the `receive.denyCurrentBranch` configuration variable is set to\n+`updateInstead`.  Such a push by default is refused if the working\n+tree and the index of the remote repository has any difference from\n+the currently checked out commit; when both the working tree and the\n+index match the current commit, they are updated to match the newly\n+pushed tip of the branch.  This hook is to be used to override the\n+default behaviour.\n+\n+The hook receives the commit with which the tip of the current\n+branch is going to be updated.  It can exit with a non-zero status\n+to refuse the push (when it does so, it must not modify the index or\n+the working tree).  Or it can make any necessary changes to the\n+working tree and to the index to bring them to the desired state\n+when the tip of the current branch is updated to the new commit, and\n+exit with a zero status.\n+\n+For example, the hook can simply run `git read-tree -u -m HEAD \"$1\"`\n+in order to emulate 'git fetch' that is run in the reverse direction\n+with `git push`, as the two-tree form of `read-tree -u -m` is\n+essentially the same as `git checkout` that switches branches while\n+keeping the local changes in the working tree that do not interfere\n+with the difference between the branches.\n+\n+\n pre-auto-gc\n ~~~~~~~~~~~\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 11800cd..fc8ec9c 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -797,6 +797,20 @@ static const char *push_to_deploy(unsigned char *sha1,\n \treturn NULL;\n }\n \n+static const char *push_to_checkout_hook = \"push-to-checkout\";\n+\n+static const char *push_to_checkout(unsigned char *sha1,\n+\t\t\t\t    struct argv_array *env,\n+\t\t\t\t    const char *work_tree)\n+{\n+\targv_array_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n+\tif (run_hook_le(env->argv, push_to_checkout_hook,\n+\t\t\tsha1_to_hex(sha1), NULL))\n+\t\treturn \"push-to-checkout hook declined\";\n+\telse\n+\t\treturn NULL;\n+}\n+\n static const char *update_worktree(unsigned char *sha1)\n {\n \tconst char *retval;\n@@ -808,7 +822,10 @@ static const char *update_worktree(unsigned char *sha1)\n \n \targv_array_pushf(&env, \"GIT_DIR=%s\", absolute_path(get_git_dir()));\n \n-\tretval = push_to_deploy(sha1, &env, work_tree);\n+\tif (!find_hook(push_to_checkout_hook))\n+\t\tretval = push_to_deploy(sha1, &env, work_tree);\n+\telse\n+\t\tretval = push_to_checkout(sha1, &env, work_tree);\n \n \targv_array_clear(&env);\n \treturn retval;\ndiff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh\nindex 85c7fec..e4436c1 100755\n--- a/t/t5516-fetch-push.sh\n+++ b/t/t5516-fetch-push.sh\n@@ -1434,4 +1434,67 @@ test_expect_success 'receive.denyCurrentBranch = updateInstead' '\n \n '\n \n+test_expect_success 'updateInstead with push-to-checkout hook' '\n+\trm -fr testrepo &&\n+\tgit init testrepo &&\n+\t(\n+\t\tcd testrepo &&\n+\t\tgit pull .. master &&\n+\t\tgit reset --hard HEAD^^ &&\n+\t\tgit tag initial &&\n+\t\tgit config receive.denyCurrentBranch updateInstead &&\n+\t\twrite_script .git/hooks/push-to-checkout <<-\\EOF\n+\t\techo >&2 updating from $(git rev-parse HEAD)\n+\t\techo >&2 updating to \"$1\"\n+\n+\t\tgit update-index -q --refresh &&\n+\t\tgit read-tree -u -m HEAD \"$1\" || {\n+\t\t\tstatus=$?\n+\t\t\techo >&2 read-tree failed\n+\t\t\texit $status\n+\t\t}\n+\t\tEOF\n+\t) &&\n+\n+\t# Try pushing into a pristine\n+\tgit push testrepo master &&\n+\t(\n+\t\tcd testrepo &&\n+\t\tgit diff --quiet &&\n+\t\tgit diff HEAD --quiet &&\n+\t\ttest $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD)\n+\t) &&\n+\n+\t# Try pushing into a repository with conflicting change\n+\t(\n+\t\tcd testrepo &&\n+\t\tgit reset --hard initial &&\n+\t\techo conflicting >path2\n+\t) &&\n+\ttest_must_fail git push testrepo master &&\n+\t(\n+\t\tcd testrepo &&\n+\t\ttest $(git rev-parse initial) = $(git rev-parse HEAD) &&\n+\t\ttest conflicting = \"$(cat path2)\" &&\n+\t\tgit diff-index --quiet --cached HEAD\n+\t) &&\n+\n+\t# Try pushing into a repository with unrelated change\n+\t(\n+\t\tcd testrepo &&\n+\t\tgit reset --hard initial &&\n+\t\techo unrelated >path1 &&\n+\t\techo irrelevant >path5 &&\n+\t\tgit add path5\n+\t) &&\n+\tgit push testrepo master &&\n+\t(\n+\t\tcd testrepo &&\n+\t\ttest \"$(cat path1)\" = unrelated &&\n+\t\ttest \"$(cat path5)\" = irrelevant &&\n+\t\ttest \"$(git diff --name-only --cached HEAD)\" = path5 &&\n+\t\ttest $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD)\n+\t)\n+'\n+\n test_done\n-- \n2.2.1-390-g773ecee\n"},{"id":"254503","messageId":"alpine.DEB.1.00.1501090943460.21312@s15462909.onlinehome-server.info","threadId":"38319","inReplyTo":"xmqqbnm8etxg.fsf@gitster.dls.corp.google.com","subject":"Re: [PATCH v2] receive-pack: support push-to-checkout hook","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2015-01-09T08:44:10Z","receivedAt":"2015-01-09T08:44:10Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Thu, 8 Jan 2015, Junio C Hamano wrote:\n\n>  * This is an update to $gmane/260527; relative to what I have been\n>    keeping in 'pu', the only difference is that it comes with\n>    documentation updates.\n\nThanks, it is very nice!\nDscho\n"}]}