receive-pack: add another option for receive.denyCurrentBranch

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 <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed Nov 26, 2014 at 23:44 UTC 1404bcbb6b3bdb248d32024430644e55faec91ce
3 files changed +124 -2
Documentation/config.txt
+7
@@ -2129,6 +2129,13 @@ receive.denyCurrentBranch::
2129 print a warning of such a push to stderr, but allow the push to
2130 proceed. If set to false or "ignore", allow such pushes with no
2131 message. Defaults to "refuse".
2132 ++
2133 +Another option is "updateInstead" which will update the working
2134 +directory (must be clean) if pushing into the current branch. This option is
2135 +intended for synchronizing working directories when one side is not easily
2136 +accessible via interactive ssh (e.g. a live web site, hence the requirement
2137 +that the working directory be clean). This mode also comes in handy when
2138 +developing inside a VM to test and fix code on different Operating Systems.
2139
2140 receive.denyNonFastForwards::
2141 If set to true, git-receive-pack will deny a ref update which is
builtin/receive-pack.c
+91 -2
@@ -26,7 +26,8 @@ enum deny_action {
26 DENY_UNCONFIGURED,
27 DENY_IGNORE,
28 DENY_WARN,
29 - DENY_REFUSE
29 + DENY_REFUSE,
30 + DENY_UPDATE_INSTEAD
31 };
32
33 static int deny_deletes;
@@ -76,6 +77,8 @@ static enum deny_action parse_deny_action(const char *var, const char *value)
77 return DENY_WARN;
78 if (!strcasecmp(value, "refuse"))
79 return DENY_REFUSE;
80 + if (!strcasecmp(value, "updateinstead"))
81 + return DENY_UPDATE_INSTEAD;
82 }
83 if (git_config_bool(var, value))
84 return DENY_REFUSE;
@@ -730,11 +733,89 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
733 return 0;
734 }
735
736 +static const char *update_worktree(unsigned char *sha1)
737 +{
738 + const char *update_refresh[] = {
739 + "update-index", "-q", "--ignore-submodules", "--refresh", NULL
740 + };
741 + const char *diff_files[] = {
742 + "diff-files", "--quiet", "--ignore-submodules", "--", NULL
743 + };
744 + const char *diff_index[] = {
745 + "diff-index", "--quiet", "--cached", "--ignore-submodules",
746 + "HEAD", "--", NULL
747 + };
748 + const char *read_tree[] = {
749 + "read-tree", "-u", "-m", NULL, NULL
750 + };
751 + const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : "..";
752 + struct argv_array env = ARGV_ARRAY_INIT;
753 + struct child_process child = CHILD_PROCESS_INIT;
754 +
755 + if (is_bare_repository())
756 + return "denyCurrentBranch = updateInstead needs a worktree";
757 +
758 + argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir()));
759 +
760 + child.argv = update_refresh;
761 + child.env = env.argv;
762 + child.dir = work_tree;
763 + child.no_stdin = 1;
764 + child.stdout_to_stderr = 1;
765 + child.git_cmd = 1;
766 + if (run_command(&child)) {
767 + argv_array_clear(&env);
768 + return "Up-to-date check failed";
769 + }
770 +
771 + /* run_command() does not clean up completely; reinitialize */
772 + child_process_init(&child);
773 + child.argv = diff_files;
774 + child.env = env.argv;
775 + child.dir = work_tree;
776 + child.no_stdin = 1;
777 + child.stdout_to_stderr = 1;
778 + child.git_cmd = 1;
779 + if (run_command(&child)) {
780 + argv_array_clear(&env);
781 + return "Working directory has unstaged changes";
782 + }
783 +
784 + child_process_init(&child);
785 + child.argv = diff_index;
786 + child.env = env.argv;
787 + child.no_stdin = 1;
788 + child.no_stdout = 1;
789 + child.stdout_to_stderr = 0;
790 + child.git_cmd = 1;
791 + if (run_command(&child)) {
792 + argv_array_clear(&env);
793 + return "Working directory has staged changes";
794 + }
795 +
796 + read_tree[3] = sha1_to_hex(sha1);
797 + child_process_init(&child);
798 + child.argv = read_tree;
799 + child.env = env.argv;
800 + child.dir = work_tree;
801 + child.no_stdin = 1;
802 + child.no_stdout = 1;
803 + child.stdout_to_stderr = 0;
804 + child.git_cmd = 1;
805 + if (run_command(&child)) {
806 + argv_array_clear(&env);
807 + return "Could not update working tree to new HEAD";
808 + }
809 +
810 + argv_array_clear(&env);
811 + return NULL;
812 +}
813 +
814 static const char *update(struct command *cmd, struct shallow_info *si)
815 {
816 const char *name = cmd->ref_name;
817 struct strbuf namespaced_name_buf = STRBUF_INIT;
737 - const char *namespaced_name;
818 + const char *namespaced_name, *ret;
819 unsigned char *old_sha1 = cmd->old_sha1;
820 unsigned char *new_sha1 = cmd->new_sha1;
821
@@ -760,6 +841,11 @@ static const char *update(struct command *cmd, struct shallow_info *si)
841 if (deny_current_branch == DENY_UNCONFIGURED)
842 refuse_unconfigured_deny();
843 return "branch is currently checked out";
844 + case DENY_UPDATE_INSTEAD:
845 + ret = update_worktree(new_sha1);
846 + if (ret)
847 + return ret;
848 + break;
849 }
850 }
851
@@ -784,10 +870,13 @@ static const char *update(struct command *cmd, struct shallow_info *si)
870 break;
871 case DENY_REFUSE:
872 case DENY_UNCONFIGURED:
873 + case DENY_UPDATE_INSTEAD:
874 if (deny_delete_current == DENY_UNCONFIGURED)
875 refuse_unconfigured_deny_delete_current();
876 rp_error("refusing to delete the current branch: %s", name);
877 return "deletion of the current branch prohibited";
878 + default:
879 + return "Invalid denyDeleteCurrent setting";
880 }
881 }
882 }
t/t5516-fetch-push.sh
+26
@@ -1330,4 +1330,30 @@ test_expect_success 'fetch into bare respects core.logallrefupdates' '
1330 )
1331 '
1332
1333 +test_expect_success 'receive.denyCurrentBranch = updateInstead' '
1334 + git push testrepo master &&
1335 + (cd testrepo &&
1336 + git reset --hard &&
1337 + git config receive.denyCurrentBranch updateInstead
1338 + ) &&
1339 + test_commit third path2 &&
1340 + git push testrepo master &&
1341 + test $(git rev-parse HEAD) = $(cd testrepo && git rev-parse HEAD) &&
1342 + test third = "$(cat testrepo/path2)" &&
1343 + (cd testrepo &&
1344 + git update-index -q --refresh &&
1345 + git diff-files --quiet -- &&
1346 + git diff-index --quiet --cached HEAD -- &&
1347 + echo changed >path2 &&
1348 + git add path2
1349 + ) &&
1350 + test_commit fourth path2 &&
1351 + test_must_fail git push testrepo master &&
1352 + test $(git rev-parse HEAD^) = $(git -C testrepo rev-parse HEAD) &&
1353 + (cd testrepo &&
1354 + git diff --quiet &&
1355 + test changed = "$(cat path2)"
1356 + )
1357 +'
1358 +
1359 test_done