Skip to content

Commit b7f7d16

Browse files
ferdinandybgitster
authored andcommitted
fetch: add configuration for set_head behaviour
In the current implementation, if refs/remotes/$remote/HEAD does not exist, running fetch will create it, but if it does exist it will not do anything, which is a somewhat safe and minimal approach. Unfortunately, for users who wish to NOT have refs/remotes/$remote/HEAD set for any reason (e.g. so that `git rev-parse origin` doesn't accidentally point them somewhere they do not want to), there is no way to remove this behaviour. On the other side of the spectrum, users may want fetch to automatically update HEAD or at least give them a warning if something changed on the remote. Introduce a new setting, remote.$remote.followRemoteHEAD with four options: - "never": do not ever do anything, not even create - "create": the current behaviour, now the default behaviour - "warn": print a message if remote and local HEAD is different - "always": silently update HEAD on every change Signed-off-by: Bence Ferdinandy <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 761e62a commit b7f7d16

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
@@ -99,6 +99,108 @@ test_expect_success "fetch test remote HEAD change" '
9999
branch=$(git rev-parse refs/remotes/origin/other) &&
100100
test "z$head" = "z$branch"'
101101

102+
test_expect_success "fetch test followRemoteHEAD never" '
103+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
104+
(
105+
cd "$D" &&
106+
cd two &&
107+
git update-ref --no-deref -d refs/remotes/origin/HEAD &&
108+
git config set remote.origin.followRemoteHEAD "never" &&
109+
git fetch &&
110+
test_must_fail git rev-parse --verify refs/remotes/origin/HEAD
111+
)
112+
'
113+
114+
test_expect_success "fetch test followRemoteHEAD warn no change" '
115+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
116+
(
117+
cd "$D" &&
118+
cd two &&
119+
git rev-parse --verify refs/remotes/origin/other &&
120+
git remote set-head origin other &&
121+
git rev-parse --verify refs/remotes/origin/HEAD &&
122+
git rev-parse --verify refs/remotes/origin/main &&
123+
git config set remote.origin.followRemoteHEAD "warn" &&
124+
git fetch >output &&
125+
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
126+
"but we have ${SQ}other${SQ} locally." >expect &&
127+
echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
128+
test_cmp expect output &&
129+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
130+
branch=$(git rev-parse refs/remotes/origin/other) &&
131+
test "z$head" = "z$branch"
132+
)
133+
'
134+
135+
test_expect_success "fetch test followRemoteHEAD warn create" '
136+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
137+
(
138+
cd "$D" &&
139+
cd two &&
140+
git update-ref --no-deref -d refs/remotes/origin/HEAD &&
141+
git config set remote.origin.followRemoteHEAD "warn" &&
142+
git rev-parse --verify refs/remotes/origin/main &&
143+
output=$(git fetch) &&
144+
test "z" = "z$output" &&
145+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
146+
branch=$(git rev-parse refs/remotes/origin/main) &&
147+
test "z$head" = "z$branch"
148+
)
149+
'
150+
151+
test_expect_success "fetch test followRemoteHEAD warn detached" '
152+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
153+
(
154+
cd "$D" &&
155+
cd two &&
156+
git update-ref --no-deref -d refs/remotes/origin/HEAD &&
157+
git update-ref refs/remotes/origin/HEAD HEAD &&
158+
HEAD=$(git log --pretty="%H") &&
159+
git config set remote.origin.followRemoteHEAD "warn" &&
160+
git fetch >output &&
161+
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
162+
"but we have a detached HEAD pointing to" \
163+
"${SQ}${HEAD}${SQ} locally." >expect &&
164+
echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
165+
test_cmp expect output
166+
)
167+
'
168+
169+
test_expect_success "fetch test followRemoteHEAD warn quiet" '
170+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
171+
(
172+
cd "$D" &&
173+
cd two &&
174+
git rev-parse --verify refs/remotes/origin/other &&
175+
git remote set-head origin other &&
176+
git rev-parse --verify refs/remotes/origin/HEAD &&
177+
git rev-parse --verify refs/remotes/origin/main &&
178+
git config set remote.origin.followRemoteHEAD "warn" &&
179+
output=$(git fetch --quiet) &&
180+
test "z" = "z$output" &&
181+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
182+
branch=$(git rev-parse refs/remotes/origin/other) &&
183+
test "z$head" = "z$branch"
184+
)
185+
'
186+
187+
test_expect_success "fetch test followRemoteHEAD always" '
188+
test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
189+
(
190+
cd "$D" &&
191+
cd two &&
192+
git rev-parse --verify refs/remotes/origin/other &&
193+
git remote set-head origin other &&
194+
git rev-parse --verify refs/remotes/origin/HEAD &&
195+
git rev-parse --verify refs/remotes/origin/main &&
196+
git config set remote.origin.followRemoteHEAD "always" &&
197+
git fetch &&
198+
head=$(git rev-parse refs/remotes/origin/HEAD) &&
199+
branch=$(git rev-parse refs/remotes/origin/main) &&
200+
test "z$head" = "z$branch"
201+
)
202+
'
203+
102204
test_expect_success 'fetch --prune on its own works as expected' '
103205
cd "$D" &&
104206
git clone . prune &&

0 commit comments

Comments
 (0)