Merge branch 'bf/fetch-set-head-config'

"git fetch" honors "remote.<remote>.followRemoteHEAD" settings to
tweak the remote-tracking HEAD in "refs/remotes/<remote>/HEAD".

* bf/fetch-set-head-config:
  remote set-head: set followRemoteHEAD to "warn" if "always"
  fetch set_head: add warn-if-not-$branch option
  fetch set_head: move warn advice into advise_if_enabled
  fetch: add configuration for set_head behaviour
maint
Junio C Hamano 2024-12-19 10:58:29 -08:00
commit a1f34d5955
9 changed files with 258 additions and 7 deletions

View File

@ -101,6 +101,19 @@ remote.<name>.serverOption::
The default set of server options used when fetching from this remote. The default set of server options used when fetching from this remote.
These server options can be overridden by the `--server-option=` command These server options can be overridden by the `--server-option=` command
line arguments. 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".
A variant on "warn" is "warn-if-not-$branch", which behaves like
"warn", but if `HEAD` on the remote is `$branch` it will be silent.
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 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 priority configuration file (e.g. `.git/config` in a repository) to clear

View File

@ -53,6 +53,7 @@ static struct {
[ADVICE_COMMIT_BEFORE_MERGE] = { "commitBeforeMerge" }, [ADVICE_COMMIT_BEFORE_MERGE] = { "commitBeforeMerge" },
[ADVICE_DETACHED_HEAD] = { "detachedHead" }, [ADVICE_DETACHED_HEAD] = { "detachedHead" },
[ADVICE_DIVERGING] = { "diverging" }, [ADVICE_DIVERGING] = { "diverging" },
[ADVICE_FETCH_SET_HEAD_WARN] = { "fetchRemoteHEADWarn" },
[ADVICE_FETCH_SHOW_FORCED_UPDATES] = { "fetchShowForcedUpdates" }, [ADVICE_FETCH_SHOW_FORCED_UPDATES] = { "fetchShowForcedUpdates" },
[ADVICE_FORCE_DELETE_BRANCH] = { "forceDeleteBranch" }, [ADVICE_FORCE_DELETE_BRANCH] = { "forceDeleteBranch" },
[ADVICE_GRAFT_FILE_DEPRECATED] = { "graftFileDeprecated" }, [ADVICE_GRAFT_FILE_DEPRECATED] = { "graftFileDeprecated" },

View File

@ -20,6 +20,7 @@ enum advice_type {
ADVICE_COMMIT_BEFORE_MERGE, ADVICE_COMMIT_BEFORE_MERGE,
ADVICE_DETACHED_HEAD, ADVICE_DETACHED_HEAD,
ADVICE_DIVERGING, ADVICE_DIVERGING,
ADVICE_FETCH_SET_HEAD_WARN,
ADVICE_FETCH_SHOW_FORCED_UPDATES, ADVICE_FETCH_SHOW_FORCED_UPDATES,
ADVICE_FORCE_DELETE_BRANCH, ADVICE_FORCE_DELETE_BRANCH,
ADVICE_GRAFT_FILE_DEPRECATED, ADVICE_GRAFT_FILE_DEPRECATED,

View File

@ -1579,10 +1579,47 @@ static const char *strip_refshead(const char *name){
return name; return name;
} }


static int set_head(const struct ref *remote_refs) static void set_head_advice_msg(const char *remote, const char *head_name)
{ {
int result = 0, is_bare; const char message_advice_set_head[] =
struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT; N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
"'remote.%s.followRemoteHEAD' configuration option to a different value\n"
"if you do not want to see this message. Specifically running\n"
"'git config set remote.%s.followRemoteHEAD %s' will disable the warning\n"
"until the remote changes HEAD to something else.");

advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
remote, head_name, remote, remote, head_name);
}

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);
set_head_advice_msg(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);
set_head_advice_msg(remote, head_name);
}
strbuf_release(&buf_prefix);
}

static int set_head(const struct ref *remote_refs, int follow_remote_head,
const char *no_warn_branch)
{
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; const char *remote = gtransport->remote->name;
char *head_name = NULL; char *head_name = NULL;
struct ref *ref, *matches; struct ref *ref, *matches;
@ -1603,6 +1640,8 @@ static int set_head(const struct ref *remote_refs)
string_list_append(&heads, strip_refshead(ref->name)); string_list_append(&heads, strip_refshead(ref->name));
} }


if (follow_remote_head == FOLLOW_REMOTE_NEVER)
goto cleanup;


if (!heads.nr) if (!heads.nr)
result = 1; result = 1;
@ -1614,6 +1653,7 @@ static int set_head(const struct ref *remote_refs)
if (!head_name) if (!head_name)
goto cleanup; goto cleanup;
is_bare = is_bare_repository(); is_bare = is_bare_repository();
create_only = follow_remote_head == FOLLOW_REMOTE_ALWAYS ? 0 : !is_bare;
if (is_bare) { if (is_bare) {
strbuf_addstr(&b_head, "HEAD"); strbuf_addstr(&b_head, "HEAD");
strbuf_addf(&b_remote_head, "refs/heads/%s", head_name); strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
@ -1626,9 +1666,16 @@ static int set_head(const struct ref *remote_refs)
result = 1; result = 1;
goto cleanup; goto cleanup;
} }
if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf, was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
"fetch", NULL, !is_bare)) "fetch", &b_local_head, create_only);
if (was_detached == -1) {
result = 1; result = 1;
goto cleanup;
}
if (verbosity >= 0 &&
follow_remote_head == FOLLOW_REMOTE_WARN &&
(!no_warn_branch || strcmp(no_warn_branch, head_name)))
report_set_head(remote, head_name, &b_local_head, was_detached);


