Skip to content

Commit 69bfc59

Browse files
committed
Merge branch 'bf/fetch-set-head-config' (early part) into next
* 'bf/fetch-set-head-config' (early part): fetch: add configuration for set_head behaviour
2 parents 3c1d2e2 + b7f7d16 commit 69bfc59

File tree

5 files changed

+171
-6
lines changed

5 files changed

+171
-6
lines changed

Documentation/config/remote.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ 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". Setting
112+
to "always" will silently update it to the value on the remote.
113+
Finally, setting it to "never" will never change or create the local
114+
reference.
104115
+
105116
This is a multi-valued variable, and an empty value can be used in a higher
106117
priority configuration file (e.g. `.git/config` in a repository) to clear

builtin/fetch.c

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,10 +1579,35 @@ 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 report_set_head(const char *remote, const char *head_name,
1583+
struct strbuf *buf_prev, int updateres) {
1584+
struct strbuf buf_prefix = STRBUF_INIT;
1585+
const char *prev_head = NULL;
1586+
1587+
strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
1588+
skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
1589+
1590+
if (prev_head && strcmp(prev_head, head_name)) {
1591+
printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
1592+
remote, head_name, prev_head);
1593+
printf("Run 'git remote set-head %s %s' to follow the change.\n",
1594+
remote, head_name);
1595+
}
1596+
else if (updateres && buf_prev->len) {
1597+
printf("'HEAD' at '%s' is '%s', "
1598+
"but we have a detached HEAD pointing to '%s' locally.\n",
1599+
remote, head_name, buf_prev->buf);
1600+
printf("Run 'git remote set-head %s %s' to follow the change.\n",
1601+
remote, head_name);
1602+
}
1603+
strbuf_release(&buf_prefix);
1604+
}
1605+
1606+
static int set_head(const struct ref *remote_refs, int follow_remote_head)
15831607
{
1584-
int result = 0, is_bare;
1585-
struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
1608+
int result = 0, create_only, is_bare, was_detached;
1609+
struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
1610+
b_local_head = STRBUF_INIT;
15861611
const char *remote = gtransport->remote->name;
15871612
char *head_name = NULL;
15881613
struct ref *ref, *matches;
@@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs)
16031628
string_list_append(&heads, strip_refshead(ref->name));
16041629
}
16051630

1631+
if (follow_remote_head == FOLLOW_REMOTE_NEVER)
1632+
goto cleanup;
16061633

16071634
if (!heads.nr)
16081635
result = 1;
@@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs)
16141641
if (!head_name)
16151642
goto cleanup;
16161643
is_bare = is_bare_repository();
1644+
create_only = follow_remote_head == FOLLOW_REMOTE_ALWAYS ? 0 : !is_bare;
16171645
if (is_bare) {
16181646
strbuf_addstr(&b_head, "HEAD");
16191647
strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
@@ -1626,16 +1654,22 @@ static int set_head(const struct ref *remote_refs)
16261654
result = 1;
16271655
goto cleanup;
16281656
}
1629-
if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
1630-
"fetch", NULL, !is_bare))
1657+
was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
1658+
"fetch", &b_local_head, create_only);
1659+
if (was_detached == -1) {
16311660
result = 1;
1661+
goto cleanup;
1662+
}
1663+
if (follow_remote_head == FOLLOW_REMOTE_WARN && verbosity >= 0)
1664+
report_set_head(remote, head_name, &b_local_head, was_detached);
16321665

