Skip to content

Commit cba07bb

Browse files
committed
Merge branch 'jc/push-to-checkout'
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
2 parents 39fa611 + 0855331 commit cba07bb

File tree

4 files changed

+143
-26
lines changed

4 files changed

+143
-26
lines changed

Documentation/config.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2158,11 +2158,15 @@ receive.denyCurrentBranch::
21582158
message. Defaults to "refuse".
21592159
+
21602160
Another option is "updateInstead" which will update the working
2161-
directory (must be clean) if pushing into the current branch. This option is
2161+
tree if pushing into the current branch. This option is
21622162
intended for synchronizing working directories when one side is not easily
21632163
accessible via interactive ssh (e.g. a live web site, hence the requirement
21642164
that the working directory be clean). This mode also comes in handy when
21652165
developing inside a VM to test and fix code on different Operating Systems.
2166+
+
2167+
By default, "updateInstead" will refuse the push if the working tree or
2168+
the index have any difference from the HEAD, but the `push-to-checkout`
2169+
hook can be used to customize this. See linkgit:githooks[5].
21662170

21672171
receive.denyNonFastForwards::
21682172
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: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,9 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
743743
return 0;
744744
}
745745

746-
static const char *update_worktree(unsigned char *sha1)
746+
static const char *push_to_deploy(unsigned char *sha1,
747+
struct argv_array *env,
748+
const char *work_tree)
747749
{
748750
const char *update_refresh[] = {
749751
"update-index", "-q", "--ignore-submodules", "--refresh", NULL
@@ -758,69 +760,87 @@ static const char *update_worktree(unsigned char *sha1)
758760
const char *read_tree[] = {
759761
"read-tree", "-u", "-m", NULL, NULL
760762
};
761-
const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
762-
struct argv_array env = ARGV_ARRAY_INIT;
763763
struct child_process child = CHILD_PROCESS_INIT;
764764

765-
if (is_bare_repository())
766-
return "denyCurrentBranch = updateInstead needs a worktree";
767-
768-
argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
769-
770765
child.argv = update_refresh;
771-
child.env = env.argv;
766+
child.env = env->argv;
772767
child.dir = work_tree;
773768
child.no_stdin = 1;
774769
child.stdout_to_stderr = 1;
775770
child.git_cmd = 1;
776-
if (run_command(&child)) {
777-
argv_array_clear(&env);
771+
if (run_command(&child))
778772
return "Up-to-date check failed";
779-
}
780773

781774
/* run_command() does not clean up completely; reinitialize */
782775
child_process_init(&child);
783776
child.argv = diff_files;
784-
child.env = env.argv;
777+
child.env = env->argv;
785778
child.dir = work_tree;
786779
child.no_stdin = 1;
787780
child.stdout_to_stderr = 1;
788781
child.git_cmd = 1;
789-
if (run_command(&child)) {
790-
argv_array_clear(&env);
782+
if (run_command(&child))
791783
return "Working directory has unstaged changes";
792-
}
793784

794785
child_process_init(&child);
795786
child.argv = diff_index;
796-
child.env = env.argv;
787+
child.env = env->argv;
797788
child.no_stdin = 1;
798789
child.no_stdout = 1;
799790
child.stdout_to_stderr = 0;
800791
child.git_cmd = 1;
801-
if (run_command(&child)) {
802-
argv_array_clear(&env);
792+
if (run_command(&child))
803793
return "Working directory has staged changes";
804-
}
805794

806795
read_tree[3] = sha1_to_hex(sha1);
807796
child_process_init(&child);
808797
child.argv = read_tree;
809-
child.env = env.argv;
798+
child.env = env->argv;
810799
child.dir = work_tree;
811800
child.no_stdin = 1;
812801
child.no_stdout = 1;
813802
child.stdout_to_stderr = 0;
814803
child.git_cmd = 1;
815-
if (run_command(&child)) {
816-
argv_array_clear(&env);
804+
if (run_command(&child))
817805
return "Could not update working tree to new HEAD";
818-
}
819806

820-
argv_array_clear(&env);
821807
return NULL;
822808
}
823809

810+
static const char *push_to_checkout_hook = "push-to-checkout";
811+
812+
static const char *push_to_checkout(unsigned char *sha1,
813+
struct argv_array *env,
814+
const char *work_tree)
815+
{
816+
argv_array_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
817+
if (run_hook_le(env->argv, push_to_checkout_hook,
818+
sha1_to_hex(sha1), NULL))
819+
return "push-to-checkout hook declined";
820+
else
821+
return NULL;
822+
}
823+
824+
static const char *update_worktree(unsigned char *sha1)
825+
{
826+
const char *retval;
827+
const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
828+
struct argv_array env = ARGV_ARRAY_INIT;
829+
830+
if (is_bare_repository())
831+
return "denyCurrentBranch = updateInstead needs a worktree";
832+
833+
argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
834+
835+
if (!find_hook(push_to_checkout_hook))
836+
retval = push_to_deploy(sha1, &env, work_tree);
837+
else
838+
retval = push_to_checkout(sha1, &env, work_tree);
839+
840+
argv_array_clear(&env);
841+
return retval;
842+
}
843+
824844
static const char *update(struct command *cmd, struct shallow_info *si)
825845
{
826846
const char *name = cmd->ref_name;

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)