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.<name>.followRemoteHEAD::
+ How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`.
+ The default value is "create", which will create `remotes/<name>/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
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;
string_list_append(&heads, strip_refshead(ref->name));
}
+ if (follow_remote_head == FOLLOW_REMOTE_NEVER)
+ goto cleanup;
if (!heads.nr)
result = 1;
if (!head_name)
goto cleanup;
is_bare = is_bare_repository();
+ create_only = follow_remote_head == FOLLOW_REMOTE_ALWAYS ? 0 : !is_bare;
if (is_bare) {
strbuf_addstr(&b_head, "HEAD");
strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
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 == FOLLOW_REMOTE_WARN && verbosity >= 0)
+ report_set_head(remote, head_name, &b_local_head, was_detached);
cleanup:
free(head_name);
free_refs(matches);
string_list_clear(&heads, 0);
strbuf_release(&b_head);
+ strbuf_release(&b_local_head);
strbuf_release(&b_remote_head);
return result;
}
"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
} 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 = FOLLOW_REMOTE_NEVER;
+ else if (!strcmp(value, "create"))
+ remote->follow_remote_head = FOLLOW_REMOTE_CREATE;
+ else if (!strcmp(value, "warn"))
+ remote->follow_remote_head = FOLLOW_REMOTE_WARN;
+ else if (!strcmp(value, "always"))
+ remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
}
return 0;
}
void remote_state_clear(struct remote_state *remote_state);
struct remote_state *remote_state_new(void);
+ enum follow_remote_head_settings {
+ FOLLOW_REMOTE_NEVER = -1,
+ FOLLOW_REMOTE_CREATE = 0,
+ FOLLOW_REMOTE_WARN = 1,
+ FOLLOW_REMOTE_ALWAYS = 2,
+ };
+
struct remote {
struct hashmap_entry ent;
char *http_proxy_authmethod;
struct string_list server_options;
+
+ enum follow_remote_head_settings follow_remote_head;
};
/**
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 &&