Skip to content

Commit a1f34d5

Browse files
committed
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
2 parents ae75cef + 012bc56 commit a1f34d5

File tree

9 files changed

+258
-7
lines changed

9 files changed

+258
-7
lines changed

Documentation/config/remote.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,19 @@ remote.<name>.serverOption::
101101
The default set of server options used when fetching from this remote.
102102
These server options can be overridden by the `--server-option=` command
103103
line arguments.
104+
105+
remote.<name>.followRemoteHEAD::
106+
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`.
107+
The default value is "create", which will create `remotes/<name>/HEAD`
108+
if it exists on the remote, but not locally, but will not touch an
109+
already existing local reference. Setting to "warn" will print
110+
a message if the remote has a different value, than the local one and
111+
in case there is no local reference, it behaves like "create".
112+
A variant on "warn" is "warn-if-not-$branch", which behaves like
113+
"warn", but if `HEAD` on the remote is `$branch` it will be silent.
114+
Setting to "always" will silently update it to the value on the remote.
115+
Finally, setting it to "never" will never change or create the local
116+
reference.
104117
+
105118
This is a multi-valued variable, and an empty value can be used in a higher
106119
priority configuration file (e.g. `.git/config` in a repository) to clear

advice.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ static struct {
5353
[ADVICE_COMMIT_BEFORE_MERGE] = { "commitBeforeMerge" },
5454
[ADVICE_DETACHED_HEAD] = { "detachedHead" },
5555
[ADVICE_DIVERGING] = { "diverging" },
56+
[ADVICE_FETCH_SET_HEAD_WARN] = { "fetchRemoteHEADWarn" },
5657
[ADVICE_FETCH_SHOW_FORCED_UPDATES] = { "fetchShowForcedUpdates" },
5758
[ADVICE_FORCE_DELETE_BRANCH] = { "forceDeleteBranch" },
5859
[ADVICE_GRAFT_FILE_DEPRECATED] = { "graftFileDeprecated" },

advice.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ enum advice_type {
2020
ADVICE_COMMIT_BEFORE_MERGE,
2121
ADVICE_DETACHED_HEAD,
2222
ADVICE_DIVERGING,
23+
ADVICE_FETCH_SET_HEAD_WARN,
2324
ADVICE_FETCH_SHOW_FORCED_UPDATES,
2425
ADVICE_FORCE_DELETE_BRANCH,
2526
ADVICE_GRAFT_FILE_DEPRECATED,

builtin/fetch.c

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,10 +1579,47 @@ static const char *strip_refshead(const char *name){
15791579
return name;
15801580
}
15811581

1582-
static int set_head(const struct ref *remote_refs)
1582+
static void set_head_advice_msg(const char *remote, const char *head_name)
15831583
{
1584-
int result = 0, is_bare;
1585-
struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
1584+
const char message_advice_set_head[] =
1585+
N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
1586+
"'remote.%s.followRemoteHEAD' configuration option to a different value\n"
1587+
"if you do not want to see this message. Specifically running\n"
1588+
"'git config set remote.%s.followRemoteHEAD %s' will disable the warning\n"
1589+
"until the remote changes HEAD to something else.");
1590+
1591+
advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
1592+
remote, head_name, remote, remote, head_name);
1593+
}
1594+
1595+
static void report_set_head(const char *remote, const char *head_name,
1596+
struct strbuf *buf_prev, int updateres) {
1597+
struct strbuf buf_prefix = STRBUF_INIT;
1598+
const char *prev_head = NULL;
1599+
1600+
strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
1601+
skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
1602+
1603+
if (prev_head && strcmp(prev_head, head_name)) {
1604+
printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
1605+
remote, head_name, prev_head);
1606+
set_head_advice_msg(remote, head_name);
1607+
}
1608+
else if (updateres && buf_prev->len) {
1609+
printf("'HEAD' at '%s' is '%s', "
1610+
"but we have a detached HEAD pointing to '%s' locally.\n",
1611+
remote, head_name, buf_prev->buf);
1612+
set_head_advice_msg(remote, head_name);
1613+
}
1614+
strbuf_release(&buf_prefix);
1615+
}
1616+
1617+
static int set_head(const struct ref *remote_refs, int follow_remote_head,
1618+
const char *no_warn_branch)
1619+
{
1620+
int result = 0, create_only, is_bare, was_detached;
1621+
struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
1622+
b_local_head = STRBUF_INIT;
15861623
const char *remote = gtransport->remote->name;
15871624
char *head_name = NULL;
15881625
struct ref *ref, *matches;
@@ -1603,6 +1640,8 @@ static int set_head(const struct ref *remote_refs)
16031640
string_list_append(&heads, strip_refshead(ref->name));
16041641
}
16051642

1643+
if (follow_remote_head == FOLLOW_REMOTE_NEVER)
1644+
goto cleanup;
16061645

16071646
if (!heads.nr)
16081647
result = 1;
@@ -1614,6 +1653,7 @@ static int set_head(const struct ref *remote_refs)
16141653
if (!head_name)
16151654
goto cleanup;
16161655
is_bare = is_bare_repository();
1656+
create_only = follow_remote_head == FOLLOW_REMOTE_ALWAYS ? 0 : !is_bare;
16171657
if (is_bare) {
16181658
strbuf_addstr(&b_head, "HEAD");
16191659
strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
@@ -1626,16 +1666,24 @@ static int set_head(const struct ref *remote_refs)
16261666
result = 1;
16271667
goto cleanup;
16281668
}
1629-
if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
1630-
"fetch", NULL, !is_bare))
1669+
was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
1670+
"fetch", &b_local_head, create_only);
1671+
if (was_detached == -1) {
16311672
result = 1;
1673+
goto cleanup;
1674+
}
1675+
if (verbosity >= 0 &&
1676+
follow_remote_head == FOLLOW_REMOTE_WARN &&
1677+
(!no_warn_branch || strcmp(no_warn_branch, head_name)))
1678+
report_set_head(remote, head_name, &b_local_head, was_detached);
16321679

16331680
cleanup:
16341681
free(head_name);
16351682
free_refs(fetch_map);
16361683
free_refs(matches);
16371684
string_list_clear(&heads, 0);
16381685
strbuf_release(&b_head);
1686+
strbuf_release(&b_local_head);
16391687
strbuf_release(&b_remote_head);
16401688
return result;
16411689
}
@@ -1873,7 +1921,8 @@ static int do_fetch(struct transport *transport,
18731921
"you need to specify exactly one branch with the --set-upstream option"));
18741922
}
18751923
}
1876-
if (set_head(remote_refs))
1924+
if (set_head(remote_refs, transport->remote->follow_remote_head,
1925+
transport->remote->no_warn_branch))
18771926
;
18781927
/*
18791928
* Way too many cases where this can go wrong

builtin/remote.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,7 @@ static int set_head(int argc, const char **argv, const char *prefix,
14381438
b_local_head = STRBUF_INIT;
14391439
char *head_name = NULL;
14401440
struct ref_store *refs = get_main_ref_store(the_repository);
1441+
struct remote *remote;
14411442

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

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

14921502
cleanup:
14931503
free(head_name);

remote.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,24 @@ static int handle_config(const char *key, const char *value,
514514
} else if (!strcmp(subkey, "serveroption")) {
515515
return parse_transport_option(key, value,
516516
&remote->server_options);
517+
} else if (!strcmp(subkey, "followremotehead")) {
518+
const char *no_warn_branch;
519+
if (!strcmp(value, "never"))
520+
remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
521+
else if (!strcmp(value, "create"))
522+
remote->follow_remote_head = FOLLOW_REMOTE_CREATE;
523+
else if (!strcmp(value, "warn")) {
524+
remote->follow_remote_head = FOLLOW_REMOTE_WARN;
525+
remote->no_warn_branch = NULL;
526+
} else if (skip_prefix(value, "warn-if-not-", &no_warn_branch)) {
527+
remote->follow_remote_head = FOLLOW_REMOTE_WARN;
528+
remote->no_warn_branch = no_warn_branch;
529+
} else if (!strcmp(value, "always")) {
530+
remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
531+
} else {
532+
warning(_("unrecognized followRemoteHEAD value '%s' ignored"),
533+
value);
534+
}
517535
}
518536
return 0;
519537
}

remote.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ struct remote_state {
5959
void remote_state_clear(struct remote_state *remote_state);
6060
struct remote_state *remote_state_new(void);
6161

62+
enum follow_remote_head_settings {
63+
FOLLOW_REMOTE_NEVER = -1,
64+
FOLLOW_REMOTE_CREATE = 0,
65+
FOLLOW_REMOTE_WARN = 1,
66+
FOLLOW_REMOTE_ALWAYS = 2,
67+
};
68+
6269
struct remote {
6370
struct hashmap_entry ent;
6471

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

109116
struct string_list server_options;
117+
118+
enum follow_remote_head_settings follow_remote_head;
119+
const char *no_warn_branch;
110120
};
111121

112122
/**

t/t5505-remote.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,17 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
504504
)
505505
'
506506

507+
test_expect_success 'set-head changes followRemoteHEAD always to warn' '
508+
(
509+
cd test &&
510+
git config set remote.origin.followRemoteHEAD "always" &&
511+
git remote set-head --auto origin &&
512+
git config get remote.origin.followRemoteHEAD >actual &&
513+
echo "warn" >expect &&
514+
test_cmp expect actual
515+
)
516+
'
517+
507518
cat >test/expect <<\EOF
508519
refs/remotes/origin/side2
509520
EOF

t/t5510-fetch.sh

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,144 @@ test_expect_success "fetch test remote HEAD change" '
9898
branch=$(git rev-parse refs/remotes/origin/other) &&
9999
test "z$head" = "z$branch"'
100100

101+
test_expect_success "fetch test followRemoteHEAD never" '
102+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
103+
(
104+
cd "$D" &&
105+
cd two &&
106+
git update-ref --no-deref -d refs/remotes/origin/HEAD &&
107+
git config set remote.origin.followRemoteHEAD "never" &&
108+
git fetch &&
109+
test_must_fail git rev-parse --verify refs/remotes/origin/HEAD
110+
)
111+
'
112+
113+
test_expect_success "fetch test followRemoteHEAD warn no change" '
114+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
115+
(
116+
cd "$D" &&
117+
cd two &&
118+
git rev-parse --verify refs/remotes/origin/other &&
119+
git remote set-head origin other &&
120+
git rev-parse --verify refs/remotes/origin/HEAD &&
121+
git rev-parse --verify refs/remotes/origin/main &&
122+
git config set remote.origin.followRemoteHEAD "warn" &&
123+
git fetch >output &&
124+
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
125+
"but we have ${SQ}other${SQ} locally." >expect &&
126+
test_cmp expect output &&
127+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
128+
branch=$(git rev-parse refs/remotes/origin/other) &&
129+
test "z$head" = "z$branch"
130+
)
131+
'
132+
133+
test_expect_success "fetch test followRemoteHEAD warn create" '
134+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
135+
(
136+
cd "$D" &&
137+
cd two &&
138+
git update-ref --no-deref -d refs/remotes/origin/HEAD &&
139+
git config set remote.origin.followRemoteHEAD "warn" &&
140+
git rev-parse --verify refs/remotes/origin/main &&
141+
output=$(git fetch) &&
142+
test "z" = "z$output" &&
143+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
144+
branch=$(git rev-parse refs/remotes/origin/main) &&
145+
test "z$head" = "z$branch"
146+
)
147+
'
148+
149+
test_expect_success "fetch test followRemoteHEAD warn detached" '
150+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
151+
(
152+
cd "$D" &&
153+
cd two &&
154+
git update-ref --no-deref -d refs/remotes/origin/HEAD &&
155+
git update-ref refs/remotes/origin/HEAD HEAD &&
156+
HEAD=$(git log --pretty="%H") &&
157+
git config set remote.origin.followRemoteHEAD "warn" &&
158+
git fetch >output &&
159+
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
160+
"but we have a detached HEAD pointing to" \
161+
"${SQ}${HEAD}${SQ} locally." >expect &&
162+
test_cmp expect output
163+
)
164+
'
165+
166+
test_expect_success "fetch test followRemoteHEAD warn quiet" '
167+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
168+
(
169+
cd "$D" &&
170+
cd two &&
171+
git rev-parse --verify refs/remotes/origin/other &&
172+
git remote set-head origin other &&
173+
git rev-parse --verify refs/remotes/origin/HEAD &&
174+
git rev-parse --verify refs/remotes/origin/main &&
175+
git config set remote.origin.followRemoteHEAD "warn" &&
176+
output=$(git fetch --quiet) &&
177+
test "z" = "z$output" &&
178+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
179+
branch=$(git rev-parse refs/remotes/origin/other) &&
180+
test "z$head" = "z$branch"
181+
)
182+
'
183+
184+
test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is same" '
185+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
186+
(
187+
cd "$D" &&
188+
cd two &&
189+
git rev-parse --verify refs/remotes/origin/other &&
190+
git remote set-head origin other &&
191+
git rev-parse --verify refs/remotes/origin/HEAD &&
192+
git rev-parse --verify refs/remotes/origin/main &&
193+
git config set remote.origin.followRemoteHEAD "warn-if-not-main" &&
194+
actual=$(git fetch) &&
195+
test "z" = "z$actual" &&
196+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
197+
branch=$(git rev-parse refs/remotes/origin/other) &&
198+
test "z$head" = "z$branch"
199+
)
200+
'
201+
202+
test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is different" '
203+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
204+
(
205+
cd "$D" &&
206+
cd two &&
207+
git rev-parse --verify refs/remotes/origin/other &&
208+
git remote set-head origin other &&
209+
git rev-parse --verify refs/remotes/origin/HEAD &&
210+
git rev-parse --verify refs/remotes/origin/main &&
211+
git config set remote.origin.followRemoteHEAD "warn-if-not-some/different-branch" &&
212+
git fetch >actual &&
213+
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
214+
"but we have ${SQ}other${SQ} locally." >expect &&
215+
test_cmp expect actual &&
216+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
217+
branch=$(git rev-parse refs/remotes/origin/other) &&
218+
test "z$head" = "z$branch"
219+
)
220+
'
221+
222+
test_expect_success "fetch test followRemoteHEAD always" '
223+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
224+
(
225+
cd "$D" &&
226+
cd two &&
227+
git rev-parse --verify refs/remotes/origin/other &&
228+
git remote set-head origin other &&
229+
git rev-parse --verify refs/remotes/origin/HEAD &&
230+
git rev-parse --verify refs/remotes/origin/main &&
231+
git config set remote.origin.followRemoteHEAD "always" &&
232+
git fetch &&
233+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
234+
branch=$(git rev-parse refs/remotes/origin/main) &&
235+
test "z$head" = "z$branch"
236+
)
237+
'
238+
101239
test_expect_success 'fetch --prune on its own works as expected' '
102240
cd "$D" &&
103241
git clone . prune &&

0 commit comments

Comments
 (0)