Skip to content

Commit 0855331

Browse files
committed
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 <[email protected]>
1 parent 21b138d commit 0855331

File tree

4 files changed

+116
-2
lines changed

4 files changed

+116
-2
lines changed

Documentation/config.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2131,11 +2131,15 @@ receive.denyCurrentBranch::
21312131
message. Defaults to "refuse".
21322132
+
21332133
Another option is "updateInstead" which will update the working
2134-
directory (must be clean) if pushing into the current branch. This option is
2134+
tree if pushing into the current branch. This option is
21352135
intended for synchronizing working directories when one side is not easily
21362136
accessible via interactive ssh (e.g. a live web site, hence the requirement
21372137
that the working directory be clean). This mode also comes in handy when
21382138
developing inside a VM to test and fix code on different Operating Systems.
2139+
+
2140+
By default, "updateInstead" will refuse the push if the working tree or
2141+
the index have any difference from the HEAD, but the `push-to-checkout`
2142+
hook can be used to customize this. See linkgit:githooks[5].
21392143

21402144
receive.denyNonFastForwards::
21412145
If set to true, git-receive-pack will deny a ref update which is

Documentation/githooks.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,36 @@ Both standard output and standard error output are forwarded to
341341
'git send-pack' on the other end, so you can simply `echo` messages
342342
for the user.
343343

344+
push-to-checkout
345+
~~~~~~~~~~~~~~~~
346+
347+
This hook is invoked by 'git-receive-pack' on the remote repository,
348+
which happens when a 'git push' is done on a local repository, when
349+
the push tries to update the branch that is currently checked out
350+
and the `receive.denyCurrentBranch` configuration variable is set to
351+
`updateInstead`. Such a push by default is refused if the working
352+
tree and the index of the remote repository has any difference from
353+
the currently checked out commit; when both the working tree and the
354+
index match the current commit, they are updated to match the newly
355+
pushed tip of the branch. This hook is to be used to override the
356+
default behaviour.
357+
358+
The hook receives the commit with which the tip of the current
359+
branch is going to be updated. It can exit with a non-zero status
360+
to refuse the push (when it does so, it must not modify the index or
361+
the working tree). Or it can make any necessary changes to the
362+
working tree and to the index to bring them to the desired state
363+
when the tip of the current branch is updated to the new commit, and
364+
exit with a zero status.
365+
366+
For example, the hook can simply run `git read-tree -u -m HEAD "$1"`
367+
in order to emulate 'git fetch' that is run in the reverse direction
368+
with `git push`, as the two-tree form of `read-tree -u -m` is
369+
essentially the same as `git checkout` that switches branches while
370+
keeping the local changes in the working tree that do not interfere
371+
with the difference between the branches.
372+
373+
344374
pre-auto-gc
345375
~~~~~~~~~~~
346376

builtin/receive-pack.c

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,20 @@ static const char *push_to_deploy(unsigned char *sha1,
797797
return NULL;
798798
}
799799

800+
static const char *push_to_checkout_hook = "push-to-checkout";
801+
802+
static const char *push_to_checkout(unsigned char *sha1,
803+
struct argv_array *env,
804+
const char *work_tree)
805+
{
806+
argv_array_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
807+
if (run_hook_le(env->argv, push_to_checkout_hook,
808+
sha1_to_hex(sha1), NULL))
809+
return "push-to-checkout hook declined";
810+
else
811+
return NULL;
812+
}
813+
800814
static const char *update_worktree(unsigned char *sha1)
801815
{
802816
const char *retval;
@@ -808,7 +822,10 @@ static const char *update_worktree(unsigned char *sha1)
808822

809823
argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
810824

811-
retval = push_to_deploy(sha1, &env, work_tree);
825+
if (!find_hook(push_to_checkout_hook))
826+
retval = push_to_deploy(sha1, &env, work_tree);
827+
else
828+
retval = push_to_checkout(sha1, &env, work_tree);
812829

813830
argv_array_clear(&env);
814831
return retval;

t/t5516-fetch-push.sh

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,4 +1434,67 @@ test_expect_success 'receive.denyCurrentBranch = updateInstead' '
14341434
14351435
'
14361436

1437+
test_expect_success 'updateInstead with push-to-checkout hook' '
1438+
rm -fr testrepo &&
1439+
git init testrepo &&
1440+
(
1441+
cd testrepo &&
1442+
git pull .. master &&
1443+
git reset --hard HEAD^^ &&
1444+
git tag initial &&
1445+
git config receive.denyCurrentBranch updateInstead &&
1446+
write_script .git/hooks/push-to-checkout <<-\EOF
1447+
echo >&2 updating from $(git rev-parse HEAD)
1448+
echo >&2 updating to "$1"
1449+
1450+
git update-index -q --refresh &&
1451+
git read-tree -u -m HEAD "$1" || {
1452+
status=$?
1453+
echo >&2 read-tree failed
1454+
exit $status
1455+
}
1456+
EOF
1457+
) &&
1458+
1459+
# Try pushing into a pristine
1460+
git push testrepo master &&
1461+
(
1462+
cd testrepo &&
1463+
git diff --quiet &&
1464+
git diff HEAD --quiet &&
1465+
test $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD)
1466+
) &&
1467+
1468+
# Try pushing into a repository with conflicting change
1469+
(
1470+
cd testrepo &&
1471+
git reset --hard initial &&
1472+
echo conflicting >path2
1473+
) &&
1474+
test_must_fail git push testrepo master &&
1475+
(
1476+
cd testrepo &&
1477+
test $(git rev-parse initial) = $(git rev-parse HEAD) &&
1478+
test conflicting = "$(cat path2)" &&
1479+
git diff-index --quiet --cached HEAD
1480+
) &&
1481+
1482+
# Try pushing into a repository with unrelated change
1483+
(
1484+
cd testrepo &&
1485+
git reset --hard initial &&
1486+
echo unrelated >path1 &&
1487+
echo irrelevant >path5 &&
1488+
git add path5
1489+
) &&
1490+
git push testrepo master &&
1491+
(
1492+
cd testrepo &&
1493+
test "$(cat path1)" = unrelated &&
1494+
test "$(cat path5)" = irrelevant &&
1495+
test "$(git diff --name-only --cached HEAD)" = path5 &&
1496+
test $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD)
1497+
)
1498+
'
1499+
14371500
test_done

0 commit comments

Comments
 (0)