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 <[email protected]>
---
Documentation/config.txt | 7 ++++
builtin/receive-pack.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++--
t/t5516-fetch-push.sh | 21 +++++++++++
3 files changed, 120 insertions(+), 2 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 9220725..0519073 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 e908d07..7ff8be5 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,90 @@ 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", "--ignore-submodules", "--refresh", "-q", 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", sha1_to_hex(sha1), 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 not clean";
+ }
+
+ /* run_command() does not clean up completely; reinitialize */
+ 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 not clean";
+ }
+
+ /* run_command() does not clean up completely; reinitialize */
+ 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 +842,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 +871,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..b8df39c 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1330,4 +1330,25 @@ 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 --refresh &&
+ git diff-files --quiet &&
+ git diff-index --cached HEAD -- &&
+ echo changed > path2 &&
+ git add path2
+ ) &&
+ test_commit fourth path2 &&
+ test_must_fail git push testrepo master
+'
+
test_done
--
2.0.0.rc3.9669.g840d1f9