diff --git a/Documentation/config/remote.txt b/Documentation/config/remote.txt index 6d8b7d6c637eaa..024f92befcc540 100644 --- a/Documentation/config/remote.txt +++ b/Documentation/config/remote.txt @@ -101,6 +101,17 @@ remote..serverOption:: The default set of server options used when fetching from this remote. These server options can be overridden by the `--server-option=` command line arguments. + +remote..followRemoteHEAD:: + How linkgit:git-fetch[1] should handle updates to `remotes//HEAD`. + The default value is "create", which will create `remotes//HEAD` + if it exists on the remote, but not locally, but will not touch an + already existing local reference. Setting to "warn" will print + a message if the remote has a different value, than the local one and + in case there is no local reference, it behaves like "create". Setting + to "always" will silently update it to the value on the remote. + Finally, setting it to "never" will never change or create the local + reference. + This is a multi-valued variable, and an empty value can be used in a higher priority configuration file (e.g. `.git/config` in a repository) to clear diff --git a/builtin/fetch.c b/builtin/fetch.c index b3f12abeba7b79..a5b36efbc960d9 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1579,10 +1579,35 @@ static const char *strip_refshead(const char *name){ return name; } -static int set_head(const struct ref *remote_refs) +static void report_set_head(const char *remote, const char *head_name, + struct strbuf *buf_prev, int updateres) { + struct strbuf buf_prefix = STRBUF_INIT; + const char *prev_head = NULL; + + strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote); + skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head); + + if (prev_head && strcmp(prev_head, head_name)) { + printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n", + remote, head_name, prev_head); + printf("Run 'git remote set-head %s %s' to follow the change.\n", + remote, head_name); + } + else if (updateres && buf_prev->len) { + printf("'HEAD' at '%s' is '%s', " + "but we have a detached HEAD pointing to '%s' locally.\n", + remote, head_name, buf_prev->buf); + printf("Run 'git remote set-head %s %s' to follow the change.\n", + remote, head_name); + } + strbuf_release(&buf_prefix); +} + +static int set_head(const struct ref *remote_refs, int follow_remote_head) { - int result = 0, is_bare; - struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT; + int result = 0, create_only, is_bare, was_detached; + struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT, + b_local_head = STRBUF_INIT; const char *remote = gtransport->remote->name; char *head_name = NULL; struct ref *ref, *matches; @@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs) string_list_append(&heads, strip_refshead(ref->name)); } + if (follow_remote_head < 0) + goto cleanup; if (!heads.nr) result = 1; @@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs) if (!head_name) goto cleanup; is_bare = repo_is_bare(the_repository); + create_only = follow_remote_head == 2 ? 0 : !is_bare; if (is_bare) { strbuf_addstr(&b_head, "HEAD"); strbuf_addf(&b_remote_head, "refs/heads/%s", head_name); @@ -1626,9 +1654,14 @@ static int set_head(const struct ref *remote_refs) result = 1; goto cleanup; } - if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf, - "fetch", NULL, !is_bare)) + was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf, + "fetch", &b_local_head, create_only); + if (was_detached == -1) { result = 1; + goto cleanup; + } + if (follow_remote_head == 1 && verbosity >= 0) + report_set_head(remote, head_name, &b_local_head, was_detached); cleanup: free(head_name); @@ -1636,6 +1669,7 @@ static int set_head(const struct ref *remote_refs) free_refs(matches); string_list_clear(&heads, 0); strbuf_release(&b_head); + strbuf_release(&b_local_head); strbuf_release(&b_remote_head); return result; } @@ -1855,7 +1889,7 @@ static int do_fetch(struct transport *transport, "you need to specify exactly one branch with the --set-upstream option")); } } - if (set_head(remote_refs)) + if (set_head(remote_refs, transport->remote->follow_remote_head)) ; /* * Way too many cases where this can go wrong diff --git a/remote.c b/remote.c index 10104d11e3cba1..5a768ddac27bac 100644 --- a/remote.c +++ b/remote.c @@ -514,6 +514,15 @@ static int handle_config(const char *key, const char *value, } else if (!strcmp(subkey, "serveroption")) { return parse_transport_option(key, value, &remote->server_options); + } else if (!strcmp(subkey, "followremotehead")) { + if (!strcmp(value, "never")) + remote->follow_remote_head = -1; + else if (!strcmp(value, "create")) + remote->follow_remote_head = 0; + else if (!strcmp(value, "warn")) + remote->follow_remote_head = 1; + else if (!strcmp(value, "always")) + remote->follow_remote_head = 2; } return 0; } diff --git a/remote.h b/remote.h index a7e5c4e07c53ce..3ceadac82008ff 100644 --- a/remote.h +++ b/remote.h @@ -107,6 +107,15 @@ struct remote { char *http_proxy_authmethod; struct string_list server_options; + + /* + * The setting for whether to update HEAD for the remote. + * -1 never update + * 0 create only (default) + * 1 warn on change + * 2 always update + */ + int follow_remote_head; }; /** diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index 6b6d9d9b586f02..f75273b1c04e08 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -98,6 +98,108 @@ test_expect_success "fetch test remote HEAD change" ' branch=$(git rev-parse refs/remotes/origin/other) && test "z$head" = "z$branch"' +test_expect_success "fetch test followRemoteHEAD never" ' + test_when_finished "git config unset remote.origin.followRemoteHEAD" && + ( + cd "$D" && + cd two && + git update-ref --no-deref -d refs/remotes/origin/HEAD && + git config set remote.origin.followRemoteHEAD "never" && + git fetch && + test_must_fail git rev-parse --verify refs/remotes/origin/HEAD + ) +' + +test_expect_success "fetch test followRemoteHEAD warn no change" ' + test_when_finished "git config unset remote.origin.followRemoteHEAD" && + ( + cd "$D" && + cd two && + git rev-parse --verify refs/remotes/origin/other && + git remote set-head origin other && + git rev-parse --verify refs/remotes/origin/HEAD && + git rev-parse --verify refs/remotes/origin/main && + git config set remote.origin.followRemoteHEAD "warn" && + git fetch >output && + echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \ + "but we have ${SQ}other${SQ} locally." >expect && + echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect && + test_cmp expect output && + head=$(git rev-parse refs/remotes/origin/HEAD) && + branch=$(git rev-parse refs/remotes/origin/other) && + test "z$head" = "z$branch" + ) +' + +test_expect_success "fetch test followRemoteHEAD warn create" ' + test_when_finished "git config unset remote.origin.followRemoteHEAD" && + ( + cd "$D" && + cd two && + git update-ref --no-deref -d refs/remotes/origin/HEAD && + git config set remote.origin.followRemoteHEAD "warn" && + git rev-parse --verify refs/remotes/origin/main && + output=$(git fetch) && + test "z" = "z$output" && + head=$(git rev-parse refs/remotes/origin/HEAD) && + branch=$(git rev-parse refs/remotes/origin/main) && + test "z$head" = "z$branch" + ) +' + +test_expect_success "fetch test followRemoteHEAD warn detached" ' + test_when_finished "git config unset remote.origin.followRemoteHEAD" && + ( + cd "$D" && + cd two && + git update-ref --no-deref -d refs/remotes/origin/HEAD && + git update-ref refs/remotes/origin/HEAD HEAD && + HEAD=$(git log --pretty="%H") && + git config set remote.origin.followRemoteHEAD "warn" && + git fetch >output && + echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \ + "but we have a detached HEAD pointing to" \ + "${SQ}${HEAD}${SQ} locally." >expect && + echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect && + test_cmp expect output + ) +' + +test_expect_success "fetch test followRemoteHEAD warn quiet" ' + test_when_finished "git config unset remote.origin.followRemoteHEAD" && + ( + cd "$D" && + cd two && + git rev-parse --verify refs/remotes/origin/other && + git remote set-head origin other && + git rev-parse --verify refs/remotes/origin/HEAD && + git rev-parse --verify refs/remotes/origin/main && + git config set remote.origin.followRemoteHEAD "warn" && + output=$(git fetch --quiet) && + test "z" = "z$output" && + head=$(git rev-parse refs/remotes/origin/HEAD) && + branch=$(git rev-parse refs/remotes/origin/other) && + test "z$head" = "z$branch" + ) +' + +test_expect_success "fetch test followRemoteHEAD always" ' + test_when_finished "git config unset remote.origin.followRemoteHEAD" && + ( + cd "$D" && + cd two && + git rev-parse --verify refs/remotes/origin/other && + git remote set-head origin other && + git rev-parse --verify refs/remotes/origin/HEAD && + git rev-parse --verify refs/remotes/origin/main && + git config set remote.origin.followRemoteHEAD "always" && + git fetch && + head=$(git rev-parse refs/remotes/origin/HEAD) && + branch=$(git rev-parse refs/remotes/origin/main) && + test "z$head" = "z$branch" + ) +' + test_expect_success 'fetch --prune on its own works as expected' ' cd "$D" && git clone . prune &&