cleanup: cleanup:
free(head_name); free(head_name);
@ -1636,6 +1683,7 @@ cleanup:
free_refs(matches); free_refs(matches);
string_list_clear(&heads, 0); string_list_clear(&heads, 0);
strbuf_release(&b_head); strbuf_release(&b_head);
strbuf_release(&b_local_head);
strbuf_release(&b_remote_head); strbuf_release(&b_remote_head);
return result; return result;
} }
@ -1873,7 +1921,8 @@ static int do_fetch(struct transport *transport,
"you need to specify exactly one branch with the --set-upstream option")); "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,
transport->remote->no_warn_branch))
; ;
/* /*
* Way too many cases where this can go wrong * Way too many cases where this can go wrong

View File

@ -1438,6 +1438,7 @@ static int set_head(int argc, const char **argv, const char *prefix,
b_local_head = STRBUF_INIT; b_local_head = STRBUF_INIT;
char *head_name = NULL; char *head_name = NULL;
struct ref_store *refs = get_main_ref_store(the_repository); struct ref_store *refs = get_main_ref_store(the_repository);
struct remote *remote;


struct option options[] = { struct option options[] = {
OPT_BOOL('a', "auto", &opt_a, OPT_BOOL('a', "auto", &opt_a,
@ -1448,8 +1449,10 @@ static int set_head(int argc, const char **argv, const char *prefix,
}; };
argc = parse_options(argc, argv, prefix, options, argc = parse_options(argc, argv, prefix, options,
builtin_remote_sethead_usage, 0); builtin_remote_sethead_usage, 0);
if (argc) if (argc) {
strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]); strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
remote = remote_get(argv[0]);
}


if (!opt_a && !opt_d && argc == 2) { if (!opt_a && !opt_d && argc == 2) {
head_name = xstrdup(argv[1]); head_name = xstrdup(argv[1]);
@ -1488,6 +1491,13 @@ static int set_head(int argc, const char **argv, const char *prefix,
} }
if (opt_a) if (opt_a)
report_set_head_auto(argv[0], head_name, &b_local_head, was_detached); report_set_head_auto(argv[0], head_name, &b_local_head, was_detached);
if (remote->follow_remote_head == FOLLOW_REMOTE_ALWAYS) {
struct strbuf config_name = STRBUF_INIT;
strbuf_addf(&config_name,
"remote.%s.followremotehead", remote->name);
git_config_set(config_name.buf, "warn");
strbuf_release(&config_name);
}


cleanup: cleanup:
free(head_name); free(head_name);

View File

@ -514,6 +514,24 @@ static int handle_config(const char *key, const char *value,
} else if (!strcmp(subkey, "serveroption")) { } else if (!strcmp(subkey, "serveroption")) {
return parse_transport_option(key, value, return parse_transport_option(key, value,
&remote->server_options); &remote->server_options);
} else if (!strcmp(subkey, "followremotehead")) {
const char *no_warn_branch;
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;
remote->no_warn_branch = NULL;
} else if (skip_prefix(value, "warn-if-not-", &no_warn_branch)) {
remote->follow_remote_head = FOLLOW_REMOTE_WARN;
remote->no_warn_branch = no_warn_branch;
} else if (!strcmp(value, "always")) {
remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
} else {
warning(_("unrecognized followRemoteHEAD value '%s' ignored"),
value);
}
} }
return 0; return 0;
} }

View File

@ -59,6 +59,13 @@ struct remote_state {
void remote_state_clear(struct remote_state *remote_state); void remote_state_clear(struct remote_state *remote_state);
struct remote_state *remote_state_new(void); 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 remote {
struct hashmap_entry ent; struct hashmap_entry ent;


@ -107,6 +114,9 @@ struct remote {
char *http_proxy_authmethod; char *http_proxy_authmethod;


struct string_list server_options; struct string_list server_options;

enum follow_remote_head_settings follow_remote_head;
const char *no_warn_branch;
}; };


/** /**

View File

@ -504,6 +504,17 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
) )
' '


test_expect_success 'set-head changes followRemoteHEAD always to warn' '
(
cd test &&
git config set remote.origin.followRemoteHEAD "always" &&
git remote set-head --auto origin &&
git config get remote.origin.followRemoteHEAD >actual &&
echo "warn" >expect &&
test_cmp expect actual
)
'

cat >test/expect <<\EOF cat >test/expect <<\EOF
refs/remotes/origin/side2 refs/remotes/origin/side2
EOF EOF

View File

@ -98,6 +98,144 @@ test_expect_success "fetch test remote HEAD change" '
branch=$(git rev-parse refs/remotes/origin/other) && branch=$(git rev-parse refs/remotes/origin/other) &&
test "z$head" = "z$branch"' 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 &&
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 &&
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 warn-if-not-branch branch is same" '
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-if-not-main" &&
actual=$(git fetch) &&
test "z" = "z$actual" &&
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-if-not-branch branch is different" '
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-if-not-some/different-branch" &&
git fetch >actual &&
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
"but we have ${SQ}other${SQ} locally." >expect &&
test_cmp expect actual &&
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' ' test_expect_success 'fetch --prune on its own works as expected' '
cd "$D" && cd "$D" &&
git clone . prune && git clone . prune &&