From 1404bcbb6b3bdb248d32024430644e55faec91ce Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 26 Nov 2014 23:44:16 +0100 Subject: receive-pack: add another option for receive.denyCurrentBranch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When synchronizing between working directories, it can be handy to update the current branch via 'push' rather than 'pull', e.g. when pushing a fix from inside a VM, or when pushing a fix made on a user's machine (where the developer is not at liberty to install an ssh daemon let alone know the user's password). The common workaround – pushing into a temporary branch and then merging on the other machine – is no longer necessary with this patch. The new option is: 'updateInstead': Update the working tree accordingly, but refuse to do so if there are any uncommitted changes. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/Documentation/config.txt b/Documentation/config.txt index e8dd76d..6a4c19e 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -2129,6 +2129,13 @@ receive.denyCurrentBranch:: print a warning of such a push to stderr, but allow the push to proceed. If set to false or "ignore", allow such pushes with no 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 +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. receive.denyNonFastForwards:: If set to true, git-receive-pack will deny a ref update which is diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 32fc540..c047418 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -26,7 +26,8 @@ enum deny_action { DENY_UNCONFIGURED, DENY_IGNORE, DENY_WARN, - DENY_REFUSE + DENY_REFUSE, + DENY_UPDATE_INSTEAD }; static int deny_deletes; @@ -76,6 +77,8 @@ static enum deny_action parse_deny_action(const char *var, const char *value) return DENY_WARN; if (!strcasecmp(value, "refuse")) return DENY_REFUSE; + if (!strcasecmp(value, "updateinstead")) + return DENY_UPDATE_INSTEAD; } if (git_config_bool(var, value)) return DENY_REFUSE; @@ -730,11 +733,89 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si) return 0; } +static const char *update_worktree(unsigned char *sha1) +{ + const char *update_refresh[] = { + "update-index", "-q", "--ignore-submodules", "--refresh", NULL + }; + const char *diff_files[] = { + "diff-files", "--quiet", "--ignore-submodules", "--", NULL + }; + const char *diff_index[] = { + "diff-index", "--quiet", "--cached", "--ignore-submodules", + "HEAD", "--", NULL + }; + const char *read_tree[] = { + "read-tree", "-u", "-m", NULL, NULL + }; + const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : ".."; + struct argv_array env = ARGV_ARRAY_INIT; + struct child_process child = CHILD_PROCESS_INIT; + + if (is_bare_repository()) + return "denyCurrentBranch = updateInstead needs a worktree"; + + argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir())); + + 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)) { + 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); + return NULL; +} + static const char *update(struct command *cmd, struct shallow_info *si) { const char *name = cmd->ref_name; struct strbuf namespaced_name_buf = STRBUF_INIT; - const char *namespaced_name; + const char *namespaced_name, *ret; unsigned char *old_sha1 = cmd->old_sha1; unsigned char *new_sha1 = cmd->new_sha1; @@ -760,6 +841,11 @@ static const char *update(struct command *cmd, struct shallow_info *si) if (deny_current_branch == DENY_UNCONFIGURED) refuse_unconfigured_deny(); return "branch is currently checked out"; + case DENY_UPDATE_INSTEAD: + ret = update_worktree(new_sha1); + if (ret) + return ret; + break; } } @@ -784,10 +870,13 @@ static const char *update(struct command *cmd, struct shallow_info *si) break; case DENY_REFUSE: case DENY_UNCONFIGURED: + case DENY_UPDATE_INSTEAD: if (deny_delete_current == DENY_UNCONFIGURED) refuse_unconfigured_deny_delete_current(); rp_error("refusing to delete the current branch: %s", name); return "deletion of the current branch prohibited"; + default: + return "Invalid denyDeleteCurrent setting"; } } } diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index f4da20a..7b353d0 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -1330,4 +1330,30 @@ test_expect_success 'fetch into bare respects core.logallrefupdates' ' ) ' +test_expect_success 'receive.denyCurrentBranch = updateInstead' ' + git push testrepo master && + (cd testrepo && + git reset --hard && + git config receive.denyCurrentBranch updateInstead + ) && + test_commit third path2 && + git push testrepo master && + test $(git rev-parse HEAD) = $(cd testrepo && git rev-parse HEAD) && + test third = "$(cat testrepo/path2)" && + (cd testrepo && + git update-index -q --refresh && + git diff-files --quiet -- && + git diff-index --quiet --cached HEAD -- && + echo changed >path2 && + git add path2 + ) && + test_commit fourth path2 && + test_must_fail git push testrepo master && + test $(git rev-parse HEAD^) = $(git -C testrepo rev-parse HEAD) && + (cd testrepo && + git diff --quiet && + test changed = "$(cat path2)" + ) +' + test_done -- cgit v0.10.2-6-g49f6 From 4d7a5ceacc97e69c4ab5e1543f61fafafb963a9c Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Nov 2014 17:54:30 -0800 Subject: t5516: more tests for receive.denyCurrentBranch=updateInstead The previous one tests only the case where a path to be updated by the push-to-deploy has an incompatible change in the target's working tree that has already been added to the index, but the feature itself wants to require the working tree to be a lot cleaner than what is tested. Add a handful more tests to protect the feature from future changes that mistakenly (from the viewpoint of the inventor of the feature) loosens the cleanliness requirement, namely: - A change only to the working tree but not to the index is still a change to be protected; - An untracked file in the working tree that would be overwritten by a push-to-deploy needs to be protected; - A change that happens to make a file identical to what is being pushed is still a change to be protected (i.e. the feature's cleanliness requirement is more strict than that of checkout). Also, test that a stat-only change to the working tree is not a reason to reject a push-to-deploy. Signed-off-by: Junio C Hamano diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 7b353d0..85c7fec 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -1332,28 +1332,106 @@ test_expect_success 'fetch into bare respects core.logallrefupdates' ' test_expect_success 'receive.denyCurrentBranch = updateInstead' ' git push testrepo master && - (cd testrepo && + ( + cd testrepo && git reset --hard && git config receive.denyCurrentBranch updateInstead ) && test_commit third path2 && + + # Try pushing into a repository with pristine working tree git push testrepo master && - test $(git rev-parse HEAD) = $(cd testrepo && git rev-parse HEAD) && - test third = "$(cat testrepo/path2)" && - (cd testrepo && + ( + cd testrepo && + git update-index -q --refresh && + git diff-files --quiet -- && + git diff-index --quiet --cached HEAD -- && + test third = "$(cat path2)" && + test $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD) + ) && + + # Try pushing into a repository with working tree needing a refresh + ( + cd testrepo && + git reset --hard HEAD^ && + test $(git -C .. rev-parse HEAD^) = $(git rev-parse HEAD) && + test-chmtime +100 path1 + ) && + git push testrepo master && + ( + cd testrepo && git update-index -q --refresh && git diff-files --quiet -- && git diff-index --quiet --cached HEAD -- && - echo changed >path2 && - git add path2 + test_cmp ../path1 path1 && + test third = "$(cat path2)" && + test $(git -C .. rev-parse HEAD) = $(git rev-parse HEAD) ) && + + # Update what is to be pushed test_commit fourth path2 && + + # Try pushing into a repository with a dirty working tree + # (1) the working tree updated + ( + cd testrepo && + echo changed >path1 + ) && test_must_fail git push testrepo master && - test $(git rev-parse HEAD^) = $(git -C testrepo rev-parse HEAD) && - (cd testrepo && + ( + cd testrepo && + test $(git -C .. rev-parse HEAD^) = $(git rev-parse HEAD) && + git diff --quiet --cached && + test changed = "$(cat path1)" + ) && + + # (2) the index updated + ( + cd testrepo && + echo changed >path1 && + git add path1 + ) && + test_must_fail git push testrepo master && + ( + cd testrepo && + test $(git -C .. rev-parse HEAD^) = $(git rev-parse HEAD) && + git diff --quiet && + test changed = "$(cat path1)" + ) && + + # Introduce a new file in the update + test_commit fifth path3 && + + # (3) the working tree has an untracked file that would interfere + ( + cd testrepo && + git reset --hard && + echo changed >path3 + ) && + test_must_fail git push testrepo master && + ( + cd testrepo && + test $(git -C .. rev-parse HEAD^^) = $(git rev-parse HEAD) && + git diff --quiet && + git diff --quiet --cached && + test changed = "$(cat path3)" + ) && + + # (4) the target changes to what gets pushed but it still is a change + ( + cd testrepo && + git reset --hard && + echo fifth >path3 && + git add path3 + ) && + test_must_fail git push testrepo master && + ( + cd testrepo && + test $(git -C .. rev-parse HEAD^^) = $(git rev-parse HEAD) && git diff --quiet && - test changed = "$(cat path2)" + test fifth = "$(cat path3)" ) + ' test_done -- cgit v0.10.2-6-g49f6