Merge branch 'js/push-to-deploy'
authorJunio C Hamano <gitster@pobox.com>
Mon, 22 Dec 2014 20:27:03 +0000 (12:27 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 22 Dec 2014 20:27:04 +0000 (12:27 -0800)
"git push" into a repository with a working tree normally refuses
to modify the branch that is checked out. The command learned to
optionally do an equivalent of "git reset --hard" only when there
is no change to the working tree and the index instead, which would
be useful to "deploy" by pushing into a repository.

* js/push-to-deploy:
t5516: more tests for receive.denyCurrentBranch=updateInstead
receive-pack: add another option for receive.denyCurrentBranch

Documentation/config.txt
builtin/receive-pack.c
t/t5516-fetch-push.sh
index b563ea8d3477aa686d332a321d684f6659e9fe06..ac781a44d733f672d5b88281a8bea48bc25be226 100644 (file)
@@ -2144,6 +2144,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
index 1eefaddac883399b465dde1601ffbdfc579312c1..8266c1fccf0c0b5908c14a726d443dec36a22d58 100644 (file)
@@ -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";
                        }
                }
        }
index f4da20aa9bf7df1b2ab1fda207b70e1913277603..85c7fecd22a37d06001398883954180a2b1a9868 100755 (executable)
@@ -1330,4 +1330,108 @@ 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 &&
+
+       # Try pushing into a repository with pristine working tree
+       git push testrepo master &&
+       (
+               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 -- &&
+               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 &&
+       (
+               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 fifth = "$(cat path3)"
+       )
+
+'
+
 test_done