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
This commit is contained in:
commit
cba07bb6ff
@ -2158,11 +2158,15 @@ receive.denyCurrentBranch::
|
|||||||
message. Defaults to "refuse".
|
message. Defaults to "refuse".
|
||||||
+
|
+
|
||||||
Another option is "updateInstead" which will update the working
|
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
|
intended for synchronizing working directories when one side is not easily
|
||||||
accessible via interactive ssh (e.g. a live web site, hence the requirement
|
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
|
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.
|
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::
|
receive.denyNonFastForwards::
|
||||||
If set to true, git-receive-pack will deny a ref update which is
|
If set to true, git-receive-pack will deny a ref update which is
|
||||||
|
@ -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
|
'git send-pack' on the other end, so you can simply `echo` messages
|
||||||
for the user.
|
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
|
pre-auto-gc
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -743,7 +743,9 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
|
|||||||
return 0;
|
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[] = {
|
const char *update_refresh[] = {
|
||||||
"update-index", "-q", "--ignore-submodules", "--refresh", NULL
|
"update-index", "-q", "--ignore-submodules", "--refresh", NULL
|
||||||
@ -758,67 +760,85 @@ static const char *update_worktree(unsigned char *sha1)
|
|||||||
const char *read_tree[] = {
|
const char *read_tree[] = {
|
||||||
"read-tree", "-u", "-m", NULL, NULL
|
"read-tree", "-u", "-m", NULL, NULL
|
||||||
};
|
};
|
||||||
|
struct child_process child = CHILD_PROCESS_INIT;
|
||||||
|
|
||||||
|
child.argv = update_refresh;
|
||||||
|
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))
|
||||||
|
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.dir = work_tree;
|
||||||
|
child.no_stdin = 1;
|
||||||
|
child.stdout_to_stderr = 1;
|
||||||
|
child.git_cmd = 1;
|
||||||
|
if (run_command(&child))
|
||||||
|
return "Working directory has unstaged changes";
|
||||||
|
|
||||||
|
child_process_init(&child);
|
||||||
|
child.argv = diff_index;
|
||||||
|
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))
|
||||||
|
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.dir = work_tree;
|
||||||
|
child.no_stdin = 1;
|
||||||
|
child.no_stdout = 1;
|
||||||
|
child.stdout_to_stderr = 0;
|
||||||
|
child.git_cmd = 1;
|
||||||
|
if (run_command(&child))
|
||||||
|
return "Could not update working tree to new HEAD";
|
||||||
|
|
||||||
|
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 : "..";
|
const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
|
||||||
struct argv_array env = ARGV_ARRAY_INIT;
|
struct argv_array env = ARGV_ARRAY_INIT;
|
||||||
struct child_process child = CHILD_PROCESS_INIT;
|
|
||||||
|
|
||||||
if (is_bare_repository())
|
if (is_bare_repository())
|
||||||
return "denyCurrentBranch = updateInstead needs a worktree";
|
return "denyCurrentBranch = updateInstead needs a worktree";
|
||||||
|
|
||||||
argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
|
argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
|
||||||
|
|
||||||
child.argv = update_refresh;
|
if (!find_hook(push_to_checkout_hook))
|
||||||
child.env = env.argv;
|
retval = push_to_deploy(sha1, &env, work_tree);
|
||||||
child.dir = work_tree;
|
else
|
||||||
child.no_stdin = 1;
|
retval = push_to_checkout(sha1, &env, work_tree);
|
||||||
child.stdout_to_stderr = 1;
|
|
||||||
child.git_cmd = 1;
|
|
||||||
if (run_command(&child)) {
|
|
||||||
argv_array_clear(&env);
|
|
||||||
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.dir = work_tree;
|
|
||||||
child.no_stdin = 1;
|
|
||||||
child.stdout_to_stderr = 1;
|
|
||||||
child.git_cmd = 1;
|
|
||||||
if (run_command(&child)) {
|
|
||||||
argv_array_clear(&env);
|
|
||||||
return "Working directory has unstaged changes";
|
|
||||||
}
|
|
||||||
|
|
||||||
child_process_init(&child);
|
|
||||||
child.argv = diff_index;
|
|
||||||
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);
|
|
||||||
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.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);
|
|
||||||
return "Could not update working tree to new HEAD";
|
|
||||||
}
|
|
||||||
|
|
||||||
argv_array_clear(&env);
|
argv_array_clear(&env);
|
||||||
return NULL;
|
return retval;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *update(struct command *cmd, struct shallow_info *si)
|
static const char *update(struct command *cmd, struct shallow_info *si)
|
||||||
|
@ -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
|
test_done
|
||||||
|
Loading…
Reference in New Issue
Block a user