16331666
cleanup:
16341667
free(head_name);
16351668
free_refs(fetch_map);
16361669
free_refs(matches);
16371670
string_list_clear(&heads, 0);
16381671
strbuf_release(&b_head);
1672+
strbuf_release(&b_local_head);
16391673
strbuf_release(&b_remote_head);
16401674
return result;
16411675
}
@@ -1855,7 +1889,7 @@ static int do_fetch(struct transport *transport,
18551889
"you need to specify exactly one branch with the --set-upstream option"));
18561890
}
18571891
}
1858-
if (set_head(remote_refs))
1892+
if (set_head(remote_refs, transport->remote->follow_remote_head))
18591893
;
18601894
/*
18611895
* Way too many cases where this can go wrong

remote.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,15 @@ 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+
if (!strcmp(value, "never"))
519+
remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
520+
else if (!strcmp(value, "create"))
521+
remote->follow_remote_head = FOLLOW_REMOTE_CREATE;
522+
else if (!strcmp(value, "warn"))
523+
remote->follow_remote_head = FOLLOW_REMOTE_WARN;
524+
else if (!strcmp(value, "always"))
525+
remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
517526
}
518527
return 0;
519528
}

remote.h

Lines changed: 9 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,8 @@ struct remote {
107114
char *http_proxy_authmethod;
108115

109116
struct string_list server_options;
117+
118+
enum follow_remote_head_settings follow_remote_head;
110119
};
111120

112121
/**

t/t5510-fetch.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,108 @@ 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+
echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
127+
test_cmp expect output &&
128+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
129+
branch=$(git rev-parse refs/remotes/origin/other) &&
130+
test "z$head" = "z$branch"
131+
)
132+
'
133+
134+
test_expect_success "fetch test followRemoteHEAD warn create" '
135+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
136+
(
137+
cd "$D" &&
138+
cd two &&
139+
git update-ref --no-deref -d refs/remotes/origin/HEAD &&
140+
git config set remote.origin.followRemoteHEAD "warn" &&
141+
git rev-parse --verify refs/remotes/origin/main &&
142+
output=$(git fetch) &&
143+
test "z" = "z$output" &&
144+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
145+
branch=$(git rev-parse refs/remotes/origin/main) &&
146+
test "z$head" = "z$branch"
147+
)
148+
'
149+
150+
test_expect_success "fetch test followRemoteHEAD warn detached" '
151+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
152+
(
153+
cd "$D" &&
154+
cd two &&
155+
git update-ref --no-deref -d refs/remotes/origin/HEAD &&
156+
git update-ref refs/remotes/origin/HEAD HEAD &&
157+
HEAD=$(git log --pretty="%H") &&
158+
git config set remote.origin.followRemoteHEAD "warn" &&
159+
git fetch >output &&
160+
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
161+
"but we have a detached HEAD pointing to" \
162+
"${SQ}${HEAD}${SQ} locally." >expect &&
163+
echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
164+
test_cmp expect output
165+
)
166+
'
167+
168+
test_expect_success "fetch test followRemoteHEAD warn quiet" '
169+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
170+
(
171+
cd "$D" &&
172+
cd two &&
173+
git rev-parse --verify refs/remotes/origin/other &&
174+
git remote set-head origin other &&
175+
git rev-parse --verify refs/remotes/origin/HEAD &&
176+
git rev-parse --verify refs/remotes/origin/main &&
177+
git config set remote.origin.followRemoteHEAD "warn" &&
178+
output=$(git fetch --quiet) &&
179+
test "z" = "z$output" &&
180+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
181+
branch=$(git rev-parse refs/remotes/origin/other) &&
182+
test "z$head" = "z$branch"
183+
)
184+
'
185+
186+
test_expect_success "fetch test followRemoteHEAD always" '
187+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
188+
(
189+
cd "$D" &&
190+
cd two &&
191+
git rev-parse --verify refs/remotes/origin/other &&
192+
git remote set-head origin other &&
193+
git rev-parse --verify refs/remotes/origin/HEAD &&
194+
git rev-parse --verify refs/remotes/origin/main &&
195+
git config set remote.origin.followRemoteHEAD "always" &&
196+
git fetch &&
197+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
198+
branch=$(git rev-parse refs/remotes/origin/main) &&
199+
test "z$head" = "z$branch"
200+
)
201+
'
202+
101203
test_expect_success 'fetch --prune on its own works as expected' '
102204
cd "$D" &&
103205
git clone . prune &&

0 commit comments

Comments
 (0)