Skip to content

Commit

Permalink
Merge branch 'bf/fetch-set-head-config' into seen
Browse files Browse the repository at this point in the history
* bf/fetch-set-head-config:
  fetch: add configuration for set_head behaviour
  • Loading branch information
gitster committed Nov 28, 2024
2 parents 45ee101 + 45f327b commit b3ca3e0
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 6 deletions.
11 changes: 11 additions & 0 deletions Documentation/config/remote.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ remote.<name>.serverOption::
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
Expand Down
46 changes: 40 additions & 6 deletions builtin/fetch.c
Original file line number Diff line number Diff line change
Expand Up @@ -1579,10 +1579,35 @@ static const char *strip_refshead(const char *name){
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;
Expand All @@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs)
string_list_append(&heads, strip_refshead(ref->name));
}

if (follow_remote_head < 0)
goto cleanup;

if (!heads.nr)
result = 1;
Expand All @@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs)
if (!head_name)
goto cleanup;
is_bare = repo_is_bare(the_repository);
create_only = follow_remote_head == 2 ? 0 : !is_bare;
if (is_bare) {
strbuf_addstr(&b_head, "HEAD");
strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
Expand All @@ -1626,16 +1654,22 @@ static int set_head(const struct ref *remote_refs)
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 == 1 && verbosity >= 0)
report_set_head(remote, head_name, &b_local_head, was_detached);

cleanup:
free(head_name);
free_refs(fetch_map);
free_refs(matches);
string_list_clear(&heads, 0);
strbuf_release(&b_head);
strbuf_release(&b_local_head);
strbuf_release(&b_remote_head);
return result;
}
Expand Down Expand Up @@ -1855,7 +1889,7 @@ static int do_fetch(struct transport *transport,
"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
Expand Down
9 changes: 9 additions & 0 deletions remote.c
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,15 @@ static int handle_config(const char *key, const char *value,
} 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 = -1;
else if (!strcmp(value, "create"))
remote->follow_remote_head = 0;
else if (!strcmp(value, "warn"))
remote->follow_remote_head = 1;
else if (!strcmp(value, "always"))
remote->follow_remote_head = 2;
}
return 0;
}
Expand Down
9 changes: 9 additions & 0 deletions remote.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ struct remote {
char *http_proxy_authmethod;

struct string_list server_options;

/*
* The setting for whether to update HEAD for the remote.
* -1 never update
* 0 create only (default)
* 1 warn on change
* 2 always update
*/
int follow_remote_head;
};

/**
Expand Down
102 changes: 102 additions & 0 deletions t/t5510-fetch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,108 @@ test_expect_success "fetch test remote HEAD change" '
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 &&
Expand Down

0 comments on commit b3ca3e0

Please sign in to comment.