receive-pack: support push-to-checkout hook

When receive.denyCurrentBranch is set to updateInstead, a push that
tries to update the branch that is currently checked out is accepted
only when the index and the working tree exactly matches the
currently checked out commit, in which case the index and the
working tree are updated to match the pushed commit.  Otherwise the
push is refused.

This hook can be used to customize this "push-to-deploy" logic.  The
hook receives the commit with which the tip of the current branch is
going to be updated, and can decide what kind of local changes are
acceptable and how to update the index and the working tree to match
the updated tip of the current branch.

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.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Junio C Hamano 2014-12-01 15:29:54 -08:00
parent 21b138d0f6
commit 0855331941
4 changed files with 116 additions and 2 deletions

View File

@ -2131,11 +2131,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

View 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
~~~~~~~~~~~

View File

@ -797,6 +797,20 @@ static const char *push_to_deploy(unsigned char *sha1,
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;
@ -808,7 +822,10 @@ static const char *update_worktree(unsigned char *sha1)
argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
retval = push_to_deploy(sha1, &env, work_tree);
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;

View File

@ -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