Skip to contentSkip to conversation

fetch: add config to avoid fetching every branch in shallow repo

PATCH In review Harald Nordgren via GitGitGadget 5 participants 132 messages Sep 19 – Nov 11 Page 1 of 2

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

PATCH 1/1 fetch: add config to avoid fetching every branch in shallow repo

From: Harald Nordgren <[email protected]>

In a shallow, sparsely checked out clone of a repository with many branches, plain git pull can take minutes or hang outright, even though only one branch is actually being worked on.

Add fetch.shallow, off by default. When enabled, a fetch or pull for a shallow repository that isn't already scoped to specific refs fetches only the current branch's tracked upstream, instead of every branch the remote has. git pull ultimately runs such a fetch under the hood, so this fixes pull the same way. It has no effect once the repository is no longer shallow, and no effect on a fetch of a remote the current branch doesn't track, both fall back to the existing behavior.

This is opt-in rather than automatic because it changes what a plain fetch or pull leaves in refs/remotes/<name>/ for anyone who currently relies on it syncing every branch of a shallow remote, not just the one they are on. Scoping remote.<name>.fetch by hand already covers this for a single remote, but that requires knowing the config exists and applies it permanently, even to branches that are not currently checked out.

The remote's recorded default branch (remotes/<name>/HEAD) is kept up to date the same way it always is, only the other branches are skipped.

---
fetch: add fetch.shallow so pull doesn't fetch every branch on shallow repo

Add fetch.shallow config for big shallow repo, so git fetch/pull doesn't hang by fetching every branch.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v1
Pull-Request: https://github.com/git/git/pull/2412

 Documentation/config/fetch.adoc |  15 ++++
 builtin/fetch.c                 |  18 +++-
 t/t5537-fetch-shallow.sh        | 146 ++++++++++++++++++++++++++++++++
 3 files changed, 175 insertions(+), 4 deletions(-)
Documentation/config/fetch.adoc +15 -0
index 00435e9a16..e63e54a463 100644
@@ -145,3 +145,18 @@ remove the value for the `fetch.bundleCreationToken` value before fetching.
 `never`;;
 	Never create or modify the `remotes/<name>/HEAD` symbolic-ref.
 --
+
+`fetch.shallow`::
+	If true, and the repository is a shallow repository (see
+	linkgit:git-clone[1] `--depth`), a fetch or `git pull` that names no
+	explicit refspec and would otherwise fall back to the remote's
+	configured `remote.<name>.fetch` refspec instead fetches only the
+	current branch's upstream, when that upstream is on the remote being
+	fetched. This avoids negotiating history for every branch the remote
+	advertises, which can be slow on a shallow repository that tracks
+	many disjoint shallow histories. It has no effect on a fetch that
+	names an explicit remote or refspec, and no effect on a repository
+	that is not shallow. Defaults to false.
++
+`remotes/<name>/HEAD` is still kept up to date per `fetch.followRemoteHEAD`
+while this is in effect, only the other branches are skipped.
builtin/fetch.c +14 -4
index 533fdfe7d8..b22f7fe5f4 100644
@@ -111,6 +111,7 @@ struct fetch_config {
 	int recurse_submodules;
 	int parallel;
 	int submodule_fetch_jobs;
+	int shallow;
 };
 
 static int git_fetch_config(const char *k, const char *v,
@@ -175,6 +176,11 @@ static int git_fetch_config(const char *k, const char *v,
 		return 0;
 	}
 
+	if (!strcmp(k, "fetch.shallow")) {
+		fetch_config->shallow = git_config_bool(k, v);
+		return 0;
+	}
+
 	if (!strcmp(k, "fetch.followremotehead")) {
 		if (!v)
 			return config_error_nonbool(k);
@@ -1958,15 +1964,19 @@ static int do_fetch(struct transport *transport,
 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
 	} else {
 		struct branch *branch = branch_get(NULL);
+		int tracks_this_remote = branch && branch_has_merge_config(branch) &&
+			!strcmp(branch->remote_name, transport->remote->name);
+		int narrow_to_tracked_ref = config->shallow &&
+			is_repository_shallow(the_repository) && tracks_this_remote;
 
 		if (transport->remote->fetch.nr) {
-			refspec_ref_prefixes(&transport->remote->fetch,
-					     &transport_ls_refs_options.ref_prefixes);
+			if (!narrow_to_tracked_ref)
+				refspec_ref_prefixes(&transport->remote->fetch,
+						     &transport_ls_refs_options.ref_prefixes);
 			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
 				do_set_head = 1;
 		}
-		if (branch && branch_has_merge_config(branch) &&
-		    !strcmp(branch->remote_name, transport->remote->name)) {
+		if (tracks_this_remote) {
 			int i;
 			for (i = 0; i < branch->merge_nr; i++) {
 				strvec_push(&transport_ls_refs_options.ref_prefixes,
t/t5537-fetch-shallow.sh +146 -0

158 lines not on this page · raw message

base-commit: d38352cd43ab9745686d697872408bc3249a153f

Signature · 1 line
gitgitgadget
#1 lore raw

Hi Harald

Quoted 5 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

In a shallow, sparsely checked out clone of a repository with many branches, plain git pull can take minutes or hang outright, even though only one branch is actually being worked on.

I think the sparse checkout is irrelevant? It is unclear to me if this is talking about a case where there are many branches in the remote repository and only one of them was cloned, then adding a second remote created a wildcard fetch refspec; or if there are intentionally lots of remote tracking branches in the local repository and you don't want to wait for them all to update. If it is the former then we should think how we can improve the behavior of "git remote add" in a sparse repository to prevent it adding a wildcard fetch refspec and instead setup the new remote to fetch only the branch(es) we're interested in.

Thanks

Phillip

Quoted 275 lines

Add fetch.shallow, off by default. When enabled, a fetch or pull for a shallow repository that isn't already scoped to specific refs fetches only the current branch's tracked upstream, instead of every branch the remote has. git pull ultimately runs such a fetch under the hood, so this fixes pull the same way. It has no effect once the repository is no longer shallow, and no effect on a fetch of a remote the current branch doesn't track, both fall back to the existing behavior.

This is opt-in rather than automatic because it changes what a plain fetch or pull leaves in refs/remotes/<name>/ for anyone who currently relies on it syncing every branch of a shallow remote, not just the one they are on. Scoping remote.<name>.fetch by hand already covers this for a single remote, but that requires knowing the config exists and applies it permanently, even to branches that are not currently checked out.

The remote's recorded default branch (remotes/<name>/HEAD) is kept up to date the same way it always is, only the other branches are skipped.

---
fetch: add fetch.shallow so pull doesn't fetch every branch on shallow repo

Add fetch.shallow config for big shallow repo, so git fetch/pull doesn't hang by fetching every branch.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v1
Pull-Request: https://github.com/git/git/pull/2412

  Documentation/config/fetch.adoc |  15 ++++
  builtin/fetch.c                 |  18 +++-
  t/t5537-fetch-shallow.sh        | 146 ++++++++++++++++++++++++++++++++
  3 files changed, 175 insertions(+), 4 deletions(-)
Documentation/config/fetch.adoc +15 -0
index 00435e9a16..e63e54a463 100644
@@ -145,3 +145,18 @@ remove the value for the `fetch.bundleCreationToken` value before fetching.
  `never`;;
  	Never create or modify the `remotes/<name>/HEAD` symbolic-ref.
  --
+
+`fetch.shallow`::
+	If true, and the repository is a shallow repository (see
+	linkgit:git-clone[1] `--depth`), a fetch or `git pull` that names no
+	explicit refspec and would otherwise fall back to the remote's
+	configured `remote.<name>.fetch` refspec instead fetches only the
+	current branch's upstream, when that upstream is on the remote being
+	fetched. This avoids negotiating history for every branch the remote
+	advertises, which can be slow on a shallow repository that tracks
+	many disjoint shallow histories. It has no effect on a fetch that
+	names an explicit remote or refspec, and no effect on a repository
+	that is not shallow. Defaults to false.
++
+`remotes/<name>/HEAD` is still kept up to date per `fetch.followRemoteHEAD`
+while this is in effect, only the other branches are skipped.
builtin/fetch.c +14 -4
index 533fdfe7d8..b22f7fe5f4 100644
@@ -111,6 +111,7 @@ struct fetch_config {
  	int recurse_submodules;
  	int parallel;
  	int submodule_fetch_jobs;
+	int shallow;
  };
 
  static int git_fetch_config(const char *k, const char *v,
@@ -175,6 +176,11 @@ static int git_fetch_config(const char *k, const char *v,
  		return 0;
  	}
 
+	if (!strcmp(k, "fetch.shallow")) {
+		fetch_config->shallow = git_config_bool(k, v);
+		return 0;
+	}
+
  	if (!strcmp(k, "fetch.followremotehead")) {
  		if (!v)
  			return config_error_nonbool(k);
@@ -1958,15 +1964,19 @@ static int do_fetch(struct transport *transport,
  		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
  	} else {
  		struct branch *branch = branch_get(NULL);
+		int tracks_this_remote = branch && branch_has_merge_config(branch) &&
+			!strcmp(branch->remote_name, transport->remote->name);
+		int narrow_to_tracked_ref = config->shallow &&
+			is_repository_shallow(the_repository) && tracks_this_remote;
 
  		if (transport->remote->fetch.nr) {
-			refspec_ref_prefixes(&transport->remote->fetch,
-					     &transport_ls_refs_options.ref_prefixes);
+			if (!narrow_to_tracked_ref)
+				refspec_ref_prefixes(&transport->remote->fetch,
+						     &transport_ls_refs_options.ref_prefixes);
  			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
  				do_set_head = 1;
  		}
-		if (branch && branch_has_merge_config(branch) &&
-		    !strcmp(branch->remote_name, transport->remote->name)) {
+		if (tracks_this_remote) {
  			int i;
  			for (i = 0; i < branch->merge_nr; i++) {
  				strvec_push(&transport_ls_refs_options.ref_prefixes,
t/t5537-fetch-shallow.sh +146 -0
index f323ceebd2..8143af9fc3 100755
@@ -13,6 +13,24 @@ commit() {
  	git commit -m "$1"
  }
 
+check_upstream_refs () {
+	git for-each-ref --format="%(refname)" refs/remotes/upstream/ >actual &&
+	cat >expect &&
+	test_cmp expect actual
+}
+
+check_upstream_head () {
+	git symbolic-ref refs/remotes/upstream/HEAD >actual &&
+	echo "refs/remotes/upstream/$1" >expect &&
+	test_cmp expect actual
+}
+
+check_same_tip () {
+	git log --oneline -1 "$1" >expect &&
+	git -C "$2" log --oneline -1 "$3" >actual &&
+	test_cmp expect actual
+}
+
  test_expect_success 'setup' '
  	commit 1 &&
  	commit 2 &&
@@ -261,6 +279,134 @@ test_expect_success 'fetch --deepen does not truncate' '
  	test_cmp expect actual
  '
 
+test_expect_success 'fetch.shallow setup' '
+	git branch narrow-side &&
+	git clone --no-local --depth=1 --branch main --single-branch \
+		.git narrow-default &&
+	git clone --no-local --depth=1 --branch main --single-branch \
+		.git narrow-enabled &&
+	(
+		cd narrow-default &&
+		git remote add upstream ../.git &&
+		git fetch --depth=1 upstream main:refs/remotes/upstream/main &&
+		git branch --set-upstream-to=upstream/main main
+	) &&
+	(
+		cd narrow-enabled &&
+		git remote add upstream ../.git &&
+		git fetch --depth=1 upstream main:refs/remotes/upstream/main &&
+		git branch --set-upstream-to=upstream/main main &&
+		git config fetch.shallow true
+	)
+'
+
+test_expect_success 'a refspec-less fetch expands to the configured refspec by default' '
+	(
+		cd narrow-default &&
+		git fetch upstream &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		refs/remotes/upstream/narrow-side
+		EOF
+	)
+'
+
+test_expect_success 'fetch.shallow=true limits a refspec-less fetch to the tracked branch' '
+	(
+		cd narrow-enabled &&
+		git fetch upstream &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		EOF
+	)
+'
+
+test_expect_success 'fetch.shallow=true still creates refs/remotes/<remote>/HEAD' '
+	(
+		cd narrow-enabled &&
+		git symbolic-ref -d refs/remotes/upstream/HEAD &&
+		git fetch upstream &&
+		check_upstream_head main
+	)
+'
+
+test_expect_success 'fetch.shallow=true with followRemoteHEAD=always corrects a stale HEAD' '
+	test_when_finished \
+		"git -C narrow-enabled update-ref -d refs/remotes/upstream/stale-branch" &&
+	(
+		cd narrow-enabled &&
+		git update-ref refs/remotes/upstream/stale-branch refs/remotes/upstream/main &&
+		git symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/stale-branch &&
+		git -c fetch.followRemoteHEAD=always fetch upstream &&
+		check_upstream_head main
+	)
+'
+
+test_expect_success 'fetch.shallow=true still updates the tracked branch' '
+	commit 5 &&
+	git -C narrow-enabled fetch upstream &&
+	check_same_tip main narrow-enabled refs/remotes/upstream/main
+'
+
+test_expect_success 'fetch.shallow=true keeps git pull narrowed too' '
+	test_when_finished "git branch -D narrow-side" &&
+	commit 6 &&
+	(
+		cd narrow-enabled &&
+		git pull &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		EOF
+	) &&
+	check_same_tip main narrow-enabled HEAD
+'
+
+test_expect_success 'fetch.shallow=true has no effect on a non-shallow repository' '
+	git clone --no-local --branch main --single-branch .git narrow-full &&
+	(
+		cd narrow-full &&
+		git rev-parse --is-shallow-repository >actual &&
+		echo false >expect &&
+		test_cmp expect actual &&
+		git remote add upstream ../.git &&
+		git fetch upstream &&
+		git branch --set-upstream-to=upstream/main main &&
+		git config fetch.shallow true
+	) &&
+	test_when_finished "git branch -D narrow-full-side" &&
+	git branch narrow-full-side &&
+	(
+		cd narrow-full &&
+		git fetch upstream &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		refs/remotes/upstream/narrow-full-side
+		EOF
+	)
+'
+
+test_expect_success 'fetch.shallow=true only narrows a fetch of the tracked remote' '
+	test_when_finished "git branch -D other-side" &&
+	git branch other-side &&
+	git clone --no-local --depth=1 --branch main --single-branch \
+		.git narrow-other-remote &&
+	(
+		cd narrow-other-remote &&
+		git remote add upstream ../.git &&
+		git config fetch.shallow true &&
+		git fetch upstream &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		refs/remotes/upstream/other-side
+		EOF
+	)
+'
+
  . "$TEST_DIRECTORY"/lib-httpd.sh
  start_httpd
 

base-commit: d38352cd43ab9745686d697872408bc3249a153f

#2 lore raw
Quoted 6 lines · Phillip Wood

Hi Harald

Quoted 2 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Quoted 8 lines
Quoted 3 lines

In a shallow, sparsely checked out clone of a repository with many branches, plain git pull can take minutes or hang outright, even though only one branch is actually being worked on.

I think the sparse checkout is irrelevant? It is unclear to me if this is talking about a case where there are many branches in the remote repository and only one of them was cloned, then adding a second remote created a wildcard fetch refspec

Yeah likely. I cloned my fork of homebrew-core with --depth=1 and then added real upstream as a remote. Then I made my work on a feature branch.

Then tried to set my branch to track upstream, but it didn't work.

Then 'git pull' and it hung.

Quoted 4 lines

If it is the former then we should think
how we can improve the behavior of "git remote add" in a sparse repository to prevent it adding a wildcard fetch refspec and instead setup the new remote to fetch only the branch(es) we're interested in.

I didn't even consider that, that sounds like an interesting idea!

Harald

#3 lore raw
Quoted 6 lines
Quoted 4 lines

If it is the former then we should think
how we can improve the behavior of "git remote add" in a sparse repository to prevent it adding a wildcard fetch refspec and instead setup the new remote to fetch only the branch(es) we're interested in.

I didn't even consider that, that sounds like an interesting idea!

Would you suggest we do that instead of this, or in addition to this fix?

Seems maybe we only need the fix on "git remote add".

Harald

#4 lore raw
Quoted 9 lines · Harald Nordgren
Quoted 6 lines
Quoted 4 lines

If it is the former then we should think
how we can improve the behavior of "git remote add" in a sparse repository to prevent it adding a wildcard fetch refspec and instead setup the new remote to fetch only the branch(es) we're interested in.

I didn't even consider that, that sounds like an interesting idea!

Would you suggest we do that instead of this, or in addition to this fix?

I'd say instead - if we can fix "git remote add" then the problem shouldn't arise in the first place which is better than having an opt-in config option that most users are probably unaware of.

Thanks

Phillip

Quoted 6 lines

Seems maybe we only need the fix on "git remote add".

Harald

#5 lore raw
Quoted 7 lines · Phillip Wood
Quoted 5 lines · Harald Nordgren
Quoted 5 lines
Quoted 4 lines

If it is the former then we should think
how we can improve the behavior of "git remote add" in a sparse repository to prevent it adding a wildcard fetch refspec and instead setup the new remote to fetch only the branch(es) we're interested in.

Quoted 8 lines
Quoted 4 lines
Quoted 1 line

I didn't even consider that, that sounds like an interesting idea!

Would you suggest we do that instead of this, or in addition to this fix?

I'd say instead - if we can fix "git remote add" then the problem shouldn't arise in the first place which is better than having an opt-in config option that most users are probably unaware of.

That makes a lot of sense.

However, I think there is info missing on the local side that we can't get without making a network call to the newly added remote, like its branches (and default branch). How do we handle that?

Harald

#6 lore raw

Hi Harald

Quoted 17 lines · Harald Nordgren
Quoted 7 lines · Phillip Wood
Quoted 5 lines · Harald Nordgren
Quoted 5 lines
Quoted 4 lines

If it is the former then we should think
how we can improve the behavior of "git remote add" in a sparse repository to prevent it adding a wildcard fetch refspec and instead setup the new remote to fetch only the branch(es) we're interested in.

Quoted 8 lines
Quoted 4 lines
Quoted 1 line

I didn't even consider that, that sounds like an interesting idea!

Would you suggest we do that instead of this, or in addition to this fix?

I'd say instead - if we can fix "git remote add" then the problem shouldn't arise in the first place which is better than having an opt-in config option that most users are probably unaware of.

Quoted 5 lines

That makes a lot of sense.

However, I think there is info missing on the local side that we can't get without making a network call to the newly added remote, like its branches (and default branch). How do we handle that?

There is a "--track" option for "git remote add" to set up the branches that are fetched. Without "--fetch" we could refuse to add a new remote in a shallow repository unless the user tells us which branches to track. That is a bit of a pain, but I think it is better than adding a wildcard refspec that makes "git fetch" unusably slow. We could also perhaps look at the refspecs for the existing remotes and, if they are all fetching the same branch(es?) copy those.

With "--fetch" we could lookup the default branch on the remote and fetch that if "--track" isn't given.

Thanks

Phillip

#7 lore raw
In reply to Phillip Wood
Quoted 9 lines · Phillip Wood

I think the sparse checkout is irrelevant? It is unclear to me if this is talking about a case where there are many branches in the remote repository and only one of them was cloned, then adding a second remote created a wildcard fetch refspec; or if there are intentionally lots of remote tracking branches in the local repository and you don't want to wait for them all to update. If it is the former then we should think how we can improve the behavior of "git remote add" in a sparse repository to prevent it adding a wildcard fetch refspec and instead setup the new remote to fetch only the branch(es) we're interested in.

Very good suggestions. "Avoid wildcards" is easy, but designing a suitable alternative ("only the ones we are interested in") may be harder.

Perhaps we want to have something similar in spirit to the "matching" mode 'git push' has, where the set of local branches we have defines the set of branches we are interested in? That is, when 'remote.*.fetch' is configured to signal that special mode, 'git fetch' would:

- Find each local branch that has its '@{upstream}' set to a branch at the remote we are fetching from.

- Fetch these branches at the remote that our local branches care about.

I said "in spirit" above, and I find it tempting to use ':' and '+:' as the special '<refspec>' to trigger this mode, to mimic what 'git push' does when using the local branches we have as the set of branches we care about. But there are important differences:

(1) The correspondence between local and remote-tracking branches is not one-to-one, as you can fork multiple local topics out of the same upstream branch. Maybe our 7 local branches build on top of only 2 branches we fetch from the remote, for example.

(2) Corollary. Unlike the matching mode in 'git push' where local branch 'B' is used to update branch 'B' at the remote (if it exists), this new mode in 'git fetch' only uses local branches as a guide to determine which branches to fetch from the remote. If our local branch 'B' builds on top of branch 'U' at the remote, it is branch 'U' we fetch and store as the 'refs/remotes/R/U' remote-tracking branch, where 'R' is the remote, and 'B' as the name does not get anywhere in the picture.

In other words, this is not "matching" at all, even though it takes inspiration from it. I do not know what it should be called, but I think it would be a useful addition.

Thanks.

#8 lore raw
Quoted 50 lines · Junio C Hamano
Quoted 9 lines · Phillip Wood

I think the sparse checkout is irrelevant? It is unclear to me if this is talking about a case where there are many branches in the remote repository and only one of them was cloned, then adding a second remote created a wildcard fetch refspec; or if there are intentionally lots of remote tracking branches in the local repository and you don't want to wait for them all to update. If it is the former then we should think how we can improve the behavior of "git remote add" in a sparse repository to prevent it adding a wildcard fetch refspec and instead setup the new remote to fetch only the branch(es) we're interested in.

Very good suggestions. "Avoid wildcards" is easy, but designing a suitable alternative ("only the ones we are interested in") may be harder.

Perhaps we want to have something similar in spirit to the "matching" mode 'git push' has, where the set of local branches we have defines the set of branches we are interested in? That is, when 'remote.*.fetch' is configured to signal that special mode, 'git fetch' would:

- Find each local branch that has its '@{upstream}' set to a branch at the remote we are fetching from.

- Fetch these branches at the remote that our local branches care about.

I said "in spirit" above, and I find it tempting to use ':' and '+:' as the special '<refspec>' to trigger this mode, to mimic what 'git push' does when using the local branches we have as the set of branches we care about. But there are important differences:

(1) The correspondence between local and remote-tracking branches is not one-to-one, as you can fork multiple local topics out of the same upstream branch. Maybe our 7 local branches build on top of only 2 branches we fetch from the remote, for example.

(2) Corollary. Unlike the matching mode in 'git push' where local branch 'B' is used to update branch 'B' at the remote (if it exists), this new mode in 'git fetch' only uses local branches as a guide to determine which branches to fetch from the remote. If our local branch 'B' builds on top of branch 'U' at the remote, it is branch 'U' we fetch and store as the 'refs/remotes/R/U' remote-tracking branch, where 'R' is the remote, and 'B' as the name does not get anywhere in the picture.

Quoted 3 lines

In other words, this is not "matching" at all, even though it takes inspiration from it. I do not know what it should be called, but I think it would be a useful addition.

Very interesting idea! I would like to suggest that the default branch of the upstream is always included as well, even if it's not the upstream of any branch yet.

I run this on every repo I work with

    git branch --set-upstream-to=upstream # upstream's default branch

and noticed on the shallow repo that it didn't work unless I first ran this

    git remote set-head upstream --auto

which was very annoying.

Harald

#9 lore raw
In reply to Junio C Hamano
Quoted 22 lines · Junio C Hamano
Quoted 9 lines · Phillip Wood

I think the sparse checkout is irrelevant? It is unclear to me if this is talking about a case where there are many branches in the remote repository and only one of them was cloned, then adding a second remote created a wildcard fetch refspec; or if there are intentionally lots of remote tracking branches in the local repository and you don't want to wait for them all to update. If it is the former then we should think how we can improve the behavior of "git remote add" in a sparse repository to prevent it adding a wildcard fetch refspec and instead setup the new remote to fetch only the branch(es) we're interested in.

Very good suggestions. "Avoid wildcards" is easy, but designing a suitable alternative ("only the ones we are interested in") may be harder.

Perhaps we want to have something similar in spirit to the "matching" mode 'git push' has, where the set of local branches we have defines the set of branches we are interested in? That is, when 'remote.*.fetch' is configured to signal that special mode, 'git fetch' would:

Quoted 5 lines

- Find each local branch that has its '@{upstream}' set to a branch at the remote we are fetching from.

- Fetch these branches at the remote that our local branches care about.

I can see that being useful fetch mode for a remote that we've already fetched from, but for a newly added remote there will be no local branches with their upstream set to it because "git branch --set-upstream-to" fails if the upstream does not already exist. So I like the idea for fetching from existing remotes, but it leaves us with a chicken-and-egg problem when adding new remotes, so I'm not sure how it would work in practice.

Thanks

Phillip

Quoted 26 lines

I said "in spirit" above, and I find it tempting to use ':' and '+:' as the special '<refspec>' to trigger this mode, to mimic what 'git push' does when using the local branches we have as the set of branches we care about. But there are important differences:

(1) The correspondence between local and remote-tracking branches is not one-to-one, as you can fork multiple local topics out of the same upstream branch. Maybe our 7 local branches build on top of only 2 branches we fetch from the remote, for example.

(2) Corollary. Unlike the matching mode in 'git push' where local branch 'B' is used to update branch 'B' at the remote (if it exists), this new mode in 'git fetch' only uses local branches as a guide to determine which branches to fetch from the remote. If our local branch 'B' builds on top of branch 'U' at the remote, it is branch 'U' we fetch and store as the 'refs/remotes/R/U' remote-tracking branch, where 'R' is the remote, and 'B' as the name does not get anywhere in the picture.

In other words, this is not "matching" at all, even though it takes inspiration from it. I do not know what it should be called, but I think it would be a useful addition.

Thanks.

#10 lore raw
Quoted 9 lines · Phillip Wood
Quoted 8 lines

when 'remote.*.fetch' is configured to signal that special mode, 'git fetch' would:

- Find each local branch that has its '@{upstream}' set to a branch at the remote we are fetching from.

- Fetch these branches at the remote that our local branches care about.

Quoted 7 lines

I can see that being useful fetch mode for a remote that we've already fetched from, but for a newly added remote there will be no local branches with their upstream set to it because "git branch --set-upstream-to" fails if the upstream does not already exist. So I like the idea for fetching from existing remotes, but it leaves us with a chicken-and-egg problem when adding new remotes, so I'm not sure how it would work in practice.

Just like with "git push there :", you prime the pump by explicitly doing something (for "push", you do "git push there mine" to express your preference to work with branch 'mine' and share it with the remote).

So if we are to allow customizing the refspec used for fetch with "git remote add", you might do:

    $ git remote add --fetch=: second https://ho.st/git/second

which creates:

[remote "second"]
url = https://ho.st/git/second
fetch = :

(Note: I am not sure if ":" is a good special token to express this mode of fetching, as I said earlier).

Your initial "git fetch second" without any other arguments will be a no-op. You may decide to work on top of their 'main' branch by running:

    $ git fetch second main:refs/remotes/second/main
    $ git checkout -b topic -t second/main

At that point, the local branch 'topic' is built on top of their 'main' branch by having its @{upstream} set to that remote-tracking branch. After that, running "git fetch" will update 'second/main' and no other remote-tracking branch.

#11 lore raw
Quoted 36 lines · Junio C Hamano
Quoted 9 lines · Phillip Wood
Quoted 8 lines

when 'remote.*.fetch' is configured to signal that special mode, 'git fetch' would:

- Find each local branch that has its '@{upstream}' set to a branch at the remote we are fetching from.

- Fetch these branches at the remote that our local branches care about.

Quoted 7 lines

I can see that being useful fetch mode for a remote that we've already fetched from, but for a newly added remote there will be no local branches with their upstream set to it because "git branch --set-upstream-to" fails if the upstream does not already exist. So I like the idea for fetching from existing remotes, but it leaves us with a chicken-and-egg problem when adding new remotes, so I'm not sure how it would work in practice.

Just like with "git push there :", you prime the pump by explicitly doing something (for "push", you do "git push there mine" to express your preference to work with branch 'mine' and share it with the remote).

So if we are to allow customizing the refspec used for fetch with "git remote add", you might do:

    $ git remote add --fetch=: second https://ho.st/git/second

which creates:

[remote "second"]
url = https://ho.st/git/second
fetch = :

Quoted 8 lines

(Note: I am not sure if ":" is a good special token to express this mode of fetching, as I said earlier).

Your initial "git fetch second" without any other arguments will be a no-op. You may decide to work on top of their 'main' branch by running:

    $ git fetch second main:refs/remotes/second/main

I've wanted something similar for notes, so allow me to interject from the sidelines: it would be even nicer to still have the ability to map fetches (so that "git fetch second main" did the right thing, creating a useful remote tracking branch with the usual hierarchy; configurable, of course) on top of this. So, the first "git fetch second" does nothing, but "git fetch second main" + creating the branch does as you describe.

Quoted 6 lines
    $ git checkout -b topic -t second/main

At that point, the local branch 'topic' is built on top of their 'main' branch by having its @{upstream} set to that remote-tracking branch. After that, running "git fetch" will update 'second/main' and no other remote-tracking branch.

Signature · 1 line
D. Ben Knoble
#12 lore raw
Quoted 5 lines · D. Ben Knoble
Quoted 1 line
    $ git fetch second main:refs/remotes/second/main

I've wanted something similar for notes, so allow me to interject from the sidelines: it would be even nicer to still have the ability to map fetches (so that "git fetch second main" did the right thing, creating

The thing is, the command line

    $ git fetch second main

has been used for the past 20 years as a "single-shot fetch" syntax that expresses that the user does not intend to keep interacting with the same 'main' branch or even the same 'second' repository, and for the "single-shot fetch", it is absolutely the wrong thing to create a remote-tracking branch.

It would be even worse if we created a remote 'second' and remote-tracking branch 'refs/remotes/second/main' when you ran

    $ git fetch https://ho.st/second main

Having said that, I suspect that the fact that you have the shorthand 'second' (i.e., you have "[remote "second"] url = ..." defined) may be a good enough sign that you expect to keep interacting with that repository, and some people might appreciate it if

    $ git fetch second main

created a remote-tracking branch "refs/remotes/second/main" automatically.

But we cannot suddenly start doing so without breaking people's expectations, and without a good transition plan. We need at least an escape hatch for users to say "No, this is a single-shot fetch; do not write the object anywhere other than FETCH_HEAD as we have always done".

#13 lore raw
Quoted 11 lines · Junio C Hamano
Quoted 5 lines · D. Ben Knoble
Quoted 1 line
    $ git fetch second main:refs/remotes/second/main

I've wanted something similar for notes, so allow me to interject from the sidelines: it would be even nicer to still have the ability to map fetches (so that "git fetch second main" did the right thing, creating

The thing is, the command line

Quoted 7 lines
    $ git fetch second main

has been used for the past 20 years as a "single-shot fetch" syntax that expresses that the user does not intend to keep interacting with the same 'main' branch or even the same 'second' repository, and for the "single-shot fetch", it is absolutely the wrong thing to create a remote-tracking branch.

Yes, perhaps I was a bit cavalier about that particular case; but in your other message you suggested allowing configurable refmaps, and that's what I was getting at previously without having the vocabulary for it. Thanks!

Quoted 13 lines

It would be even worse if we created a remote 'second' and remote-tracking branch 'refs/remotes/second/main' when you ran

    $ git fetch https://ho.st/second main

Having said that, I suspect that the fact that you have the shorthand 'second' (i.e., you have "[remote "second"] url = ..." defined) may be a good enough sign that you expect to keep interacting with that repository, and some people might appreciate it if

    $ git fetch second main
Quoted 8 lines

created a remote-tracking branch "refs/remotes/second/main" automatically.

But we cannot suddenly start doing so without breaking people's expectations, and without a good transition plan. We need at least an escape hatch for users to say "No, this is a single-shot fetch; do not write the object anywhere other than FETCH_HEAD as we have always done".

So anyway, I think fetch.<remote>.refmap is the right thing here, thanks!

Signature · 1 line
D. Ben Knoble
#14 lore raw
Quoted 11 lines · D. Ben Knoble
Quoted 11 lines

Having said that, I suspect that the fact that you have the shorthand 'second' (i.e., you have "[remote "second"] url = ..." defined) may be a good enough sign that you expect to keep interacting with that repository, and some people might appreciate it if

    $ git fetch second main

created a remote-tracking branch "refs/remotes/second/main" automatically.

Quoted 7 lines
Quoted 5 lines

But we cannot suddenly start doing so without breaking people's expectations, and without a good transition plan. We need at least an escape hatch for users to say "No, this is a single-shot fetch; do not write the object anywhere other than FETCH_HEAD as we have always done".

So anyway, I think fetch.<remote>.refmap is the right thing here, thanks!

Ahh, yes, if we introduce fetch.second.refmap that makes it pretend as if you gave a --refmap argument on the command line, your

	$ git fetch second main

will automatically do what you want it to do. I didn't realize that it was the implication of what I was outlining while preparing the response to Harald ;-)

Thanks.

#15 lore raw

PATCH v2 1/1 fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

git remote add sets a new remote up to fetch every branch by default. In an already shallow repository, that turns the next plain fetch or pull into a slow or hanging one, even when only one or two branches are ever used.

Add a special fetch refspec, "+:", that fetches only the branches our local branches are built on, plus the remote's default branch so a brand new remote is usable right away, without needing to first set anything up to track it. git remote add now uses it instead of the usual wildcard refspec whenever the repository is already shallow.

---
fetch: avoid fetching every branch of a new remote in a shallow repo

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v2
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v1:

1: ec05d3e2d6 ! 1: f04f5ce219 fetch: add config to avoid fetching every branch in shallow repo

     @@ Metadata
      Author: Harald Nordgren <[email protected]>
      
       ## Commit message ##
     -    fetch: add config to avoid fetching every branch in shallow repo
     +    fetch: avoid fetching every branch of a new remote in a shallow repo
      
     -    In a shallow, sparsely checked out clone of a repository with many
     -    branches, plain git pull can take minutes or hang outright, even
     -    though only one branch is actually being worked on.
     +    git remote add sets a new remote up to fetch every branch by default.
     +    In an already shallow repository, that turns the next plain fetch or
     +    pull into a slow or hanging one, even when only one or two branches
     +    are ever used.
      
     -    Add fetch.shallow, off by default. When enabled, a fetch or pull for
     -    a shallow repository that isn't already scoped to specific refs
     -    fetches only the current branch's tracked upstream, instead of every
     -    branch the remote has. git pull ultimately runs such a fetch under
     -    the hood, so this fixes pull the same way. It has no effect once the
     -    repository is no longer shallow, and no effect on a fetch of a remote
     -    the current branch doesn't track, both fall back to the existing
     -    behavior.
     -
     -    This is opt-in rather than automatic because it changes what a plain
     -    fetch or pull leaves in refs/remotes/<name>/ for anyone who currently
     -    relies on it syncing every branch of a shallow remote, not just the
     -    one they are on. Scoping remote.<name>.fetch by hand already covers
     -    this for a single remote, but that requires knowing the config exists
     -    and applies it permanently, even to branches that are not currently
     -    checked out.
     -
     -    The remote's recorded default branch (remotes/<name>/HEAD) is kept up
     -    to date the same way it always is, only the other branches are
     -    skipped.
     +    Add a special fetch refspec, "+:", that fetches only the branches our
     +    local branches are built on, plus the remote's default branch so a
     +    brand new remote is usable right away, without needing to first set
     +    anything up to track it. git remote add now uses it instead of the
     +    usual wildcard refspec whenever the repository is already shallow.
      
          Signed-off-by: Harald Nordgren <[email protected]>
      
     - ## Documentation/config/fetch.adoc ##
     -@@ Documentation/config/fetch.adoc: remove the value for the `fetch.bundleCreationToken` value before fetching.
     - `never`;;
     - 	Never create or modify the `remotes/<name>/HEAD` symbolic-ref.
     - --
     -+
     -+`fetch.shallow`::
     -+	If true, and the repository is a shallow repository (see
     -+	linkgit:git-clone[1] `--depth`), a fetch or `git pull` that names no
     -+	explicit refspec and would otherwise fall back to the remote's
     -+	configured `remote.<name>.fetch` refspec instead fetches only the
     -+	current branch's upstream, when that upstream is on the remote being
     -+	fetched. This avoids negotiating history for every branch the remote
     -+	advertises, which can be slow on a shallow repository that tracks
     -+	many disjoint shallow histories. It has no effect on a fetch that
     -+	names an explicit remote or refspec, and no effect on a repository
     -+	that is not shallow. Defaults to false.
     + ## Documentation/git-remote.adoc ##
     +@@ Documentation/git-remote.adoc: Add a remote named _<name>_ for the repository at
     + _<URL>_.  The command `git fetch <name>` can then be used to create and
     + update remote-tracking branches `<name>/<branch>`.
     + +
     ++If the repository is already a shallow repository (see linkgit:git-clone[1]
     ++`--depth`) and neither `-t` nor `--mirror` is given, the remote is set up
     ++with the special tracking refspec (see the _<refspec>_ entry in
     ++linkgit:git-fetch[1]) instead of the default glob refspec, so that
     ++`git fetch <name>` does not need to negotiate history for every branch
     ++the remote has.
     +++
     + With `-f` option, `git fetch <name>` is run immediately after
     + the remote information is set up.
     + +
     +
     + ## Documentation/pull-fetch-param.adoc ##
     +@@ Documentation/pull-fetch-param.adoc: Negative refspecs can themselves be pattern refspecs. However, they may only
     + contain a _<src>_ and do not specify a _<dst>_. Fully spelled out hex object
     + names are also not supported.
     + +
     ++The special refspec `:` (or `+:` to allow non-fast-forward updates)
     ++directs Git to fetch whichever branches at the remote our local branches
     ++have as their `@{upstream}`, plus the remote's default branch, which is
     ++always included so that it is available even before anything is set up
     ++to track it. Unlike the same syntax when pushing, the local branch name
     ++plays no role here beyond identifying which remote branch to fetch: the
     ++result is written to `refs/remotes/<remote>/<branch>` using the remote's
     ++own branch name, and several local branches built on the same remote
     ++branch only cause it to be fetched once.
      ++
     -+`remotes/<name>/HEAD` is still kept up to date per `fetch.followRemoteHEAD`
     -+while this is in effect, only the other branches are skipped.
     + `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`;
     + it requests fetching everything up to the given tag.
     + +
      
       ## builtin/fetch.c ##
     -@@ builtin/fetch.c: struct fetch_config {
     - 	int recurse_submodules;
     - 	int parallel;
     - 	int submodule_fetch_jobs;
     -+	int shallow;
     - };
     - 
     - static int git_fetch_config(const char *k, const char *v,
     -@@ builtin/fetch.c: static int git_fetch_config(const char *k, const char *v,
     - 		return 0;
     +@@ builtin/fetch.c: static void filter_prefetch_refspec(struct refspec *rs)
       	}
     + }
       
     -+	if (!strcmp(k, "fetch.shallow")) {
     -+		fetch_config->shallow = git_config_bool(k, v);
     -+		return 0;
     ++static void fetch_tracked_branches(struct remote *remote,
     ++				   const struct ref *remote_refs,
     ++				   struct ref ***tail,
     ++				   char **default_branch_out,
     ++				   int force)
     ++{
     ++	struct string_list tracked = STRING_LIST_INIT_DUP;
     ++	struct string_list_item *item;
     ++	struct ref *head;
     ++	char *default_branch = NULL;
     ++
     ++	branches_tracking_remote(remote, &tracked);
     ++
     ++	head = get_remote_ref(remote_refs, "HEAD");
     ++	if (head && head->symref && *head->symref) {
     ++		default_branch = xstrdup(head->symref);
     ++		string_list_insert(&tracked, default_branch);
     ++	}
     ++	free_one_ref(head);
     ++
     ++	for_each_string_list_item(item, &tracked) {
     ++		struct refspec_item ref = { .force = force };
     ++		const char *branch_name = item->string;
     ++		int is_default_branch = default_branch &&
     ++			!strcmp(item->string, default_branch);
     ++
     ++		skip_prefix(branch_name, "refs/heads/", &branch_name);
     ++		ref.src = xstrdup(is_default_branch ? "HEAD" : item->string);
     ++		ref.dst = xstrfmt("refs/remotes/%s/%s", remote->name, branch_name);
     ++		get_fetch_map(remote_refs, &ref, tail, 1);
     ++		free(ref.src);
     ++		free(ref.dst);
     ++	}
     ++
     ++	if (default_branch && default_branch_out) {
     ++		const char *branch_name = default_branch;
     ++		skip_prefix(branch_name, "refs/heads/", &branch_name);
     ++		*default_branch_out = xstrdup(branch_name);
      +	}
      +
     - 	if (!strcmp(k, "fetch.followremotehead")) {
     - 		if (!v)
     - 			return config_error_nonbool(k);
     ++	free(default_branch);
     ++	string_list_clear(&tracked, 0);
     ++}
     ++
     + static struct ref *get_ref_map(struct remote *remote,
     + 			       const struct ref *remote_refs,
     + 			       struct refspec *rs,
     +-			       int tags, int *autotags)
     ++			       int tags, int *autotags,
     ++			       char **bootstrap_head_branch)
     + {
     + 	int i;
     + 	struct ref *rm;
     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
     + 		     /* Note: has_merge implies non-NULL branch->remote_name */
     + 		     (has_merge && !strcmp(branch->remote_name, remote->name)))) {
     + 			for (i = 0; i < remote->fetch.nr; i++) {
     ++				if (remote->fetch.items[i].tracking) {
     ++					fetch_tracked_branches(remote, remote_refs, &tail,
     ++								bootstrap_head_branch,
     ++								remote->fetch.items[i].force);
     ++					continue;
     ++				}
     + 				get_fetch_map(remote_refs, &remote->fetch.items[i], &tail, 0);
     + 				if (remote->fetch.items[i].dst &&
     + 				    remote->fetch.items[i].dst[0])
     +@@ builtin/fetch.c: static void warn_set_head(const char *remote, const char *head_name,
     + }
     + 
     + static int set_head(const struct ref *remote_refs, struct remote *remote,
     +-			int follow_remote_head)
     ++			int follow_remote_head, const char *known_head_branch)
     + {
     + 	int result = 0, create_only, baremirror, was_detached;
     + 	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
     + 		      b_local_head = STRBUF_INIT;
     + 	const char *no_warn_branch = remote->no_warn_branch;
     + 	char *head_name = NULL;
     +-	struct ref *ref, *matches;
     ++	struct ref *ref, *matches = NULL;
     + 	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
     + 	struct refspec_item refspec = {
     + 		.force = 0,
     +@@ builtin/fetch.c: static int set_head(const struct ref *remote_refs, struct remote *remote,
     + 	struct string_list heads = STRING_LIST_INIT_DUP;
     + 	struct ref_store *refs = get_main_ref_store(the_repository);
     + 
     +-	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
     +-	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
     +-				    fetch_map, REMOTE_GUESS_HEAD_ALL);
     +-	for (ref = matches; ref; ref = ref->next) {
     +-		string_list_append(&heads, strip_refshead(ref->name));
     +-	}
     ++	if (known_head_branch) {
     ++		head_name = xstrdup(known_head_branch);
     ++	} else {
     ++		get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
     ++		matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
     ++					    fetch_map, REMOTE_GUESS_HEAD_ALL);
     ++		for (ref = matches; ref; ref = ref->next) {
     ++			string_list_append(&heads, strip_refshead(ref->name));
     ++		}
     + 
     +-	if (!heads.nr)
     +-		result = 1;
     +-	else if (heads.nr > 1)
     +-		result = 1;
     +-	else
     +-		head_name = xstrdup(heads.items[0].string);
     ++		if (!heads.nr)
     ++			result = 1;
     ++		else if (heads.nr > 1)
     ++			result = 1;
     ++		else
     ++			head_name = xstrdup(heads.items[0].string);
     ++	}
     + 
     + 	if (!head_name)
     + 		goto cleanup;
     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 	struct strmap rejected_refs = STRMAP_INIT;
     + 	int summary_width = 0;
     + 	int follow_remote_head;
     ++	char *bootstrap_head_branch = NULL;
     + 
     + 	if (tags == TAGS_DEFAULT) {
     + 		if (transport->remote->fetch_tags == 2)
      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
       	} else {
       		struct branch *branch = branch_get(NULL);
      +		int tracks_this_remote = branch && branch_has_merge_config(branch) &&
      +			!strcmp(branch->remote_name, transport->remote->name);
     -+		int narrow_to_tracked_ref = config->shallow &&
     -+			is_repository_shallow(the_repository) && tracks_this_remote;
     ++		int tracking_refspec = transport->remote->fetch.nr == 1 &&
     ++			transport->remote->fetch.items[0].tracking;
       
     - 		if (transport->remote->fetch.nr) {
     --			refspec_ref_prefixes(&transport->remote->fetch,
     --					     &transport_ls_refs_options.ref_prefixes);
     -+			if (!narrow_to_tracked_ref)
     -+				refspec_ref_prefixes(&transport->remote->fetch,
     -+						     &transport_ls_refs_options.ref_prefixes);
     - 			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     - 				do_set_head = 1;
     +-		if (transport->remote->fetch.nr) {
     ++		if (tracking_refspec) {
     ++			struct string_list tracked = STRING_LIST_INIT_DUP;
     ++			struct string_list_item *item;
     ++
     ++			branches_tracking_remote(transport->remote, &tracked);
     ++			for_each_string_list_item(item, &tracked)
     ++				strvec_push(&transport_ls_refs_options.ref_prefixes,
     ++					    item->string);
     ++			string_list_clear(&tracked, 0);
     ++			strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
     ++		} else if (transport->remote->fetch.nr) {
     + 			refspec_ref_prefixes(&transport->remote->fetch,
     + 					     &transport_ls_refs_options.ref_prefixes);
     +-			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     +-				do_set_head = 1;
       		}
      -		if (branch && branch_has_merge_config(branch) &&
      -		    !strcmp(branch->remote_name, transport->remote->name)) {
     ++		if (transport->remote->fetch.nr &&
     ++		    follow_remote_head != FOLLOW_REMOTE_NEVER)
     ++			do_set_head = 1;
      +		if (tracks_this_remote) {
       			int i;
       			for (i = 0; i < branch->merge_nr; i++) {
       				strvec_push(&transport_ls_refs_options.ref_prefixes,
     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 	transport_ls_refs_options_release(&transport_ls_refs_options);
     + 
     + 	ref_map = get_ref_map(transport->remote, remote_refs, rs,
     +-			      tags, &autotags);
     ++			      tags, &autotags, &bootstrap_head_branch);
     ++
     + 	if (!update_head_ok)
     + 		check_not_current_branch(ref_map);
     + 
     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 		 * Way too many cases where this can go wrong so let's just
     + 		 * ignore errors and fail silently for now.
     + 		 */
     +-		set_head(remote_refs, transport->remote, follow_remote_head);
     ++		set_head(remote_refs, transport->remote, follow_remote_head,
     ++			 bootstrap_head_branch);
     + 	}
     + 
     + cleanup:
     ++	free(bootstrap_head_branch);
     + 	/*
     + 	 * When using batched updates, we want to commit the non-rejected
     + 	 * updates and also handle the rejections.
      
     - ## t/t5537-fetch-shallow.sh ##
     -@@ t/t5537-fetch-shallow.sh: commit() {
     - 	git commit -m "$1"
     + ## builtin/remote.c ##
     +@@
     + #include "rebase.h"
     + #include "refs.h"
     + #include "refspec.h"
     ++#include "shallow.h"
     + #include "odb.h"
     + #include "strvec.h"
     + #include "commit-reach.h"
     +@@ builtin/remote.c: static void add_branch(const char *key, const char *branchname,
     + 		       const char *remotename, int mirror, struct strbuf *tmp)
     + {
     + 	strbuf_reset(tmp);
     +-	strbuf_addch(tmp, '+');
     +-	if (mirror)
     +-		strbuf_addf(tmp, "refs/%s:refs/%s",
     +-				branchname, branchname);
     +-	else
     +-		strbuf_addf(tmp, "refs/heads/%s:refs/remotes/%s/%s",
     +-				branchname, remotename, branchname);
     ++	if (!strcmp(branchname, ":")) {
     ++		strbuf_addstr(tmp, "+:");
     ++	} else {
     ++		strbuf_addch(tmp, '+');
     ++		if (mirror)
     ++			strbuf_addf(tmp, "refs/%s:refs/%s",
     ++					branchname, branchname);
     ++		else
     ++			strbuf_addf(tmp, "refs/heads/%s:refs/remotes/%s/%s",
     ++					branchname, remotename, branchname);
     ++	}
     + 	repo_config_set_multivar(the_repository, key, tmp->buf, "^$", 0);
       }
       
     -+check_upstream_refs () {
     -+	git for-each-ref --format="%(refname)" refs/remotes/upstream/ >actual &&
     -+	cat >expect &&
     -+	test_cmp expect actual
     -+}
     +@@ builtin/remote.c: static int add(int argc, const char **argv, const char *prefix,
     + 		strbuf_reset(&buf);
     + 		strbuf_addf(&buf, "remote.%s.fetch", name);
     + 		if (track.nr == 0)
     +-			string_list_append(&track, "*");
     +-		for (size_t i = 0; i < track.nr; i++) {
     ++			string_list_append(&track,
     ++				mirror == MIRROR_NONE &&
     ++				is_repository_shallow(the_repository) ?
     ++				":" : "*");
     ++		for (size_t i = 0; i < track.nr; i++)
     + 			add_branch(buf.buf, track.items[i].string,
     + 				   name, mirror, &buf2);
     +-		}
     + 	}
     + 
     + 	if (mirror & MIRROR_PUSH) {
     +
     + ## refspec.c ##
     +@@ refspec.c: static int parse_refspec(struct refspec_item *item, const char *refspec,
     + 		return 1;
     + 	}
     + 
     ++	if (fetch && rhs == lhs && rhs[1] == '\0') {
     ++		item->tracking = 1;
     ++		return 1;
     ++	}
     ++
     + 	if (rhs) {
     + 		size_t rlen = strlen(++rhs);
     + 		is_glob = (1 <= rlen && strchr(rhs, '*'));
     +@@ refspec.c: void refspec_item_clear(struct refspec_item *item)
     + 	item->force = 0;
     + 	item->pattern = 0;
     + 	item->matching = 0;
     ++	item->tracking = 0;
     + 	item->exact_sha1 = 0;
     + }
     + 
     +@@ refspec.c: static int refspec_find_negative_match(struct refspec *rs, struct refspec_item *
     + 		struct refspec_item *refspec = &rs->items[i];
     + 		char *expn_name;
     + 
     +-		if (refspec->negative)
     ++		if (refspec->negative || refspec->tracking)
     + 			continue;
     + 
     + 		/* Note the reversal of src and dst */
     +
     + ## refspec.h ##
     +@@ refspec.h: struct refspec_item {
     + 	unsigned matching : 1;
     + 	unsigned exact_sha1 : 1;
     + 	unsigned negative : 1;
     ++	unsigned tracking : 1;
     + 
     + 	char *src;
     + 	char *dst;
     +
     + ## remote.c ##
     +@@ remote.c: void ref_push_report_free(struct ref_push_report *report)
     + 
     + int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
     + {
     ++	if (remote->fetch.nr == 1 && remote->fetch.items[0].tracking) {
     ++		struct strbuf prefix = STRBUF_INIT;
     ++		const char *branch_name;
     ++		int ret = -1;
     ++
     ++		strbuf_addf(&prefix, "refs/remotes/%s/", remote->name);
     ++		if (!refspec->src) {
     ++			if (refspec->dst &&
     ++			    skip_prefix(refspec->dst, prefix.buf, &branch_name)) {
     ++				refspec->src = xstrfmt("refs/heads/%s", branch_name);
     ++				refspec->force = 1;
     ++				ret = 0;
     ++			}
     ++		} else if (skip_prefix(refspec->src, "refs/heads/", &branch_name)) {
     ++			free(refspec->dst);
     ++			refspec->dst = xstrfmt("%s%s", prefix.buf, branch_name);
     ++			refspec->force = 1;
     ++			ret = 0;
     ++		}
     ++		strbuf_release(&prefix);
     ++		return ret;
     ++	}
     ++
     + 	return refspec_find_match(&remote->fetch, refspec);
     + }
     + 
     +@@ remote.c: int branch_merge_matches(struct branch *branch,
     + 	return refname_match(branch->merge[i]->src, refname);
     + }
     + 
     ++struct branches_tracking_remote_cb_data {
     ++	struct remote *remote;
     ++	struct string_list *tracked;
     ++};
     ++
     ++static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
     ++{
     ++	struct branches_tracking_remote_cb_data *data = cb_data;
     ++	struct branch *branch;
     ++
     ++	branch = branch_get(ref->name);
     ++	if (!branch_has_merge_config(branch) ||
     ++	    strcmp(branch->remote_name, data->remote->name))
     ++		return 0;
      +
     -+check_upstream_head () {
     -+	git symbolic-ref refs/remotes/upstream/HEAD >actual &&
     -+	echo "refs/remotes/upstream/$1" >expect &&
     -+	test_cmp expect actual
     ++	for (int i = 0; i < branch->merge_nr; i++)
     ++		string_list_insert(data->tracked, branch->merge[i]->src);
     ++
     ++	return 0;
      +}
      +
     -+check_same_tip () {
     -+	git log --oneline -1 "$1" >expect &&
     -+	git -C "$2" log --oneline -1 "$3" >actual &&
     -+	test_cmp expect actual
     ++void branches_tracking_remote(struct remote *remote, struct string_list *tracked)
     ++{
     ++	struct branches_tracking_remote_cb_data data = { remote, tracked };
     ++
     ++	refs_for_each_branch_ref(get_main_ref_store(the_repository),
     ++				  add_if_tracking_remote, &data);
      +}
      +
     - test_expect_success 'setup' '
     - 	commit 1 &&
     - 	commit 2 &&
     -@@ t/t5537-fetch-shallow.sh: test_expect_success 'fetch --deepen does not truncate' '
     - 	test_cmp expect actual
     + __attribute__((format (printf,2,3)))
     + static char *error_buf(struct strbuf *err, const char *fmt, ...)
     + {
     +@@ remote.c: int get_fetch_map(const struct ref *remote_refs,
     + {
     + 	struct ref *ref_map, **rmp;
     + 
     +-	if (refspec->negative)
     ++	if (refspec->negative || refspec->tracking)
     + 		return 0;
     + 
     + 	if (refspec->pattern) {
     +
     + ## remote.h ##
     +@@ remote.h: int branch_has_merge_config(struct branch *branch);
     + 
     + int branch_merge_matches(struct branch *, int n, const char *);
     + 
     ++/* fills tracked with the refname of every local branch's upstream on remote */
     ++void branches_tracking_remote(struct remote *remote, struct string_list *tracked);
     ++
     + /* list of the remote in a group as configured */
     + struct remote_group_data {
     + 	const char *name;
     +
     + ## t/meson.build ##
     +@@ t/meson.build: integration_tests = [
     +   't5582-fetch-negative-refspec.sh',
     +   't5583-push-branches.sh',
     +   't5584-http-429-retry.sh',
     ++  't5585-fetch-tracking-refspec.sh',
     +   't5600-clone-fail-cleanup.sh',
     +   't5601-clone.sh',
     +   't5602-clone-remote-exec.sh',
     +
     + ## t/t5505-remote.sh ##
     +@@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only' '
     + 	test_grep ! "\[blob:none\]" out
       '
       
     -+test_expect_success 'fetch.shallow setup' '
     -+	git branch narrow-side &&
     -+	git clone --no-local --depth=1 --branch main --single-branch \
     -+		.git narrow-default &&
     ++test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' '
     ++	test_when_finished "rm -rf shallow-add" &&
      +	git clone --no-local --depth=1 --branch main --single-branch \
     -+		.git narrow-enabled &&
     -+	(
     -+		cd narrow-default &&
     -+		git remote add upstream ../.git &&
     -+		git fetch --depth=1 upstream main:refs/remotes/upstream/main &&
     -+		git branch --set-upstream-to=upstream/main main
     -+	) &&
     ++		one shallow-add &&
      +	(
     -+		cd narrow-enabled &&
     -+		git remote add upstream ../.git &&
     -+		git fetch --depth=1 upstream main:refs/remotes/upstream/main &&
     -+		git branch --set-upstream-to=upstream/main main &&
     -+		git config fetch.shallow true
     ++		cd shallow-add &&
     ++		git remote add -t main upstream ../two &&
     ++		test_cmp_config "+refs/heads/main:refs/remotes/upstream/main" \
     ++			remote.upstream.fetch
      +	)
      +'
      +
     -+test_expect_success 'a refspec-less fetch expands to the configured refspec by default' '
     ++test_expect_success 'add remote keeps the wildcard refspec in a full repository' '
     ++	test_when_finished "rm -rf full-add" &&
     ++	git clone --no-local one full-add &&
      +	(
     -+		cd narrow-default &&
     -+		git fetch upstream &&
     -+		check_upstream_refs <<-\EOF
     -+		refs/remotes/upstream/HEAD
     -+		refs/remotes/upstream/main
     -+		refs/remotes/upstream/narrow-side
     -+		EOF
     ++		cd full-add &&
     ++		git remote add upstream ../two &&
     ++		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
     ++			remote.upstream.fetch
      +	)
      +'
      +
     -+test_expect_success 'fetch.shallow=true limits a refspec-less fetch to the tracked branch' '
     ++test_expect_success 'a remote added in a shallow repository defaults to the tracking refspec' '
     ++	test_when_finished "rm -rf shallow-add" &&
     ++	git clone --no-local --depth=1 --branch main --single-branch \
     ++		one shallow-add &&
      +	(
     -+		cd narrow-enabled &&
     ++		cd shallow-add &&
     ++		git remote add upstream ../two &&
     ++		test_cmp_config "+:" remote.upstream.fetch &&
      +		git fetch upstream &&
     -+		check_upstream_refs <<-\EOF
     ++		git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
     ++		cat >expect <<-\EOF &&
      +		refs/remotes/upstream/HEAD
      +		refs/remotes/upstream/main
      +		EOF
     ++		test_cmp expect actual &&
     ++		git branch --set-upstream-to=upstream &&
     ++		test_cmp_config upstream branch.main.remote &&
     ++		test_cmp_config refs/heads/main branch.main.merge
      +	)
      +'
      +
     -+test_expect_success 'fetch.shallow=true still creates refs/remotes/<remote>/HEAD' '
     + test_expect_success 'check remote-tracking' '
     + 	(
     + 		cd test &&
     +
     + ## t/t5585-fetch-tracking-refspec.sh (new) ##
     +@@
     ++#!/bin/sh
     ++
     ++test_description='"git fetch" with the tracking refspec ":" (or "+:")
     ++
     ++The special refspec fetches whichever branches at the remote our local
     ++branches have as their @{upstream}, plus the default branch of that
     ++remote, which is always included so that it is available even before
     ++anything is set up to track it. git remote add uses it by default for
     ++a new remote in an already shallow repository, so that adding a
     ++second remote there does not turn the next plain fetch or pull into
     ++one that negotiates history for every branch the remote has.
     ++'
     ++
     ++GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
     ++export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
     ++
     ++. ./test-lib.sh
     ++
     ++test_expect_success 'setup' '
     ++	test_commit main-1 &&
     ++	test_commit main-2 &&
     ++	git checkout -b side main-1 &&
     ++	test_commit side-1 &&
     ++	git checkout -b next main-1 &&
     ++	test_commit next-1 &&
     ++	git checkout main
     ++'
     ++
     ++test_expect_success 'clone shallow and single-branch, then add a second remote' '
     ++	git clone --no-local --depth=1 --branch main --single-branch . client &&
      +	(
     -+		cd narrow-enabled &&
     -+		git symbolic-ref -d refs/remotes/upstream/HEAD &&
     -+		git fetch upstream &&
     -+		check_upstream_head main
     ++		cd client &&
     ++		git remote add upstream .. &&
     ++		test_cmp_config "+:" remote.upstream.fetch
      +	)
      +'
      +
     -+test_expect_success 'fetch.shallow=true with followRemoteHEAD=always corrects a stale HEAD' '
     -+	test_when_finished \
     -+		"git -C narrow-enabled update-ref -d refs/remotes/upstream/stale-branch" &&
     ++test_expect_success 'fetching the new remote does not need every branch it has' '
      +	(
     -+		cd narrow-enabled &&
     -+		git update-ref refs/remotes/upstream/stale-branch refs/remotes/upstream/main &&
     -+		git symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/stale-branch &&
     -+		git -c fetch.followRemoteHEAD=always fetch upstream &&
     -+		check_upstream_head main
     ++		cd client &&
     ++		git fetch upstream &&
     ++		git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
     ++		cat >expect <<-\EOF &&
     ++		refs/remotes/upstream/HEAD
     ++		refs/remotes/upstream/main
     ++		EOF
     ++		test_cmp expect actual
      +	)
      +'
      +
     -+test_expect_success 'fetch.shallow=true still updates the tracked branch' '
     -+	commit 5 &&
     -+	git -C narrow-enabled fetch upstream &&
     -+	check_same_tip main narrow-enabled refs/remotes/upstream/main
     ++test_expect_success 'set-upstream-to now resolves right after that first fetch' '
     ++	(
     ++		cd client &&
     ++		git branch --set-upstream-to=upstream &&
     ++		test_cmp_config upstream branch.main.remote &&
     ++		test_cmp_config refs/heads/main branch.main.merge
     ++	)
      +'
      +
     -+test_expect_success 'fetch.shallow=true keeps git pull narrowed too' '
     -+	test_when_finished "git branch -D narrow-side" &&
     -+	commit 6 &&
     ++test_expect_success 'a branch checked out from a one-time fetch is kept updated by later plain fetches' '
     ++	(
     ++		cd client &&
     ++		git fetch upstream side:refs/remotes/upstream/side &&
     ++		git branch side-topic upstream/side
     ++	) &&
     ++	git checkout side &&
     ++	test_commit side-2 &&
     ++	git checkout main &&
      +	(
     -+		cd narrow-enabled &&
     -+		git pull &&
     -+		check_upstream_refs <<-\EOF
     ++		cd client &&
     ++		git fetch upstream &&
     ++		git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
     ++		cat >expect <<-\EOF &&
      +		refs/remotes/upstream/HEAD
      +		refs/remotes/upstream/main
     ++		refs/remotes/upstream/side
      +		EOF
     -+	) &&
     -+	check_same_tip main narrow-enabled HEAD
     ++		test_cmp expect actual &&
     ++		git rev-parse refs/remotes/upstream/side >actual-oid &&
     ++		git -C .. rev-parse side >expect-oid &&
     ++		test_cmp expect-oid actual-oid
     ++	)
      +'
      +
     -+test_expect_success 'fetch.shallow=true has no effect on a non-shallow repository' '
     -+	git clone --no-local --branch main --single-branch .git narrow-full &&
     -+	(
     -+		cd narrow-full &&
     -+		git rev-parse --is-shallow-repository >actual &&
     -+		echo false >expect &&
     -+		test_cmp expect actual &&
     -+		git remote add upstream ../.git &&
     -+		git fetch upstream &&
     -+		git branch --set-upstream-to=upstream/main main &&
     -+		git config fetch.shallow true
     -+	) &&
     -+	test_when_finished "git branch -D narrow-full-side" &&
     -+	git branch narrow-full-side &&
     ++test_expect_success 'a second branch tracking the same upstream branch does not fetch it twice' '
      +	(
     -+		cd narrow-full &&
     ++		cd client &&
     ++		git branch side-topic-2 upstream/side &&
      +		git fetch upstream &&
     -+		check_upstream_refs <<-\EOF
     ++		git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
     ++		cat >expect <<-\EOF &&
      +		refs/remotes/upstream/HEAD
      +		refs/remotes/upstream/main
     -+		refs/remotes/upstream/narrow-full-side
     ++		refs/remotes/upstream/side
      +		EOF
     ++		test_cmp expect actual
      +	)
      +'
      +
     -+test_expect_success 'fetch.shallow=true only narrows a fetch of the tracked remote' '
     -+	test_when_finished "git branch -D other-side" &&
     -+	git branch other-side &&
     -+	git clone --no-local --depth=1 --branch main --single-branch \
     -+		.git narrow-other-remote &&
     ++test_expect_success 'a branch tracking a different remote is not fetched from upstream' '
      +	(
     -+		cd narrow-other-remote &&
     -+		git remote add upstream ../.git &&
     -+		git config fetch.shallow true &&
     ++		cd client &&
     ++		git remote add other .. &&
     ++		git fetch other next:refs/remotes/other/next &&
     ++		git branch next-topic other/next &&
      +		git fetch upstream &&
     -+		check_upstream_refs <<-\EOF
     ++		git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
     ++		cat >expect <<-\EOF &&
      +		refs/remotes/upstream/HEAD
      +		refs/remotes/upstream/main
     -+		refs/remotes/upstream/other-side
     ++		refs/remotes/upstream/side
      +		EOF
     ++		test_cmp expect actual
      +	)
      +'
      +
     - . "$TEST_DIRECTORY"/lib-httpd.sh
     - start_httpd
     - 
     ++test_expect_success 'git remote show does not choke on the tracking refspec' '
     ++	(
     ++		cd client &&
     ++		git remote show upstream
     ++	)
     ++'
     ++
     ++test_done
 Documentation/git-remote.adoc       |   7 ++
 Documentation/pull-fetch-param.adoc |  10 +++
 builtin/fetch.c                     | 117 +++++++++++++++++++++-----
 builtin/remote.c                    |  27 +++---
 refspec.c                           |   8 +-
 refspec.h                           |   1 +
 remote.c                            |  54 +++++++++++-
 remote.h                            |   3 +
 t/meson.build                       |   1 +
 t/t5505-remote.sh                   |  44 ++++++++++
 t/t5585-fetch-tracking-refspec.sh   | 124 ++++++++++++++++++++++++++++
 11 files changed, 362 insertions(+), 34 deletions(-)
 create mode 100755 t/t5585-fetch-tracking-refspec.sh
Documentation/git-remote.adoc +7 -0
index eaae30aa88..7856ccf54f 100644
@@ -52,6 +52,13 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t` nor `--mirror` is given, the remote is set up
+with the special tracking refspec (see the _<refspec>_ entry in
+linkgit:git-fetch[1]) instead of the default glob refspec, so that
+`git fetch <name>` does not need to negotiate history for every branch
+the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
Documentation/pull-fetch-param.adoc +10 -0
index d903dc8900..e1a42152d9 100644
@@ -48,6 +48,16 @@ Negative refspecs can themselves be pattern refspecs. However, they may only
 contain a _<src>_ and do not specify a _<dst>_. Fully spelled out hex object
 names are also not supported.
 +
+The special refspec `:` (or `+:` to allow non-fast-forward updates)
+directs Git to fetch whichever branches at the remote our local branches
+have as their `@{upstream}`, plus the remote's default branch, which is
+always included so that it is available even before anything is set up
+to track it. Unlike the same syntax when pushing, the local branch name
+plays no role here beyond identifying which remote branch to fetch: the
+result is written to `refs/remotes/<remote>/<branch>` using the remote's
+own branch name, and several local branches built on the same remote
+branch only cause it to be fetched once.
++
 `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`;
 it requests fetching everything up to the given tag.
 +
builtin/fetch.c +95 -22

182 lines not on this page · raw message

builtin/remote.c +17 -10
index de989ea3ba..e407d67037 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -127,13 +128,17 @@ static void add_branch(const char *key, const char *branchname,
 		       const char *remotename, int mirror, struct strbuf *tmp)
 {
 	strbuf_reset(tmp);
-	strbuf_addch(tmp, '+');
-	if (mirror)
-		strbuf_addf(tmp, "refs/%s:refs/%s",
-				branchname, branchname);
-	else
-		strbuf_addf(tmp, "refs/heads/%s:refs/remotes/%s/%s",
-				branchname, remotename, branchname);
+	if (!strcmp(branchname, ":")) {
+		strbuf_addstr(tmp, "+:");
+	} else {
+		strbuf_addch(tmp, '+');
+		if (mirror)
+			strbuf_addf(tmp, "refs/%s:refs/%s",
+					branchname, branchname);
+		else
+			strbuf_addf(tmp, "refs/heads/%s:refs/remotes/%s/%s",
+					branchname, remotename, branchname);
+	}
 	repo_config_set_multivar(the_repository, key, tmp->buf, "^$", 0);
 }
 
@@ -233,11 +238,13 @@ static int add(int argc, const char **argv, const char *prefix,
 		strbuf_reset(&buf);
 		strbuf_addf(&buf, "remote.%s.fetch", name);
 		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
+			string_list_append(&track,
+				mirror == MIRROR_NONE &&
+				is_repository_shallow(the_repository) ?
+				":" : "*");
+		for (size_t i = 0; i < track.nr; i++)
 			add_branch(buf.buf, track.items[i].string,
 				   name, mirror, &buf2);
-		}
 	}
 
 	if (mirror & MIRROR_PUSH) {
refspec.c +7 -1
index 7cb479983b..84a19b5b5c 100644
@@ -49,6 +49,11 @@ static int parse_refspec(struct refspec_item *item, const char *refspec,
 		return 1;
 	}
 
+	if (fetch && rhs == lhs && rhs[1] == '\0') {
+		item->tracking = 1;
+		return 1;
+	}
+
 	if (rhs) {
 		size_t rlen = strlen(++rhs);
 		is_glob = (1 <= rlen && strchr(rhs, '*'));
@@ -181,6 +186,7 @@ void refspec_item_clear(struct refspec_item *item)
 	item->force = 0;
 	item->pattern = 0;
 	item->matching = 0;
+	item->tracking = 0;
 	item->exact_sha1 = 0;
 }
 
@@ -373,7 +379,7 @@ static int refspec_find_negative_match(struct refspec *rs, struct refspec_item *
 		struct refspec_item *refspec = &rs->items[i];
 		char *expn_name;
 
-		if (refspec->negative)
+		if (refspec->negative || refspec->tracking)
 			continue;
 
 		/* Note the reversal of src and dst */
refspec.h +1 -0
index fadef67933..97bd6ffef5 100644
@@ -27,6 +27,7 @@ struct refspec_item {
 	unsigned matching : 1;
 	unsigned exact_sha1 : 1;
 	unsigned negative : 1;
+	unsigned tracking : 1;
 
 	char *src;
 	char *dst;
remote.c +53 -1

72 lines not on this page · raw message

remote.h +3 -0
index cca02033b9..5ab187e00a 100644
@@ -353,6 +353,9 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index 3ca7b27104..4c13048d46 100644
@@ -728,6 +728,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5585-fetch-tracking-refspec.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5505-remote.sh +44 -0

50 lines not on this page · raw message

t/t5585-fetch-tracking-refspec.sh new file +124 -0

124 lines not on this page · raw message

base-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7

Signature · 1 line
gitgitgadget
#16 lore raw

Re: [PATCH v2] fetch: avoid fetching every branch of a new remote in a shallow repo

Quoted 7 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

git remote add sets a new remote up to fetch every branch by default. In an already shallow repository, that turns the next plain fetch or pull into a slow or hanging one, even when only one or two branches are ever used.

Quoted 5 lines

Add a special fetch refspec, "+:", that fetches only the branches our local branches are built on, plus the remote's default branch so a brand new remote is usable right away, without needing to first set anything up to track it. git remote add now uses it instead of the usual wildcard refspec whenever the repository is already shallow.

That's way too much for a single patch. It needs to be split into digestible chunks, but I offhand do not know how many pieces are appropriate, so let's think aloud together and try to refine the design while we do so.

The outline of our design so far should give something like this in our configuration file.

[remote "second"]
fetch = :+

[branch "topic1"]
remote = second
merge = refs/heads/main

[branch "topic2"]
remote = second
merge = refs/heads/next

In the "fetch only what we build on" mode, we would collect local branches $X where branch.$X.remote == second and then collect branch.$X.merge for these local branches. In this case, we would decide to fetch 'main' and 'next' branches in the end.

But notice that this does not give us sufficient information. There is no explicit clue that tells that the remote-tracking branches for this remote 'second' should be stored under refs/remotes/second/ hierarchy. A normal remote that is defined like so:

[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*

does not have such a problem, as it makes it crystal clear that their branches go under refs/remotes/origin/ hierarchy.

So using "fetch = :+" is *not* a good idea, as I said. Let's scrap that syntax.

One thing we could do is probably to introduce

[remote "second"]
refmap = +refs/heads/*:refs/remotes/second/*

instead to give this clue (see "git fetch --help" for what a refmap is; it looks similar to refspec but only defines how their refs are mapped to our namespace without specifying what to be fetched, which is exactly what we need here). We do not use remote.second.fetch at all.

It would be an easy first step to teach that an explicit

	$ git fetch second main next

with such a remote.second.refmap should behave the same way as

	$ git fetch --refmap='+refs/heads/*:refs/remotes/second/*' second \
		main next

in a repository without the refmote.second.refmap configuration. As "git fetch --refmap=... second main next" should already work, it would be only the matter of supplementing the command line argument with configured default.

Then teach "git fetch" to further treat

	$ git fetch --refmap='+refs/heads/*:refs/remotes/second/*' second

i.e., fetch with refmap but without specifying what exactly to fetch, as a request to fetch their branches we build on (and nothing else), using the refmap, in other words, the lack of "what to fetch" in the above command line signals "git fetch" to rewrite the above to

	$ git fetch --refmap='+refs/heads/*:refs/remotes/second/*' second \
		main next

internally. Since we have the previous remote.X.refmap step already, it means that with remote.second.refmap configured properly, the user can only say

	$ git fetch second

and it would do the right thing in our scenario.

Another and final step would be to teach "git remote add" to add

[remote "second"]
refmap = +refs/heads/*:refs/remotes/second/*

when you want to fetch only what you build on. I am not sure what should trigger the decision. Your initial message said something about shallow and sparse and an earlier review refuted one of them (I do not recall which offhand, but probably sparse). It probably is a good idea to start with an explicit command line option to "git remote add --limited-fetch" in a single commit.

And then add heuristics (like "in a shallow clone, this mode is turned on by default, but an explicit '--no-limited-fetch' can countermand it") in another commit.

So far, we identified four distinct commits, each bite sized.

- remote.X.refmap configuration acts as if --refmap=... command line argument was passed.

- passing refmap without saying what to fetch enumerates what their branches we build on, and pretend as if the user listed these branches on the command line to fetch.

- "git remote add --limited-fetch" creates remote.X.refmap instead of remote.X.fetch as necessary.

- "git remote add" without explicit "--[no-]limited-fetch" uses heuristics to enable it.

Or something like that, perhaps?

#17 lore raw

v3 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (4):
fetch: add remote.<name>.refmap
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   7 ++
 Documentation/fetch-options.adoc |   7 ++
 Documentation/git-remote.adoc    |  14 ++-
 builtin/fetch.c                  | 142 +++++++++++++++++++++++++------
 builtin/remote.c                 |  31 +++++--
 remote.c                         |  41 ++++++++-
 remote.h                         |   9 ++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  75 ++++++++++++++++
 t/t5510-fetch.sh                 |  17 ++++
 t/t5585-fetch-refmap.sh          | 124 +++++++++++++++++++++++++++
 11 files changed, 433 insertions(+), 35 deletions(-)
 create mode 100755 t/t5585-fetch-refmap.sh

base-commit: 0f8e75abebff0877cae681a3d5ff31ac47f54220
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v3
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v2:

 -:  ---------- > 1:  b04c00b974 fetch: add remote.<name>.refmap
 1:  f04f5ce219 ! 2:  4ec508a223 fetch: avoid fetching every branch of a new remote in a shallow repo
     @@ Metadata
      Author: Harald Nordgren <[email protected]>
      
       ## Commit message ##
     -    fetch: avoid fetching every branch of a new remote in a shallow repo
     +    fetch: infer branches to fetch from a refmap-only remote
      
     -    git remote add sets a new remote up to fetch every branch by default.
     -    In an already shallow repository, that turns the next plain fetch or
     -    pull into a slow or hanging one, even when only one or two branches
     -    are ever used.
     +    Configuring remote.<name>.refmap without remote.<name>.fetch used to
     +    make a refspec-less "git fetch <name>" fail with "--refmap option is
     +    only meaningful with command-line refspec(s)", since a refmap only
     +    says where to put fetched refs, not what to fetch.
      
     -    Add a special fetch refspec, "+:", that fetches only the branches our
     -    local branches are built on, plus the remote's default branch so a
     -    brand new remote is usable right away, without needing to first set
     -    anything up to track it. git remote add now uses it instead of the
     -    usual wildcard refspec whenever the repository is already shallow.
     +    Make that case infer what to fetch: the local branches whose
     +    @{upstream} is already on that remote, plus the remote's default
     +    branch, which is always included so it is available even before
     +    anything is set up to track it. This lets a remote be configured to
     +    fetch only the branches actually in use, without listing them by
     +    hand in remote.<name>.fetch, and without needing to touch the
     +    command line every time.
      
          Signed-off-by: Harald Nordgren <[email protected]>
      
     - ## Documentation/git-remote.adoc ##
     -@@ Documentation/git-remote.adoc: Add a remote named _<name>_ for the repository at
     - _<URL>_.  The command `git fetch <name>` can then be used to create and
     - update remote-tracking branches `<name>/<branch>`.
     - +
     -+If the repository is already a shallow repository (see linkgit:git-clone[1]
     -+`--depth`) and neither `-t` nor `--mirror` is given, the remote is set up
     -+with the special tracking refspec (see the _<refspec>_ entry in
     -+linkgit:git-fetch[1]) instead of the default glob refspec, so that
     -+`git fetch <name>` does not need to negotiate history for every branch
     -+the remote has.
     -++
     - With `-f` option, `git fetch <name>` is run immediately after
     - the remote information is set up.
     - +
     + ## Documentation/config/remote.adoc ##
     +@@ Documentation/config/remote.adoc: remote.<name>.fetch::
     + 
     + remote.<name>.refmap::
     + 	The default value of the `--refmap` option for linkgit:git-fetch[1].
     +-	Only takes effect when the fetch names what to fetch explicitly,
     +-	either on the command line or via `remote.<name>.fetch`. See the
     +-	`--refmap` entry in linkgit:git-fetch[1].
     ++	If `remote.<name>.fetch` is not set either, a refspec-less fetch
     ++	infers what to fetch from local branches built on this remote,
     ++	instead of fetching every branch it has. See the `--refmap` entry
     ++	in linkgit:git-fetch[1].
     + 
     + remote.<name>.push::
     + 	The default set of "refspec" for linkgit:git-push[1]. See
      
     - ## Documentation/pull-fetch-param.adoc ##
     -@@ Documentation/pull-fetch-param.adoc: Negative refspecs can themselves be pattern refspecs. However, they may only
     - contain a _<src>_ and do not specify a _<dst>_. Fully spelled out hex object
     - names are also not supported.
     - +
     -+The special refspec `:` (or `+:` to allow non-fast-forward updates)
     -+directs Git to fetch whichever branches at the remote our local branches
     -+have as their `@{upstream}`, plus the remote's default branch, which is
     -+always included so that it is available even before anything is set up
     -+to track it. Unlike the same syntax when pushing, the local branch name
     -+plays no role here beyond identifying which remote branch to fetch: the
     -+result is written to `refs/remotes/<remote>/<branch>` using the remote's
     -+own branch name, and several local branches built on the same remote
     -+branch only cause it to be fetched once.
     -++
     - `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`;
     - it requests fetching everything up to the given tag.
     + ## Documentation/fetch-options.adoc ##
     +@@ Documentation/fetch-options.adoc: endif::git-pull[]
     + 	command-line arguments. See section on "Configured Remote-tracking
     + 	Branches" for details.
       +
     +-`remote.<name>.refmap` provides the default value for this option, the
     +-same way `remote.<name>.fetch` provides the default refspecs to fetch.
     ++When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
     ++there is nothing to fetch, neither on the command line nor from
     ++`remote.<name>.fetch`, Git infers what to fetch from the local branches
     ++whose `@{upstream}` is on that remote, plus the remote's default branch,
     ++which is always included so that it is available even before anything
     ++is set up to track it.
     + 
     + `-t`::
     + `--tags`::
      
       ## builtin/fetch.c ##
      @@ builtin/fetch.c: static void filter_prefetch_refspec(struct refspec *rs)
     - 	}
     - }
     - 
     -+static void fetch_tracked_branches(struct remote *remote,
     -+				   const struct ref *remote_refs,
     -+				   struct ref ***tail,
     -+				   char **default_branch_out,
     -+				   int force)
     -+{
     -+	struct string_list tracked = STRING_LIST_INIT_DUP;
     -+	struct string_list_item *item;
     -+	struct ref *head;
     -+	char *default_branch = NULL;
     -+
     -+	branches_tracking_remote(remote, &tracked);
     -+
     -+	head = get_remote_ref(remote_refs, "HEAD");
     -+	if (head && head->symref && *head->symref) {
     -+		default_branch = xstrdup(head->symref);
     -+		string_list_insert(&tracked, default_branch);
     -+	}
     -+	free_one_ref(head);
     -+
     -+	for_each_string_list_item(item, &tracked) {
     -+		struct refspec_item ref = { .force = force };
     -+		const char *branch_name = item->string;
     -+		int is_default_branch = default_branch &&
     -+			!strcmp(item->string, default_branch);
     -+
     -+		skip_prefix(branch_name, "refs/heads/", &branch_name);
     -+		ref.src = xstrdup(is_default_branch ? "HEAD" : item->string);
     -+		ref.dst = xstrfmt("refs/remotes/%s/%s", remote->name, branch_name);
     -+		get_fetch_map(remote_refs, &ref, tail, 1);
     -+		free(ref.src);
     -+		free(ref.dst);
     -+	}
     -+
     -+	if (default_branch && default_branch_out) {
     -+		const char *branch_name = default_branch;
     -+		skip_prefix(branch_name, "refs/heads/", &branch_name);
     -+		*default_branch_out = xstrdup(branch_name);
     -+	}
     -+
     -+	free(default_branch);
     -+	string_list_clear(&tracked, 0);
     -+}
     -+
       static struct ref *get_ref_map(struct remote *remote,
       			       const struct ref *remote_refs,
       			       struct refspec *rs,
     @@ builtin/fetch.c: static void filter_prefetch_refspec(struct refspec *rs)
       	int i;
       	struct ref *rm;
      @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
     - 		     /* Note: has_merge implies non-NULL branch->remote_name */
     - 		     (has_merge && !strcmp(branch->remote_name, remote->name)))) {
     - 			for (i = 0; i < remote->fetch.nr; i++) {
     -+				if (remote->fetch.items[i].tracking) {
     -+					fetch_tracked_branches(remote, remote_refs, &tail,
     -+								bootstrap_head_branch,
     -+								remote->fetch.items[i].force);
     -+					continue;
     -+				}
     - 				get_fetch_map(remote_refs, &remote->fetch.items[i], &tail, 0);
     - 				if (remote->fetch.items[i].dst &&
     - 				    remote->fetch.items[i].dst[0])
     + 	struct ref **tail = &ref_map;
     + 	struct refspec *effective_refmap =
     + 		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
     ++	struct refspec inferred_rs;
     ++	int inferred_branches = 0;
     + 
     + 	/* opportunistically-updated references: */
     + 	struct ref *orefs = NULL, **oref_tail = &orefs;
     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
     + 	struct hashmap existing_refs;
     + 	int existing_refs_populated = 0;
     + 
     ++	refspec_init_fetch(&inferred_rs, the_hash_algo);
     ++
     + 	filter_prefetch_refspec(rs);
     + 	if (remote)
     + 		filter_prefetch_refspec(&remote->fetch);
     + 
     ++	if (!rs->nr && remote && !remote->fetch.nr &&
     ++	    effective_refmap && effective_refmap->nr) {
     ++		struct string_list tracked = STRING_LIST_INIT_DUP;
     ++		struct string_list_item *item;
     ++		struct ref *head;
     ++		char *default_branch = NULL;
     ++		char *default_branch_dst = NULL;
     ++
     ++		branches_tracking_remote(remote, &tracked);
     ++		for_each_string_list_item(item, &tracked)
     ++			refspec_append(&inferred_rs, item->string);
     ++
     ++		/* Always fetch the default branch too, as "HEAD". */
     ++		head = get_remote_ref(remote_refs, "HEAD");
     ++		if (head && head->symref && *head->symref)
     ++			default_branch = xstrdup(head->symref);
     ++		free_one_ref(head);
     ++
     ++		for (i = 0; default_branch && !default_branch_dst &&
     ++			    !string_list_has_string(&tracked, default_branch) &&
     ++			    i < effective_refmap->nr; i++) {
     ++			struct refspec_item *map = &effective_refmap->items[i];
     ++
     ++			if (map->pattern)
     ++				match_refname_with_pattern(map->src, default_branch,
     ++							    map->dst, &default_branch_dst);
     ++			else if (!strcmp(map->src, default_branch))
     ++				default_branch_dst = xstrdup(map->dst);
     ++		}
     ++
     ++		if (default_branch_dst) {
     ++			struct refspec_item head_item = { .force = 1 };
     ++
     ++			head_item.src = xstrdup("HEAD");
     ++			head_item.dst = default_branch_dst;
     ++			get_fetch_map(remote_refs, &head_item, &tail, 1);
     ++			free(head_item.src);
     ++		}
     ++
     ++		if (default_branch && bootstrap_head_branch) {
     ++			const char *branch_name = default_branch;
     ++			skip_prefix(branch_name, "refs/heads/", &branch_name);
     ++			*bootstrap_head_branch = xstrdup(branch_name);
     ++		}
     ++
     ++		free(default_branch);
     ++		free(default_branch_dst);
     ++		string_list_clear(&tracked, 0);
     ++
     ++		rs = &inferred_rs;
     ++		inferred_branches = 1;
     ++	}
     ++
     + 	if (rs->nr) {
     + 		struct refspec *fetch_refspec;
     + 
     + 		for (i = 0; i < rs->nr; i++) {
     +-			get_fetch_map(remote_refs, &rs->items[i], &tail, 0);
     ++			get_fetch_map(remote_refs, &rs->items[i], &tail,
     ++				      inferred_branches);
     + 			if (rs->items[i].dst && rs->items[i].dst[0])
     + 				*autotags = 1;
     + 		}
     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
     + 
     + 		for (i = 0; i < fetch_refspec->nr; i++)
     + 			get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
     ++	} else if (inferred_branches) {
     ++		/* Already fully handled above. */
     + 	} else if (effective_refmap && effective_refmap->nr) {
     + 		die("--refmap option is only meaningful with command-line refspec(s)");
     + 	} else {
     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
     + 	if (existing_refs_populated)
     + 		hashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent);
     + 
     ++	refspec_clear(&inferred_rs);
     + 	return ref_map;
     + }
     + 
      @@ builtin/fetch.c: static void warn_set_head(const char *remote, const char *head_name,
       }
       
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
       	} else {
       		struct branch *branch = branch_get(NULL);
     +-
     +-		if (transport->remote->fetch.nr) {
      +		int tracks_this_remote = branch && branch_has_merge_config(branch) &&
      +			!strcmp(branch->remote_name, transport->remote->name);
     -+		int tracking_refspec = transport->remote->fetch.nr == 1 &&
     -+			transport->remote->fetch.items[0].tracking;
     - 
     --		if (transport->remote->fetch.nr) {
     -+		if (tracking_refspec) {
     ++		struct refspec *effective_refmap = refmap.nr ? &refmap :
     ++			&transport->remote->refmap;
     ++		int inferred_branches = !transport->remote->fetch.nr &&
     ++			effective_refmap->nr;
     ++
     ++		if (inferred_branches) {
      +			struct string_list tracked = STRING_LIST_INIT_DUP;
      +			struct string_list_item *item;
      +
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       		}
      -		if (branch && branch_has_merge_config(branch) &&
      -		    !strcmp(branch->remote_name, transport->remote->name)) {
     -+		if (transport->remote->fetch.nr &&
     ++		if ((transport->remote->fetch.nr || inferred_branches) &&
      +		    follow_remote_head != FOLLOW_REMOTE_NEVER)
      +			do_set_head = 1;
      +		if (tracks_this_remote) {
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       	 * When using batched updates, we want to commit the non-rejected
       	 * updates and also handle the rejections.
      
     - ## builtin/remote.c ##
     -@@
     - #include "rebase.h"
     - #include "refs.h"
     - #include "refspec.h"
     -+#include "shallow.h"
     - #include "odb.h"
     - #include "strvec.h"
     - #include "commit-reach.h"
     -@@ builtin/remote.c: static void add_branch(const char *key, const char *branchname,
     - 		       const char *remotename, int mirror, struct strbuf *tmp)
     - {
     - 	strbuf_reset(tmp);
     --	strbuf_addch(tmp, '+');
     --	if (mirror)
     --		strbuf_addf(tmp, "refs/%s:refs/%s",
     --				branchname, branchname);
     --	else
     --		strbuf_addf(tmp, "refs/heads/%s:refs/remotes/%s/%s",
     --				branchname, remotename, branchname);
     -+	if (!strcmp(branchname, ":")) {
     -+		strbuf_addstr(tmp, "+:");
     -+	} else {
     -+		strbuf_addch(tmp, '+');
     -+		if (mirror)
     -+			strbuf_addf(tmp, "refs/%s:refs/%s",
     -+					branchname, branchname);
     -+		else
     -+			strbuf_addf(tmp, "refs/heads/%s:refs/remotes/%s/%s",
     -+					branchname, remotename, branchname);
     -+	}
     - 	repo_config_set_multivar(the_repository, key, tmp->buf, "^$", 0);
     - }
     - 
     -@@ builtin/remote.c: static int add(int argc, const char **argv, const char *prefix,
     - 		strbuf_reset(&buf);
     - 		strbuf_addf(&buf, "remote.%s.fetch", name);
     - 		if (track.nr == 0)
     --			string_list_append(&track, "*");
     --		for (size_t i = 0; i < track.nr; i++) {
     -+			string_list_append(&track,
     -+				mirror == MIRROR_NONE &&
     -+				is_repository_shallow(the_repository) ?
     -+				":" : "*");
     -+		for (size_t i = 0; i < track.nr; i++)
     - 			add_branch(buf.buf, track.items[i].string,
     - 				   name, mirror, &buf2);
     --		}
     - 	}
     - 
     - 	if (mirror & MIRROR_PUSH) {
     -
     - ## refspec.c ##
     -@@ refspec.c: static int parse_refspec(struct refspec_item *item, const char *refspec,
     - 		return 1;
     - 	}
     - 
     -+	if (fetch && rhs == lhs && rhs[1] == '\0') {
     -+		item->tracking = 1;
     -+		return 1;
     -+	}
     -+
     - 	if (rhs) {
     - 		size_t rlen = strlen(++rhs);
     - 		is_glob = (1 <= rlen && strchr(rhs, '*'));
     -@@ refspec.c: void refspec_item_clear(struct refspec_item *item)
     - 	item->force = 0;
     - 	item->pattern = 0;
     - 	item->matching = 0;
     -+	item->tracking = 0;
     - 	item->exact_sha1 = 0;
     - }
     - 
     -@@ refspec.c: static int refspec_find_negative_match(struct refspec *rs, struct refspec_item *
     - 		struct refspec_item *refspec = &rs->items[i];
     - 		char *expn_name;
     - 
     --		if (refspec->negative)
     -+		if (refspec->negative || refspec->tracking)
     - 			continue;
     - 
     - 		/* Note the reversal of src and dst */
     -
     - ## refspec.h ##
     -@@ refspec.h: struct refspec_item {
     - 	unsigned matching : 1;
     - 	unsigned exact_sha1 : 1;
     - 	unsigned negative : 1;
     -+	unsigned tracking : 1;
     - 
     - 	char *src;
     - 	char *dst;
     -
       ## remote.c ##
     -@@ remote.c: void ref_push_report_free(struct ref_push_report *report)
     - 
     - int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
     - {
     -+	if (remote->fetch.nr == 1 && remote->fetch.items[0].tracking) {
     -+		struct strbuf prefix = STRBUF_INIT;
     -+		const char *branch_name;
     -+		int ret = -1;
     -+
     -+		strbuf_addf(&prefix, "refs/remotes/%s/", remote->name);
     -+		if (!refspec->src) {
     -+			if (refspec->dst &&
     -+			    skip_prefix(refspec->dst, prefix.buf, &branch_name)) {
     -+				refspec->src = xstrfmt("refs/heads/%s", branch_name);
     -+				refspec->force = 1;
     -+				ret = 0;
     -+			}
     -+		} else if (skip_prefix(refspec->src, "refs/heads/", &branch_name)) {
     -+			free(refspec->dst);
     -+			refspec->dst = xstrfmt("%s%s", prefix.buf, branch_name);
     -+			refspec->force = 1;
     -+			ret = 0;
     -+		}
     -+		strbuf_release(&prefix);
     -+		return ret;
     -+	}
     -+
     - 	return refspec_find_match(&remote->fetch, refspec);
     - }
     - 
      @@ remote.c: int branch_merge_matches(struct branch *branch,
       	return refname_match(branch->merge[i]->src, refname);
       }
     @@ remote.c: int branch_merge_matches(struct branch *branch,
       __attribute__((format (printf,2,3)))
       static char *error_buf(struct strbuf *err, const char *fmt, ...)
       {
     -@@ remote.c: int get_fetch_map(const struct ref *remote_refs,
     - {
     - 	struct ref *ref_map, **rmp;
     - 
     --	if (refspec->negative)
     -+	if (refspec->negative || refspec->tracking)
     - 		return 0;
     - 
     - 	if (refspec->pattern) {
      
       ## remote.h ##
      @@ remote.h: int branch_has_merge_config(struct branch *branch);
     @@ t/meson.build: integration_tests = [
         't5582-fetch-negative-refspec.sh',
         't5583-push-branches.sh',
         't5584-http-429-retry.sh',
     -+  't5585-fetch-tracking-refspec.sh',
     ++  't5585-fetch-refmap.sh',
         't5600-clone-fail-cleanup.sh',
         't5601-clone.sh',
         't5602-clone-remote-exec.sh',
      
     - ## t/t5505-remote.sh ##
     -@@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only' '
     - 	test_grep ! "\[blob:none\]" out
     - '
     - 
     -+test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' '
     -+	test_when_finished "rm -rf shallow-add" &&
     -+	git clone --no-local --depth=1 --branch main --single-branch \
     -+		one shallow-add &&
     -+	(
     -+		cd shallow-add &&
     -+		git remote add -t main upstream ../two &&
     -+		test_cmp_config "+refs/heads/main:refs/remotes/upstream/main" \
     -+			remote.upstream.fetch
     -+	)
     -+'
     -+
     -+test_expect_success 'add remote keeps the wildcard refspec in a full repository' '
     -+	test_when_finished "rm -rf full-add" &&
     -+	git clone --no-local one full-add &&
     -+	(
     -+		cd full-add &&
     -+		git remote add upstream ../two &&
     -+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
     -+			remote.upstream.fetch
     -+	)
     -+'
     -+
     -+test_expect_success 'a remote added in a shallow repository defaults to the tracking refspec' '
     -+	test_when_finished "rm -rf shallow-add" &&
     -+	git clone --no-local --depth=1 --branch main --single-branch \
     -+		one shallow-add &&
     -+	(
     -+		cd shallow-add &&
     -+		git remote add upstream ../two &&
     -+		test_cmp_config "+:" remote.upstream.fetch &&
     -+		git fetch upstream &&
     -+		git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
     -+		cat >expect <<-\EOF &&
     -+		refs/remotes/upstream/HEAD
     -+		refs/remotes/upstream/main
     -+		EOF
     -+		test_cmp expect actual &&
     -+		git branch --set-upstream-to=upstream &&
     -+		test_cmp_config upstream branch.main.remote &&
     -+		test_cmp_config refs/heads/main branch.main.merge
     -+	)
     -+'
     -+
     - test_expect_success 'check remote-tracking' '
     - 	(
     - 		cd test &&
     -
     - ## t/t5585-fetch-tracking-refspec.sh (new) ##
     + ## t/t5585-fetch-refmap.sh (new) ##
      @@
      +#!/bin/sh
      +
     -+test_description='"git fetch" with the tracking refspec ":" (or "+:")
     ++test_description='"git fetch" with a remote.<name>.refmap but no remote.<name>.fetch
      +
     -+The special refspec fetches whichever branches at the remote our local
     -+branches have as their @{upstream}, plus the default branch of that
     -+remote, which is always included so that it is available even before
     -+anything is set up to track it. git remote add uses it by default for
     -+a new remote in an already shallow repository, so that adding a
     -+second remote there does not turn the next plain fetch or pull into
     -+one that negotiates history for every branch the remote has.
     ++When a remote has a refmap configured but no fetch refspec, a
     ++refspec-less fetch infers what to fetch from the local branches whose
     ++@{upstream} is on that remote, plus the default branch of that remote,
     ++which is always included so that it is available even before anything
     ++is set up to track it.
      +'
      +
      +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
     @@ t/t5585-fetch-tracking-refspec.sh (new)
      +	(
      +		cd client &&
      +		git remote add upstream .. &&
     -+		test_cmp_config "+:" remote.upstream.fetch
     ++		test_might_fail git config unset remote.upstream.fetch &&
     ++		git config remote.upstream.refmap \
     ++			"+refs/heads/*:refs/remotes/upstream/*"
      +	)
      +'
      +
     @@ t/t5585-fetch-tracking-refspec.sh (new)
      +	)
      +'
      +
     -+test_expect_success 'git remote show does not choke on the tracking refspec' '
     ++test_expect_success 'git remote show does not choke on a refmap-only remote' '
      +	(
      +		cd client &&
      +		git remote show upstream
 -:  ---------- > 3:  604e584956 remote: add "git remote add --limited-fetch"
 -:  ---------- > 4:  31462e9445 remote: default to --limited-fetch in a shallow repository
Signature · 1 line
gitgitgadget
#18 lore raw

PATCH v3 1/4 fetch: add remote.<name>.refmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. It only takes effect when there is something explicit to fetch, on the command line or via remote.<name>.fetch, matching how --refmap itself already behaves.

---
 Documentation/config/remote.adoc |  6 ++++++
 Documentation/fetch-options.adoc |  3 +++
 builtin/fetch.c                  |  8 +++++---
 remote.c                         | 12 +++++++++++-
 remote.h                         |  6 ++++++
 t/t5510-fetch.sh                 | 17 +++++++++++++++++
 6 files changed, 48 insertions(+), 4 deletions(-)
Documentation/config/remote.adoc +6 -0
index 3a20d0f752..00f9ed8d3d 100644
@@ -33,6 +33,12 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Only takes effect when the fetch names what to fetch explicitly,
+	either on the command line or via `remote.<name>.fetch`. See the
+	`--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 035f780e58..538914bc6e 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +5 -3
index 533fdfe7d8..7651b41139 100644
@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
 		for (i = 0; i < fetch_refspec->nr; i++)
 			get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
-	} else if (refmap.nr) {
+	} else if (effective_refmap && effective_refmap->nr) {
 		die("--refmap option is only meaningful with command-line refspec(s)");
 	} else {
 		/* Use the defaults */
remote.c +11 -1
index fe62068463..017cd9d13e 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	if (remote->fetch.nr)
+		return refspec_find_match(&remote->fetch, refspec);
+	return refspec_find_match(&remote->refmap, refspec);
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +17 -0
index a8d38d9176..bd853d0a84 100755
@@ -927,6 +927,23 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '
 	)
 '
 
+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '
+	test_when_finished "git -C three config --unset remote.origin.refmap" &&
+	git branch -f side &&
+	git -C three config remote.origin.refmap \
+		"refs/heads/*:refs/remotes/other/*" &&
+	(
+		cd three &&
+		git update-ref refs/remotes/origin/main base-origin-main &&
+		o=$(git rev-parse --verify refs/remotes/origin/main) &&
+		git fetch origin main &&
+		n=$(git rev-parse --verify refs/remotes/origin/main) &&
+		test "$o" = "$n" &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/side &&
+		git rev-parse --verify refs/remotes/other/main
+	)
+'
+
 test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '
 	git branch -f side &&
 	(
Signature · 1 line
gitgitgadget
#19 lore raw

Re: [PATCH v3 1/4] fetch: add remote.<name>.refmap

Quoted 6 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. It only takes effect when there is something explicit to fetch, on the command line or via

This ...

Documentation/config/remote.adoc remote.<name>.fetch:: · line 40

Quoted 9 lines

remote.<name>.fetch, matching how --refmap itself already behaves.

Documentation/config/remote.adoc +5 -0
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Only takes effect when the fetch names what to fetch explicitly,
+	either on the command line or via `remote.<name>.fetch`. See the
+	`--refmap` entry in linkgit:git-fetch[1].

... and this made me a bit puzzled. It may be a philosophical difference, but I've always viewed --refmap=<src>:<dst> to "take effect" whenever they are given, regardless of 0, 1, or more explicit things to fetch. It is just when you have zero explicit things to fetch, 0 things are mapped via the refmap mechanism and 0 things are fetched.

In other words, what does not "take effect" when 0 things are given explicitly to fetch is not the effect of refmap alone, but the entire 'git fetch' operation itself.

The default value of the --refmap option for linkgit:git-fetch[1]. Used to map remote refs being fetched to remote-tracking refs to store. See the --refmap entry in linkgit:git-fetch[1].

Documentation/fetch-options.adoc endif::git-pull[] · line 249

Quoted 7 lines
Documentation/fetch-options.adoc +3 -0
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.

This is perfect.

Quoted 21 lines
builtin/fetch.c +4 -2
index 533fdfe7d8..7651b41139 100644
@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;

builtin/fetch.c static struct ref *get_ref_map(struct remote *remote, · line 567

Quoted 10 lines
builtin/fetch.c +1 -1
 		else
 			fetch_refspec = &remote->fetch;
 
 		for (i = 0; i < fetch_refspec->nr; i++)
 			get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
-	} else if (refmap.nr) {
+	} else if (effective_refmap && effective_refmap->nr) {
 		die("--refmap option is only meaningful with command-line refspec(s)");
 	} else {
 		/* Use the defaults */

Looking good. One of these days, we probably should reduce our reliance on the file-scope static "global variables" but that is clearly outside the scope of this topic. Perhaps once the dust settles after this topic stabilizes. I wonder if most of them can be added as members to "struct fetch_config" and then we can pass one instance of such struct around in the call chain, or if it needs a lot more involved changes.

#20 lore raw

PATCH v3 2/4 fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote, plus the remote's default branch, which is always included so it is available even before anything is set up to track it. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   7 +-
 Documentation/fetch-options.adoc |   8 +-
 builtin/fetch.c                  | 134 +++++++++++++++++++++++++------
 remote.c                         |  29 +++++++
 remote.h                         |   3 +
 t/meson.build                    |   1 +
 t/t5585-fetch-refmap.sh          | 124 ++++++++++++++++++++++++++++
 7 files changed, 277 insertions(+), 29 deletions(-)
 create mode 100755 t/t5585-fetch-refmap.sh
Documentation/config/remote.adoc +4 -3
index 00f9ed8d3d..5b68af3af1 100644
@@ -35,9 +35,10 @@ remote.<name>.fetch::
 
 remote.<name>.refmap::
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
-	Only takes effect when the fetch names what to fetch explicitly,
-	either on the command line or via `remote.<name>.fetch`. See the
-	`--refmap` entry in linkgit:git-fetch[1].
+	If `remote.<name>.fetch` is not set either, a refspec-less fetch
+	infers what to fetch from local branches built on this remote,
+	instead of fetching every branch it has. See the `--refmap` entry
+	in linkgit:git-fetch[1].
 
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +6 -2
index 538914bc6e..c973a07caf 100644
@@ -245,8 +245,12 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+there is nothing to fetch, neither on the command line nor from
+`remote.<name>.fetch`, Git infers what to fetch from the local branches
+whose `@{upstream}` is on that remote, plus the remote's default branch,
+which is always included so that it is available even before anything
+is set up to track it.
 
 `-t`::
 `--tags`::
builtin/fetch.c +110 -24

221 lines not on this page · raw message

remote.c +29 -0
index 017cd9d13e..a5deb9b929 100644
@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+struct branches_tracking_remote_cb_data {
+	struct remote *remote;
+	struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+	struct branch *branch;
+
+	branch = branch_get(ref->name);
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, data->remote->name))
+		return 0;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(data->tracked, branch->merge[i]->src);
+
+	return 0;
+}
+
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(the_repository),
+				  add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +3 -0
index ac485a584d..da6c3ef53a 100644
@@ -359,6 +359,9 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index 3ca7b27104..ab83d5409c 100644
@@ -728,6 +728,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5585-fetch-refmap.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5585-fetch-refmap.sh new file +124 -0

124 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#21 lore raw

Re: [PATCH v3 2/4] fetch: infer branches to fetch from a refmap-only remote

Quoted 7 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Quoted 7 lines

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote, plus the remote's default branch, which is always included so it is available even before anything is set up to track it. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

You may not necessarily be interested in what the remote's HEAD is pointing at.

- You may only be working on top of their 'maint' and their 'next' without ever touching their 'master'. It would be rude for Git to decide for you to fetch their 'master', which you have no use for, without being asked at all.

- Or you may be very much interested in their 'master' that is pointed at by their HEAD. In such a case, you would have your local branch that is forked from their 'master' *anyway*, and you'll get one, without special casing their HEAD.

Especially since we are about to add the knob to trigger this mechanism to "git remote add", presumably this is to be used for second and later remotes. The primary remote may already be supplying "refs/heads/master -> refs/remotes/origin/master" when the repository was made with "git clone --shallow --single-branch" or something like that. That makes it doubly dubious to assume that the branch the HEAD of this secondary remote points at is so special and everybody that does "git remote add" wants to interact with it.

Quoted 6 lines
Documentation/fetch-options.adoc +0 -0
index 538914bc6e..c973a07caf 100644
@@ -245,8 +245,12 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking

Documentation/fetch-options.adoc endif::git-pull[] · line 253

Quoted 10 lines
Documentation/fetch-options.adoc +6 -2
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+there is nothing to fetch, neither on the command line nor from
+`remote.<name>.fetch`, Git infers what to fetch from the local branches
+whose `@{upstream}` is on that remote, plus the remote's default branch,
+which is always included so that it is available even before anything
+is set up to track it.

So, I would "plus the remote's default branch, which is always included".

builtin/fetch.c static struct ref *get_ref_map(struct remote *remote, · line 566

Quoted 12 lines
builtin/fetch.c +7 -0
index 7651b41139..c8ce89cf30 100644
...
+		if (default_branch_dst) {
+			struct refspec_item head_item = { .force = 1 };
+
+			head_item.src = xstrdup("HEAD");
+			head_item.dst = default_branch_dst;
+			get_fetch_map(remote_refs, &head_item, &tail, 1);
+			free(head_item.src);

This adds a fetch map entry with rm->name = "HEAD" and rm->peer_ref->name = "refs/remotes/second/main". Because "HEAD" does not begin with "refs/heads/", the report after fetch says

* [new ref] HEAD -> second/main

instead the usual

* [new branch] main -> second/main

doesn't it? If we do not force-include HEAD, the code would be simplar and we won't have to worry about this.

Thanks.

#22 lore raw

PATCH v3 3/4 remote: add "git remote add --limited-fetch"

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, plus the remote's default branch, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)
Documentation/git-remote.adoc +7 -1
index eaae30aa88..74fbce6e0e 100644
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [synopsis]
 git remote [-v | --verbose]
-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>
 git remote rename [--[no-]progress] <old> <new>
 git remote remove <name>
 git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_
 is created.  You can give more than one `-t <branch>` to track
 multiple branches without grabbing all branches.
 +
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
+are built on, plus the remote's default branch. See the `--refmap` entry
+in linkgit:git-fetch[1] for details.
++
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
 +
builtin/remote.c +22 -6
index de989ea3ba..f036dd5d6f 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
+	int limited_fetch = -1; /* unspecified */
 	struct string_list track = STRING_LIST_INIT_NODUP;
 	const char *master = NULL;
 	struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
 		OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
 			N_("set up remote as a mirror to push to or fetch from"),
 			PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+		OPT_BOOL(0, "limited-fetch", &limited_fetch,
+			N_("fetch only the branches we build on, instead of every branch")),
 		OPT_END()
 	};
 
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
 		die(_("specifying a master branch makes no sense with --mirror"));
 	if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
 		die(_("specifying branches to track makes sense only with fetch mirrors"));
+	if (limited_fetch == 1 && track.nr)
+		die(_("--limited-fetch does not make sense with -t/--track"));
+	if (limited_fetch == 1 && mirror)
+		die(_("--limited-fetch does not make sense with --mirror"));
 
 	name = argv[0];
 	url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
 	repo_config_set(the_repository, buf.buf, url);
 
 	if (!mirror || mirror & MIRROR_FETCH) {
+		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+			limited_fetch == 1;
+
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "remote.%s.fetch", name);
-		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
-			add_branch(buf.buf, track.items[i].string,
-				   name, mirror, &buf2);
+		if (use_limited_fetch) {
+			strbuf_addf(&buf, "remote.%s.refmap", name);
+			strbuf_reset(&buf2);
+			strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+			repo_config_set(the_repository, buf.buf, buf2.buf);
+		} else {
+			strbuf_addf(&buf, "remote.%s.fetch", name);
+			if (track.nr == 0)
+				string_list_append(&track, "*");
+			for (size_t i = 0; i < track.nr; i++)
+				add_branch(buf.buf, track.items[i].string,
+					   name, mirror, &buf2);
 		}
 	}
 
t/t5505-remote.sh +16 -0
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
 	test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+	test_when_finished "rm -rf full-add" &&
+	git clone --no-local one full-add &&
+	(
+		cd full-add &&
+		git remote add --limited-fetch upstream ../two &&
+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+			remote.upstream.refmap &&
+		test_must_fail git config get remote.upstream.fetch
+	)
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+	test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
 	(
 		cd test &&
Signature · 1 line
gitgitgadget
#23 lore raw

PATCH v3 4/4 remote: default to --limited-fetch in a shallow repository

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 59 +++++++++++++++++++++++++++++++++++
 3 files changed, 70 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index 74fbce6e0e..82b21eafa7 100644
@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on, plus the remote's default branch. See the `--refmap` entry
-in linkgit:git-fetch[1] for details.
+in linkgit:git-fetch[1] for details. `--no-limited-fetch` explicitly
+disables this, overriding the shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +4 -1
index f036dd5d6f..0cf1126976 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
 	if (!mirror || mirror & MIRROR_FETCH) {
 		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-			limited_fetch == 1;
+			(limited_fetch == 1 ||
+			 (limited_fetch == -1 &&
+			  is_repository_shallow(the_repository)));
 
 		strbuf_reset(&buf);
 		if (use_limited_fetch) {
t/t5505-remote.sh +59 -0

65 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#24 lore raw

v4 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (4):
fetch: add remote.<name>.refmap
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |   5 ++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  60 +++++++++++++---
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  41 ++++++++++-
 remote.h                         |   9 +++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 |  17 +++++
 t/t5585-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 363 insertions(+), 18 deletions(-)
 create mode 100755 t/t5585-fetch-refmap.sh

base-commit: a018953688f1b10bddf91bff8747068f5f4746a4
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v4
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v3:

1: b04c00b974 ! 1: d48a7004e4 fetch: add remote.<name>.refmap
@@ Commit message

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch
- already provides the default refspecs to fetch. It only takes effect
- when there is something explicit to fetch, on the command line or via
- remote.<name>.fetch, matching how --refmap itself already behaves.
+ already provides the default refspecs to fetch. Like --refmap itself,
+ it only maps refs that are actually being fetched, so it has nothing
+ to do when there is nothing explicit to fetch, on the command line or
+ via remote.<name>.fetch.

@@ Documentation/config/remote.adoc: remote.<name>.fetch::

+remote.<name>.refmap::
+ The default value of the --refmap option for linkgit:git-fetch[1].
-+ Only takes effect when the fetch names what to fetch explicitly,
-+ either on the command line or via remote.<name>.fetch. See the
-+ --refmap entry in linkgit:git-fetch[1].
++ Used to map remote refs being fetched to remote-tracking refs to
++ store. See the --refmap entry in linkgit:git-fetch[1].
+
remote.<name>.push::
The default set of "refspec" for linkgit:git-push[1]. See
2: 4ec508a223 ! 2: 45b26e2bb2 fetch: infer branches to fetch from a refmap-only remote

     @@ Commit message
          says where to put fetched refs, not what to fetch.
      
          Make that case infer what to fetch: the local branches whose
     -    @{upstream} is already on that remote, plus the remote's default
     -    branch, which is always included so it is available even before
     -    anything is set up to track it. This lets a remote be configured to
     -    fetch only the branches actually in use, without listing them by
     -    hand in remote.<name>.fetch, and without needing to touch the
     -    command line every time.
     +    @{upstream} is already on that remote. This lets a remote be
     +    configured to fetch only the branches actually in use, without
     +    listing them by hand in remote.<name>.fetch, and without needing to
     +    touch the command line every time.
      
          Signed-off-by: Harald Nordgren <[email protected]>
      
       ## Documentation/config/remote.adoc ##
      @@ Documentation/config/remote.adoc: remote.<name>.fetch::
     - 
       remote.<name>.refmap::
       	The default value of the `--refmap` option for linkgit:git-fetch[1].
     --	Only takes effect when the fetch names what to fetch explicitly,
     --	either on the command line or via `remote.<name>.fetch`. See the
     --	`--refmap` entry in linkgit:git-fetch[1].
     -+	If `remote.<name>.fetch` is not set either, a refspec-less fetch
     -+	infers what to fetch from local branches built on this remote,
     -+	instead of fetching every branch it has. See the `--refmap` entry
     -+	in linkgit:git-fetch[1].
     + 	Used to map remote refs being fetched to remote-tracking refs to
     +-	store. See the `--refmap` entry in linkgit:git-fetch[1].
     ++	store. If `remote.<name>.fetch` is not set either, a refspec-less
     ++	fetch infers what to fetch from local branches built on this
     ++	remote, instead of fetching every branch it has. See the
     ++	`--refmap` entry in linkgit:git-fetch[1].
       
       remote.<name>.push::
       	The default set of "refspec" for linkgit:git-push[1]. See
     @@ Documentation/fetch-options.adoc: endif::git-pull[]
      +When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
      +there is nothing to fetch, neither on the command line nor from
      +`remote.<name>.fetch`, Git infers what to fetch from the local branches
     -+whose `@{upstream}` is on that remote, plus the remote's default branch,
     -+which is always included so that it is available even before anything
     -+is set up to track it.
     ++whose `@{upstream}` is on that remote.
       
       `-t`::
       `--tags`::
      
       ## builtin/fetch.c ##
     -@@ builtin/fetch.c: static void filter_prefetch_refspec(struct refspec *rs)
     - static struct ref *get_ref_map(struct remote *remote,
     - 			       const struct ref *remote_refs,
     - 			       struct refspec *rs,
     --			       int tags, int *autotags)
     -+			       int tags, int *autotags,
     -+			       char **bootstrap_head_branch)
     - {
     - 	int i;
     - 	struct ref *rm;
      @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       	struct ref **tail = &ref_map;
       	struct refspec *effective_refmap =
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
      +	    effective_refmap && effective_refmap->nr) {
      +		struct string_list tracked = STRING_LIST_INIT_DUP;
      +		struct string_list_item *item;
     -+		struct ref *head;
     -+		char *default_branch = NULL;
     -+		char *default_branch_dst = NULL;
      +
      +		branches_tracking_remote(remote, &tracked);
      +		for_each_string_list_item(item, &tracked)
      +			refspec_append(&inferred_rs, item->string);
     -+
     -+		/* Always fetch the default branch too, as "HEAD". */
     -+		head = get_remote_ref(remote_refs, "HEAD");
     -+		if (head && head->symref && *head->symref)
     -+			default_branch = xstrdup(head->symref);
     -+		free_one_ref(head);
     -+
     -+		for (i = 0; default_branch && !default_branch_dst &&
     -+			    !string_list_has_string(&tracked, default_branch) &&
     -+			    i < effective_refmap->nr; i++) {
     -+			struct refspec_item *map = &effective_refmap->items[i];
     -+
     -+			if (map->pattern)
     -+				match_refname_with_pattern(map->src, default_branch,
     -+							    map->dst, &default_branch_dst);
     -+			else if (!strcmp(map->src, default_branch))
     -+				default_branch_dst = xstrdup(map->dst);
     -+		}
     -+
     -+		if (default_branch_dst) {
     -+			struct refspec_item head_item = { .force = 1 };
     -+
     -+			head_item.src = xstrdup("HEAD");
     -+			head_item.dst = default_branch_dst;
     -+			get_fetch_map(remote_refs, &head_item, &tail, 1);
     -+			free(head_item.src);
     -+		}
     -+
     -+		if (default_branch && bootstrap_head_branch) {
     -+			const char *branch_name = default_branch;
     -+			skip_prefix(branch_name, "refs/heads/", &branch_name);
     -+			*bootstrap_head_branch = xstrdup(branch_name);
     -+		}
     -+
     -+		free(default_branch);
     -+		free(default_branch_dst);
      +		string_list_clear(&tracked, 0);
      +
      +		rs = &inferred_rs;
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       	return ref_map;
       }
       
     -@@ builtin/fetch.c: static void warn_set_head(const char *remote, const char *head_name,
     - }
     - 
     - static int set_head(const struct ref *remote_refs, struct remote *remote,
     --			int follow_remote_head)
     -+			int follow_remote_head, const char *known_head_branch)
     - {
     - 	int result = 0, create_only, baremirror, was_detached;
     - 	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
     - 		      b_local_head = STRBUF_INIT;
     - 	const char *no_warn_branch = remote->no_warn_branch;
     - 	char *head_name = NULL;
     --	struct ref *ref, *matches;
     -+	struct ref *ref, *matches = NULL;
     - 	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
     - 	struct refspec_item refspec = {
     - 		.force = 0,
     -@@ builtin/fetch.c: static int set_head(const struct ref *remote_refs, struct remote *remote,
     - 	struct string_list heads = STRING_LIST_INIT_DUP;
     - 	struct ref_store *refs = get_main_ref_store(the_repository);
     - 
     --	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
     --	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
     --				    fetch_map, REMOTE_GUESS_HEAD_ALL);
     --	for (ref = matches; ref; ref = ref->next) {
     --		string_list_append(&heads, strip_refshead(ref->name));
     --	}
     -+	if (known_head_branch) {
     -+		head_name = xstrdup(known_head_branch);
     -+	} else {
     -+		get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
     -+		matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
     -+					    fetch_map, REMOTE_GUESS_HEAD_ALL);
     -+		for (ref = matches; ref; ref = ref->next) {
     -+			string_list_append(&heads, strip_refshead(ref->name));
     -+		}
     - 
     --	if (!heads.nr)
     --		result = 1;
     --	else if (heads.nr > 1)
     --		result = 1;
     --	else
     --		head_name = xstrdup(heads.items[0].string);
     -+		if (!heads.nr)
     -+			result = 1;
     -+		else if (heads.nr > 1)
     -+			result = 1;
     -+		else
     -+			head_name = xstrdup(heads.items[0].string);
     -+	}
     - 
     - 	if (!head_name)
     - 		goto cleanup;
     -@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 	struct strmap rejected_refs = STRMAP_INIT;
     - 	int summary_width = 0;
     - 	int follow_remote_head;
     -+	char *bootstrap_head_branch = NULL;
     - 
     - 	if (tags == TAGS_DEFAULT) {
     - 		if (transport->remote->fetch_tags == 2)
      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
       	} else {
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      +				strvec_push(&transport_ls_refs_options.ref_prefixes,
      +					    item->string);
      +			string_list_clear(&tracked, 0);
     -+			strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
      +		} else if (transport->remote->fetch.nr) {
       			refspec_ref_prefixes(&transport->remote->fetch,
       					     &transport_ls_refs_options.ref_prefixes);
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       			for (i = 0; i < branch->merge_nr; i++) {
       				strvec_push(&transport_ls_refs_options.ref_prefixes,
      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 	transport_ls_refs_options_release(&transport_ls_refs_options);
       
       	ref_map = get_ref_map(transport->remote, remote_refs, rs,
     --			      tags, &autotags);
     -+			      tags, &autotags, &bootstrap_head_branch);
     + 			      tags, &autotags);
      +
       	if (!update_head_ok)
       		check_not_current_branch(ref_map);
       
     -@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 		 * Way too many cases where this can go wrong so let's just
     - 		 * ignore errors and fail silently for now.
     - 		 */
     --		set_head(remote_refs, transport->remote, follow_remote_head);
     -+		set_head(remote_refs, transport->remote, follow_remote_head,
     -+			 bootstrap_head_branch);
     - 	}
     - 
     - cleanup:
     -+	free(bootstrap_head_branch);
     - 	/*
     - 	 * When using batched updates, we want to commit the non-rejected
     - 	 * updates and also handle the rejections.
      
       ## remote.c ##
      @@ remote.c: int branch_merge_matches(struct branch *branch,
     @@ t/t5585-fetch-refmap.sh (new)
      +
      +When a remote has a refmap configured but no fetch refspec, a
      +refspec-less fetch infers what to fetch from the local branches whose
     -+@{upstream} is on that remote, plus the default branch of that remote,
     -+which is always included so that it is available even before anything
     -+is set up to track it.
     ++@{upstream} is on that remote.
      +'
      +
      +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
     @@ t/t5585-fetch-refmap.sh (new)
      +	)
      +'
      +
     -+test_expect_success 'fetching the new remote does not need every branch it has' '
     ++test_expect_success 'a bare fetch needs nothing until a branch is tracked' '
      +	(
      +		cd client &&
      +		git fetch upstream &&
      +		git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
     -+		cat >expect <<-\EOF &&
     -+		refs/remotes/upstream/HEAD
     -+		refs/remotes/upstream/main
     -+		EOF
     -+		test_cmp expect actual
     ++		test_must_be_empty actual
      +	)
      +'
      +
     -+test_expect_success 'set-upstream-to now resolves right after that first fetch' '
     ++test_expect_success 'an explicit one-time fetch lets a branch be tracked' '
      +	(
      +		cd client &&
     -+		git branch --set-upstream-to=upstream &&
     ++		git fetch upstream main &&
     ++		git branch --set-upstream-to=upstream/main &&
      +		test_cmp_config upstream branch.main.remote &&
      +		test_cmp_config refs/heads/main branch.main.merge
      +	)
 3:  604e584956 ! 3:  e2f072c254 remote: add "git remote add --limited-fetch"
     @@ Documentation/git-remote.adoc: the `refs/remotes/<name>/` namespace, a refspec t
      +With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
      +that tracks all branches, `remote.<name>.refmap` is set up so that a
      +refspec-less `git fetch <name>` only fetches branches our local branches
     -+are built on, plus the remote's default branch. See the `--refmap` entry
     -+in linkgit:git-fetch[1] for details.
     ++are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
     ++details.
      ++
       With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
       up to point at remote's _<master>_ branch. See also the set-head command.
 4:  31462e9445 ! 4:  43b9711a2c remote: default to --limited-fetch in a shallow repository
     @@ Documentation/git-remote.adoc: Add a remote named _<name>_ for the repository at
      @@ Documentation/git-remote.adoc: With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
       that tracks all branches, `remote.<name>.refmap` is set up so that a
       refspec-less `git fetch <name>` only fetches branches our local branches
     - are built on, plus the remote's default branch. See the `--refmap` entry
     --in linkgit:git-fetch[1] for details.
     -+in linkgit:git-fetch[1] for details. `--no-limited-fetch` explicitly
     -+disables this, overriding the shallow-repository default described above.
     + are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
     +-details.
     ++details. `--no-limited-fetch` explicitly disables this, overriding the
     ++shallow-repository default described above.
       +
       With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
       up to point at remote's _<master>_ branch. See also the set-head command.
     @@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only
      +		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
      +			remote.upstream.refmap &&
      +		test_must_fail git config get remote.upstream.fetch &&
     ++		git fetch upstream main &&
     ++		git branch --set-upstream-to=upstream/main &&
     ++		test_cmp_config upstream branch.main.remote &&
     ++		test_cmp_config refs/heads/main branch.main.merge &&
      +		git fetch upstream &&
      +		git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
      +		cat >expect <<-\EOF &&
      +		refs/remotes/upstream/HEAD
      +		refs/remotes/upstream/main
      +		EOF
     -+		test_cmp expect actual &&
     -+		git branch --set-upstream-to=upstream &&
     -+		test_cmp_config upstream branch.main.remote &&
     -+		test_cmp_config refs/heads/main branch.main.merge
     ++		test_cmp expect actual
      +	)
      +'
      +
Signature · 1 line
gitgitgadget
#25 lore raw

PATCH v4 1/4 fetch: add remote.<name>.refmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Like --refmap itself, it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

---
 Documentation/config/remote.adoc |  5 +++++
 Documentation/fetch-options.adoc |  3 +++
 builtin/fetch.c                  |  8 +++++---
 remote.c                         | 12 +++++++++++-
 remote.h                         |  6 ++++++
 t/t5510-fetch.sh                 | 17 +++++++++++++++++
 6 files changed, 47 insertions(+), 4 deletions(-)
Documentation/config/remote.adoc +5 -0
index 3a20d0f752..103eada406 100644
@@ -33,6 +33,11 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Used to map remote refs being fetched to remote-tracking refs to
+	store. See the `--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 035f780e58..538914bc6e 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +5 -3
index 533fdfe7d8..7651b41139 100644
@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
 		for (i = 0; i < fetch_refspec->nr; i++)
 			get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
-	} else if (refmap.nr) {
+	} else if (effective_refmap && effective_refmap->nr) {
 		die("--refmap option is only meaningful with command-line refspec(s)");
 	} else {
 		/* Use the defaults */
remote.c +11 -1
index fe62068463..017cd9d13e 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	if (remote->fetch.nr)
+		return refspec_find_match(&remote->fetch, refspec);
+	return refspec_find_match(&remote->refmap, refspec);
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +17 -0
index a8d38d9176..bd853d0a84 100755
@@ -927,6 +927,23 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '
 	)
 '
 
+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '
+	test_when_finished "git -C three config --unset remote.origin.refmap" &&
+	git branch -f side &&
+	git -C three config remote.origin.refmap \
+		"refs/heads/*:refs/remotes/other/*" &&
+	(
+		cd three &&
+		git update-ref refs/remotes/origin/main base-origin-main &&
+		o=$(git rev-parse --verify refs/remotes/origin/main) &&
+		git fetch origin main &&
+		n=$(git rev-parse --verify refs/remotes/origin/main) &&
+		test "$o" = "$n" &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/side &&
+		git rev-parse --verify refs/remotes/other/main
+	)
+'
+
 test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '
 	git branch -f side &&
 	(
Signature · 1 line
gitgitgadget
#26 lore raw

PATCH v4 2/4 fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |   6 +-
 builtin/fetch.c                  |  52 ++++++++++++--
 remote.c                         |  29 ++++++++
 remote.h                         |   3 +
 t/meson.build                    |   1 +
 t/t5585-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 205 insertions(+), 10 deletions(-)
 create mode 100755 t/t5585-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..7c52e443a5 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
 	Used to map remote refs being fetched to remote-tracking refs to
-	store. See the `--refmap` entry in linkgit:git-fetch[1].
+	store. If `remote.<name>.fetch` is not set either, a refspec-less
+	fetch infers what to fetch from local branches built on this
+	remote, instead of fetching every branch it has. See the
+	`--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +4 -2
index 538914bc6e..315daf97d7 100644
@@ -245,8 +245,10 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+there is nothing to fetch, neither on the command line nor from
+`remote.<name>.fetch`, Git infers what to fetch from the local branches
+whose `@{upstream}` is on that remote.
 
 `-t`::
 `--tags`::
builtin/fetch.c +45 -7

99 lines not on this page · raw message

remote.c +29 -0
index 017cd9d13e..a5deb9b929 100644
@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+struct branches_tracking_remote_cb_data {
+	struct remote *remote;
+	struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+	struct branch *branch;
+
+	branch = branch_get(ref->name);
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, data->remote->name))
+		return 0;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(data->tracked, branch->merge[i]->src);
+
+	return 0;
+}
+
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(the_repository),
+				  add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +3 -0
index ac485a584d..da6c3ef53a 100644
@@ -359,6 +359,9 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index 3ca7b27104..ab83d5409c 100644
@@ -728,6 +728,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5585-fetch-refmap.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5585-fetch-refmap.sh new file +119 -0

119 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#27 lore raw

Re: [PATCH v4 2/4] fetch: infer branches to fetch from a refmap-only remote

t/t5585-fetch-refmap.sh line 43

Quoted 8 lines
t/t5585-fetch-refmap.sh new file +8 -0
+test_expect_success 'a bare fetch needs nothing until a branch is tracked' '
+       (
+               cd client &&
+               git fetch upstream &&
+               git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
+               test_must_be_empty actual
+       )
+'

I still don't think it makes sense that bare 'git fetch upstream' doesn't fetch the HEAD branch to then allow me to do

    git branch --set-upstream-to=upstream

But maybe that's for another topic.

Harald

#28 lore raw

Re: [PATCH v4 2/4] fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget
Quoted 1 line · Harald Nordgren via GitGitGadget
 t/t5585-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++

Please renumber. This number is taken by another topic already in flight, I think.

#29 lore raw

PATCH v4 3/4 remote: add "git remote add --limited-fetch"

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, plus the remote's default branch, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)
Documentation/git-remote.adoc +7 -1
index eaae30aa88..4255f8b3e6 100644
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [synopsis]
 git remote [-v | --verbose]
-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>
 git remote rename [--[no-]progress] <old> <new>
 git remote remove <name>
 git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_
 is created.  You can give more than one `-t <branch>` to track
 multiple branches without grabbing all branches.
 +
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
+details.
++
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
 +
builtin/remote.c +22 -6
index de989ea3ba..f036dd5d6f 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
+	int limited_fetch = -1; /* unspecified */
 	struct string_list track = STRING_LIST_INIT_NODUP;
 	const char *master = NULL;
 	struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
 		OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
 			N_("set up remote as a mirror to push to or fetch from"),
 			PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+		OPT_BOOL(0, "limited-fetch", &limited_fetch,
+			N_("fetch only the branches we build on, instead of every branch")),
 		OPT_END()
 	};
 
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
 		die(_("specifying a master branch makes no sense with --mirror"));
 	if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
 		die(_("specifying branches to track makes sense only with fetch mirrors"));
+	if (limited_fetch == 1 && track.nr)
+		die(_("--limited-fetch does not make sense with -t/--track"));
+	if (limited_fetch == 1 && mirror)
+		die(_("--limited-fetch does not make sense with --mirror"));
 
 	name = argv[0];
 	url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
 	repo_config_set(the_repository, buf.buf, url);
 
 	if (!mirror || mirror & MIRROR_FETCH) {
+		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+			limited_fetch == 1;
+
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "remote.%s.fetch", name);
-		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
-			add_branch(buf.buf, track.items[i].string,
-				   name, mirror, &buf2);
+		if (use_limited_fetch) {
+			strbuf_addf(&buf, "remote.%s.refmap", name);
+			strbuf_reset(&buf2);
+			strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+			repo_config_set(the_repository, buf.buf, buf2.buf);
+		} else {
+			strbuf_addf(&buf, "remote.%s.fetch", name);
+			if (track.nr == 0)
+				string_list_append(&track, "*");
+			for (size_t i = 0; i < track.nr; i++)
+				add_branch(buf.buf, track.items[i].string,
+					   name, mirror, &buf2);
 		}
 	}
 
t/t5505-remote.sh +16 -0
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
 	test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+	test_when_finished "rm -rf full-add" &&
+	git clone --no-local one full-add &&
+	(
+		cd full-add &&
+		git remote add --limited-fetch upstream ../two &&
+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+			remote.upstream.refmap &&
+		test_must_fail git config get remote.upstream.fetch
+	)
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+	test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
 	(
 		cd test &&
Signature · 1 line
gitgitgadget
#30 lore raw

PATCH v4 4/4 remote: default to --limited-fetch in a shallow repository

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index 4255f8b3e6..80f8a4a183 100644
@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
-details.
+details. `--no-limited-fetch` explicitly disables this, overriding the
+shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +4 -1
index f036dd5d6f..0cf1126976 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
 	if (!mirror || mirror & MIRROR_FETCH) {
 		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-			limited_fetch == 1;
+			(limited_fetch == 1 ||
+			 (limited_fetch == -1 &&
+			  is_repository_shallow(the_repository)));
 
 		strbuf_reset(&buf);
 		if (use_limited_fetch) {
t/t5505-remote.sh +60 -0

66 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#31 lore raw

Re: [PATCH v4 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget
Quoted 8 lines · Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Will replace.

As my eyes have been contaminated by and biased for this topic, I'll spend my time on other topics first to let my eyes and brain "forget" about it before I revisit these patches. I'd really welcome reviews with fresh eyes to happen in the meantime.

Thanks.

#32 lore raw

v5 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v5:

* Renumber test file from t5585 to t5586.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (4):
fetch: add remote.<name>.refmap
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |   5 ++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  60 +++++++++++++---
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  41 ++++++++++-
 remote.h                         |   9 +++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 |  17 +++++
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 363 insertions(+), 18 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

base-commit: a018953688f1b10bddf91bff8747068f5f4746a4
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v5
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v4:

 1:  d48a7004e4 = 1:  d48a7004e4 fetch: add remote.<name>.refmap
 2:  45b26e2bb2 ! 2:  fecdbc43b8 fetch: infer branches to fetch from a refmap-only remote
     @@ t/meson.build: integration_tests = [
         't5582-fetch-negative-refspec.sh',
         't5583-push-branches.sh',
         't5584-http-429-retry.sh',
     -+  't5585-fetch-refmap.sh',
     ++  't5586-fetch-refmap.sh',
         't5600-clone-fail-cleanup.sh',
         't5601-clone.sh',
         't5602-clone-remote-exec.sh',
      
     - ## t/t5585-fetch-refmap.sh (new) ##
     + ## t/t5586-fetch-refmap.sh (new) ##
      @@
      +#!/bin/sh
      +
 3:  e2f072c254 = 3:  b45f3cf0ab remote: add "git remote add --limited-fetch"
 4:  43b9711a2c = 4:  8c2a144eb7 remote: default to --limited-fetch in a shallow repository
Signature · 1 line
gitgitgadget
#33 lore raw

PATCH v5 1/4 fetch: add remote.<name>.refmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Like --refmap itself, it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

---
 Documentation/config/remote.adoc |  5 +++++
 Documentation/fetch-options.adoc |  3 +++
 builtin/fetch.c                  |  8 +++++---
 remote.c                         | 12 +++++++++++-
 remote.h                         |  6 ++++++
 t/t5510-fetch.sh                 | 17 +++++++++++++++++
 6 files changed, 47 insertions(+), 4 deletions(-)
Documentation/config/remote.adoc +5 -0
index 3a20d0f752..103eada406 100644
@@ -33,6 +33,11 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Used to map remote refs being fetched to remote-tracking refs to
+	store. See the `--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 035f780e58..538914bc6e 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +5 -3
index 533fdfe7d8..7651b41139 100644
@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
 		for (i = 0; i < fetch_refspec->nr; i++)
 			get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
-	} else if (refmap.nr) {
+	} else if (effective_refmap && effective_refmap->nr) {
 		die("--refmap option is only meaningful with command-line refspec(s)");
 	} else {
 		/* Use the defaults */
remote.c +11 -1
index fe62068463..017cd9d13e 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	if (remote->fetch.nr)
+		return refspec_find_match(&remote->fetch, refspec);
+	return refspec_find_match(&remote->refmap, refspec);
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +17 -0
index a8d38d9176..bd853d0a84 100755
@@ -927,6 +927,23 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '
 	)
 '
 
+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '
+	test_when_finished "git -C three config --unset remote.origin.refmap" &&
+	git branch -f side &&
+	git -C three config remote.origin.refmap \
+		"refs/heads/*:refs/remotes/other/*" &&
+	(
+		cd three &&
+		git update-ref refs/remotes/origin/main base-origin-main &&
+		o=$(git rev-parse --verify refs/remotes/origin/main) &&
+		git fetch origin main &&
+		n=$(git rev-parse --verify refs/remotes/origin/main) &&
+		test "$o" = "$n" &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/side &&
+		git rev-parse --verify refs/remotes/other/main
+	)
+'
+
 test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '
 	git branch -f side &&
 	(
Signature · 1 line
gitgitgadget
#34 lore raw

PATCH v5 2/4 fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |   6 +-
 builtin/fetch.c                  |  52 ++++++++++++--
 remote.c                         |  29 ++++++++
 remote.h                         |   3 +
 t/meson.build                    |   1 +
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 205 insertions(+), 10 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..7c52e443a5 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
 	Used to map remote refs being fetched to remote-tracking refs to
-	store. See the `--refmap` entry in linkgit:git-fetch[1].
+	store. If `remote.<name>.fetch` is not set either, a refspec-less
+	fetch infers what to fetch from local branches built on this
+	remote, instead of fetching every branch it has. See the
+	`--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +4 -2
index 538914bc6e..315daf97d7 100644
@@ -245,8 +245,10 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+there is nothing to fetch, neither on the command line nor from
+`remote.<name>.fetch`, Git infers what to fetch from the local branches
+whose `@{upstream}` is on that remote.
 
 `-t`::
 `--tags`::
builtin/fetch.c +45 -7

99 lines not on this page · raw message

remote.c +29 -0
index 017cd9d13e..a5deb9b929 100644
@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+struct branches_tracking_remote_cb_data {
+	struct remote *remote;
+	struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+	struct branch *branch;
+
+	branch = branch_get(ref->name);
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, data->remote->name))
+		return 0;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(data->tracked, branch->merge[i]->src);
+
+	return 0;
+}
+
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(the_repository),
+				  add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +3 -0
index ac485a584d..da6c3ef53a 100644
@@ -359,6 +359,9 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index 3ca7b27104..a1c7465c2b 100644
@@ -728,6 +728,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5586-fetch-refmap.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5586-fetch-refmap.sh new file +119 -0

119 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#35 lore raw

PATCH v5 3/4 remote: add "git remote add --limited-fetch"

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, plus the remote's default branch, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)
Documentation/git-remote.adoc +7 -1
index eaae30aa88..4255f8b3e6 100644
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [synopsis]
 git remote [-v | --verbose]
-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>
 git remote rename [--[no-]progress] <old> <new>
 git remote remove <name>
 git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_
 is created.  You can give more than one `-t <branch>` to track
 multiple branches without grabbing all branches.
 +
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
+details.
++
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
 +
builtin/remote.c +22 -6
index de989ea3ba..f036dd5d6f 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
+	int limited_fetch = -1; /* unspecified */
 	struct string_list track = STRING_LIST_INIT_NODUP;
 	const char *master = NULL;
 	struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
 		OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
 			N_("set up remote as a mirror to push to or fetch from"),
 			PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+		OPT_BOOL(0, "limited-fetch", &limited_fetch,
+			N_("fetch only the branches we build on, instead of every branch")),
 		OPT_END()
 	};
 
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
 		die(_("specifying a master branch makes no sense with --mirror"));
 	if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
 		die(_("specifying branches to track makes sense only with fetch mirrors"));
+	if (limited_fetch == 1 && track.nr)
+		die(_("--limited-fetch does not make sense with -t/--track"));
+	if (limited_fetch == 1 && mirror)
+		die(_("--limited-fetch does not make sense with --mirror"));
 
 	name = argv[0];
 	url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
 	repo_config_set(the_repository, buf.buf, url);
 
 	if (!mirror || mirror & MIRROR_FETCH) {
+		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+			limited_fetch == 1;
+
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "remote.%s.fetch", name);
-		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
-			add_branch(buf.buf, track.items[i].string,
-				   name, mirror, &buf2);
+		if (use_limited_fetch) {
+			strbuf_addf(&buf, "remote.%s.refmap", name);
+			strbuf_reset(&buf2);
+			strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+			repo_config_set(the_repository, buf.buf, buf2.buf);
+		} else {
+			strbuf_addf(&buf, "remote.%s.fetch", name);
+			if (track.nr == 0)
+				string_list_append(&track, "*");
+			for (size_t i = 0; i < track.nr; i++)
+				add_branch(buf.buf, track.items[i].string,
+					   name, mirror, &buf2);
 		}
 	}
 
t/t5505-remote.sh +16 -0
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
 	test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+	test_when_finished "rm -rf full-add" &&
+	git clone --no-local one full-add &&
+	(
+		cd full-add &&
+		git remote add --limited-fetch upstream ../two &&
+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+			remote.upstream.refmap &&
+		test_must_fail git config get remote.upstream.fetch
+	)
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+	test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
 	(
 		cd test &&
Signature · 1 line
gitgitgadget
#36 lore raw

Re: [PATCH v5 3/4] remote: add "git remote add --limited-fetch"

Quoted 8 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Quoted 6 lines

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, plus the remote's default branch, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

You forgot to remove ", plus the ... in the previous commit", probably, when you updated the series to stop doing so in the step [2/4].

#37 lore raw

PATCH v5 4/4 remote: default to --limited-fetch in a shallow repository

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index 4255f8b3e6..80f8a4a183 100644
@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
-details.
+details. `--no-limited-fetch` explicitly disables this, overriding the
+shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +4 -1
index f036dd5d6f..0cf1126976 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
 	if (!mirror || mirror & MIRROR_FETCH) {
 		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-			limited_fetch == 1;
+			(limited_fetch == 1 ||
+			 (limited_fetch == -1 &&
+			  is_repository_shallow(the_repository)));
 
 		strbuf_reset(&buf);
 		if (use_limited_fetch) {
t/t5505-remote.sh +60 -0

66 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#38 lore raw

v6 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v6:

* Remove leftover reference to deleted default-branch logic in commit message.

Changes in v5:

* Renumber test file from t5585 to t5586.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (4):
fetch: add remote.<name>.refmap
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |   5 ++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  60 +++++++++++++---
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  41 ++++++++++-
 remote.h                         |   9 +++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 |  17 +++++
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 363 insertions(+), 18 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

base-commit: c46c1e37724f0478939de636ab8ea5a89086d532
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v6
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v5:

 1:  d48a7004e4 = 1:  5c31a51bb7 fetch: add remote.<name>.refmap
 2:  fecdbc43b8 = 2:  74c177e3e2 fetch: infer branches to fetch from a refmap-only remote
 3:  b45f3cf0ab ! 3:  3abcc8915b remote: add "git remote add --limited-fetch"

@@ Commit message

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that
- "git fetch <name>" only fetches the branches already tracked, plus
- the remote's default branch, as described in the previous commit.
- It is rejected together with -t/--track or --mirror, since those
- already say explicitly what to fetch.
+ "git fetch <name>" only fetches the branches already tracked, as
+ described in the previous commit. It is rejected together with
+ -t/--track or --mirror, since those already say explicitly what to
+ fetch.

4: 8c2a144eb7 = 4: 84d192445c remote: default to --limited-fetch in a shallow repository

Signature · 1 line
gitgitgadget
#39 lore raw

PATCH v6 1/4 fetch: add remote.<name>.refmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Like --refmap itself, it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

---
 Documentation/config/remote.adoc |  5 +++++
 Documentation/fetch-options.adoc |  3 +++
 builtin/fetch.c                  |  8 +++++---
 remote.c                         | 12 +++++++++++-
 remote.h                         |  6 ++++++
 t/t5510-fetch.sh                 | 17 +++++++++++++++++
 6 files changed, 47 insertions(+), 4 deletions(-)
Documentation/config/remote.adoc +5 -0
index 3a20d0f752..103eada406 100644
@@ -33,6 +33,11 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Used to map remote refs being fetched to remote-tracking refs to
+	store. See the `--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 035f780e58..538914bc6e 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +5 -3
index b2decc6cfd..04d0a78ecf 100644
@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
 		for (i = 0; i < fetch_refspec->nr; i++)
 			get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
-	} else if (refmap.nr) {
+	} else if (effective_refmap && effective_refmap->nr) {
 		die("--refmap option is only meaningful with command-line refspec(s)");
 	} else {
 		/* Use the defaults */
remote.c +11 -1
index fe62068463..017cd9d13e 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	if (remote->fetch.nr)
+		return refspec_find_match(&remote->fetch, refspec);
+	return refspec_find_match(&remote->refmap, refspec);
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +17 -0
index 300bd5396d..36fa9319e1 100755
@@ -927,6 +927,23 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '
 	)
 '
 
+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '
+	test_when_finished "git -C three config --unset remote.origin.refmap" &&
+	git branch -f side &&
+	git -C three config remote.origin.refmap \
+		"refs/heads/*:refs/remotes/other/*" &&
+	(
+		cd three &&
+		git update-ref refs/remotes/origin/main base-origin-main &&
+		o=$(git rev-parse --verify refs/remotes/origin/main) &&
+		git fetch origin main &&
+		n=$(git rev-parse --verify refs/remotes/origin/main) &&
+		test "$o" = "$n" &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/side &&
+		git rev-parse --verify refs/remotes/other/main
+	)
+'
+
 test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '
 	git branch -f side &&
 	(
Signature · 1 line
gitgitgadget
#40 lore raw

PATCH v6 2/4 fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |   6 +-
 builtin/fetch.c                  |  52 ++++++++++++--
 remote.c                         |  29 ++++++++
 remote.h                         |   3 +
 t/meson.build                    |   1 +
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 205 insertions(+), 10 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..7c52e443a5 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
 	Used to map remote refs being fetched to remote-tracking refs to
-	store. See the `--refmap` entry in linkgit:git-fetch[1].
+	store. If `remote.<name>.fetch` is not set either, a refspec-less
+	fetch infers what to fetch from local branches built on this
+	remote, instead of fetching every branch it has. See the
+	`--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +4 -2
index 538914bc6e..315daf97d7 100644
@@ -245,8 +245,10 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+there is nothing to fetch, neither on the command line nor from
+`remote.<name>.fetch`, Git infers what to fetch from the local branches
+whose `@{upstream}` is on that remote.
 
 `-t`::
 `--tags`::
builtin/fetch.c +45 -7

99 lines not on this page · raw message

remote.c +29 -0
index 017cd9d13e..a5deb9b929 100644
@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+struct branches_tracking_remote_cb_data {
+	struct remote *remote;
+	struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+	struct branch *branch;
+
+	branch = branch_get(ref->name);
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, data->remote->name))
+		return 0;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(data->tracked, branch->merge[i]->src);
+
+	return 0;
+}
+
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(the_repository),
+				  add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +3 -0
index ac485a584d..da6c3ef53a 100644
@@ -359,6 +359,9 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index f65eb04684..e287a6b947 100644
@@ -730,6 +730,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5586-fetch-refmap.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5586-fetch-refmap.sh new file +119 -0

119 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#41 lore raw

PATCH v6 3/4 remote: add "git remote add --limited-fetch"

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)
Documentation/git-remote.adoc +7 -1
index eaae30aa88..4255f8b3e6 100644
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [synopsis]
 git remote [-v | --verbose]
-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>
 git remote rename [--[no-]progress] <old> <new>
 git remote remove <name>
 git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_
 is created.  You can give more than one `-t <branch>` to track
 multiple branches without grabbing all branches.
 +
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
+details.
++
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
 +
builtin/remote.c +22 -6
index de989ea3ba..f036dd5d6f 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
+	int limited_fetch = -1; /* unspecified */
 	struct string_list track = STRING_LIST_INIT_NODUP;
 	const char *master = NULL;
 	struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
 		OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
 			N_("set up remote as a mirror to push to or fetch from"),
 			PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+		OPT_BOOL(0, "limited-fetch", &limited_fetch,
+			N_("fetch only the branches we build on, instead of every branch")),
 		OPT_END()
 	};
 
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
 		die(_("specifying a master branch makes no sense with --mirror"));
 	if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
 		die(_("specifying branches to track makes sense only with fetch mirrors"));
+	if (limited_fetch == 1 && track.nr)
+		die(_("--limited-fetch does not make sense with -t/--track"));
+	if (limited_fetch == 1 && mirror)
+		die(_("--limited-fetch does not make sense with --mirror"));
 
 	name = argv[0];
 	url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
 	repo_config_set(the_repository, buf.buf, url);
 
 	if (!mirror || mirror & MIRROR_FETCH) {
+		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+			limited_fetch == 1;
+
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "remote.%s.fetch", name);
-		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
-			add_branch(buf.buf, track.items[i].string,
-				   name, mirror, &buf2);
+		if (use_limited_fetch) {
+			strbuf_addf(&buf, "remote.%s.refmap", name);
+			strbuf_reset(&buf2);
+			strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+			repo_config_set(the_repository, buf.buf, buf2.buf);
+		} else {
+			strbuf_addf(&buf, "remote.%s.fetch", name);
+			if (track.nr == 0)
+				string_list_append(&track, "*");
+			for (size_t i = 0; i < track.nr; i++)
+				add_branch(buf.buf, track.items[i].string,
+					   name, mirror, &buf2);
 		}
 	}
 
t/t5505-remote.sh +16 -0
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
 	test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+	test_when_finished "rm -rf full-add" &&
+	git clone --no-local one full-add &&
+	(
+		cd full-add &&
+		git remote add --limited-fetch upstream ../two &&
+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+			remote.upstream.refmap &&
+		test_must_fail git config get remote.upstream.fetch
+	)
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+	test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
 	(
 		cd test &&
Signature · 1 line
gitgitgadget
#42 lore raw

PATCH v6 4/4 remote: default to --limited-fetch in a shallow repository

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index 4255f8b3e6..80f8a4a183 100644
@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
-details.
+details. `--no-limited-fetch` explicitly disables this, overriding the
+shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +4 -1
index f036dd5d6f..0cf1126976 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
 	if (!mirror || mirror & MIRROR_FETCH) {
 		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-			limited_fetch == 1;
+			(limited_fetch == 1 ||
+			 (limited_fetch == -1 &&
+			  is_repository_shallow(the_repository)));
 
 		strbuf_reset(&buf);
 		if (use_limited_fetch) {
t/t5505-remote.sh +60 -0

66 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#43 lore raw

Re: [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget
Quoted 6 lines · Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v6:

* Remove leftover reference to deleted default-branch logic in commit message.

Thanks. I think this is becoming much better, but I see one glitch and one design question, for which I do not yet know the right answer.

Before going there, since one of the test scripts added by this series is called 'fetch refmap', we should have a test or two to check its more basic use.

When the user configures remote.origin.refmap, the command should behave as if --refmap were given on the command line, even when the repository does not yet have a local branch that builds on anything from the remote. Attached is my attempt to do so. It does multiple things in a single block, which we may want to split up, but I am sending it here to illustrate what we might want to test and, more importantly, to present a scenario that exposes both the design question and the glitch.

The early part of the scenario goes like this:

* We create a new repository and add ".." as a remote.
* We remove remote.origin.fetch and set up remote.origin.refmap.
* When we run "git fetch origin", nothing is fetched because nothing yet builds on what we would fetch from them.

If you try to run this with [1/4] alone, however, it errors out with "fatal: --refmap option is only meaningful with command-line refspec", which is suboptimal when triggered by a configuration variable. Even though our design says that remote.*.refmap makes the command behave as if the user gave '--refmap' on the command line, applying that rule here is a bit too strict.

Fortunately, this is rectified later in the series when we begin tracking which of our local branches build on what we get from them. Even when the number of branches to fetch is zero, meaning we should pretend no command-line refspec was given with --refmap, we no longer get the same error, which is good.

The second part of the scenario explicitly specifies what to fetch on the command line and verifies that we fetch exactly that.

Then there is the last part, where the desired behavior is unclear. What should happen if the remote.origin.* configuration defines both fetch and refmap? How would we explain our choice to the users? I do not have a good answer to this design question.

As for the glitch, the last part of the test below dies with the "fatal: --refmap option is only meaningful..." message when run with the current patchset. We might decide to error out if both are set. Alternatively, we could ignore .refmap and use .fetch, or ignore .fetch and use .refmap. Whatever we decide, the "fatal: --refmap option is only meaningful..." error is not the right message to show in this situation.

Thoughts?

 t/t5586-fetch-refmap.sh | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)
t/t5586-fetch-refmap.sh +29 -0
index b81fc48cbe..30a8d79c16 100755
@@ -22,6 +22,35 @@ test_expect_success 'setup' '
 	git checkout main
 '
 
+test_expect_success 'remote.<name>.refmap without tracking (baseline)' '
+	test_when_finished "rm -fr fetch-refmap-baseline" &&
+	git init fetch-refmap-baseline &&
+	(
+		cd fetch-refmap-baseline &&
+		git remote add origin ../ &&
+
+		# without fetch refspec, but with fetch refmap
+		git config --unset-all remote.origin.fetch &&
+		git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" &&
+
+		# nothing tracked, nothing fetched, no error
+		git fetch origin 2>error &&
+		test_grep ! "fatal: --refmap option is only meaningful" error &&
+		git for-each-ref --format="%(refname)" refs/remotes/ >actual &&
+		test_line_count = 0 actual &&
+
+		# nothing tracked, explicit ref on the command line
+		git fetch origin main &&
+		git for-each-ref --format="%(refname)" refs/remotes/ >actual &&
+		echo refs/remotes/origin/main >expect &&
+		test_cmp expect actual &&
+
+		# what should happen when we have both refmap and refspec?
+		git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" &&
+		git fetch origin
+	)
+'
+
 test_expect_success 'clone shallow and single-branch, then add a second remote' '
 	git clone --no-local --depth=1 --branch main --single-branch . client &&
 	(
#44 lore raw

Re: [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo

Quoted 6 lines

If you try to run this with [1/4] alone, however, it errors out with "fatal: --refmap option is only meaningful with command-line refspec", which is suboptimal when triggered by a configuration variable. Even though our design says that remote.*.refmap makes the command behave as if the user gave '--refmap' on the command line, applying that rule here is a bit too strict.

Thanks for pointing that out. How did you find that?

Quoted 4 lines

Then there is the last part, where the desired behavior is unclear. What should happen if the remote.origin.* configuration defines both fetch and refmap? How would we explain our choice to the users? I do not have a good answer to this design question.

I think that when both remote.<name>.fetch and remote.<name>.refmap are configured, remote.<name>.fetch can win.

Quoted 17 lines
t/t5586-fetch-refmap.sh +17 -0
+test_expect_success 'remote.<name>.refmap without tracking (baseline)' '
+       test_when_finished "rm -fr fetch-refmap-baseline" &&
+       git init fetch-refmap-baseline &&
+       (
+               cd fetch-refmap-baseline &&
+               git remote add origin ../ &&
+
+               # without fetch refspec, but with fetch refmap
+               git config --unset-all remote.origin.fetch &&
+               git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" &&
+
+               # nothing tracked, nothing fetched, no error
+               git fetch origin 2>error &&
+               test_grep ! "fatal: --refmap option is only meaningful" error &&
+               git for-each-ref --format="%(refname)" refs/remotes/ >actual &&
+               test_line_count = 0 actual &&
+

t/t5586-fetch-refmap.sh test_expect_success 'setup' ' · line 51

Quoted 10 lines
t/t5586-fetch-refmap.sh +10 -0
+               # nothing tracked, explicit ref on the command line
+               git fetch origin main &&
+               git for-each-ref --format="%(refname)" refs/remotes/ >actual &&
+               echo refs/remotes/origin/main >expect &&
+               test_cmp expect actual &&
+
+               # what should happen when we have both refmap and refspec?
+               git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" &&
+               git fetch origin
+       )

Thanks for providing this!

Harald

#45 lore raw

Re: [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo

Quoted 8 lines · Harald Nordgren
Quoted 6 lines

If you try to run this with [1/4] alone, however, it errors out with "fatal: --refmap option is only meaningful with command-line refspec", which is suboptimal when triggered by a configuration variable. Even though our design says that remote.*.refmap makes the command behave as if the user gave '--refmap' on the command line, applying that rule here is a bit too strict.

Thanks for pointing that out. How did you find that?

By thinking the process through while writing these very basic tests.

I think this topic textually interacts with Collin's followRemoteHAD work. I tried to be careful while resolving the conflicts when I merged both to 'seen', but please double check the result when I push it out.

#46 lore raw

Re: [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo

Quoted 4 lines

I think this topic textually interacts with Collin's followRemoteHAD work. I tried to be careful while resolving the conflicts when I merged both to 'seen', but please double check the result when I push it out.

I range-diffed against 'seen' and everything looks normal.

I have a new version ready to send out, do you want me to hold off?

Harald

#47 lore raw

v7 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v7:

* Fixed fatal: --refmap option is only meaningful with command-line refspec(s) firing just from configuring remote.<name>.refmap alone, with nothing else to fetch.
* When both remote.<name>.fetch and remote.<name>.refmap are configured, a refspec-less fetch follows .fetch as usual, .refmap only remaps something already being fetched by name. Added tests.

Changes in v6:

* Remove leftover reference to deleted default-branch logic in commit message.

Changes in v5:

* Renumber test file from t5585 to t5586.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (4):
fetch: add remote.<name>.refmap
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |   5 ++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  58 ++++++++++++---
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  41 ++++++++++-
 remote.h                         |   9 +++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 |  63 ++++++++++++++++
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 408 insertions(+), 17 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

base-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v7
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v7
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v6:

1: 5c31a51bb7 ! 1: b5db64d56f fetch: add remote.<name>.refmap

     @@ Commit message
          to do when there is nothing explicit to fetch, on the command line or
          via remote.<name>.fetch.
      
     +    A refspec-less fetch with a refmap configured but nothing else to say
     +    what to fetch falls back to the same defaults as a remote with no
     +    refspec or refmap at all. Only an explicit --refmap given on the
     +    command line with no command-line refspec to go with it is still
     +    rejected, since that combination is a plain mistake to type, unlike a
     +    remote.<name>.refmap configured on its own. A remote.<name>.fetch
     +    that is also configured is used as before.
     +
          Signed-off-by: Harald Nordgren <[email protected]>
      
       ## Documentation/config/remote.adoc ##
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       		else
       			fetch_refspec = &remote->fetch;
       
     - 		for (i = 0; i < fetch_refspec->nr; i++)
     - 			get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
     --	} else if (refmap.nr) {
     -+	} else if (effective_refmap && effective_refmap->nr) {
     - 		die("--refmap option is only meaningful with command-line refspec(s)");
     - 	} else {
     - 		/* Use the defaults */
      
       ## remote.c ##
      @@ remote.c: static struct remote *make_remote(struct remote_state *remote_state,
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
      +		git rev-parse --verify refs/remotes/other/main
      +	)
      +'
     ++
     ++check_fetched_refs () {
     ++	git for-each-ref --format="%(refname)" refs/remotes/ >actual &&
     ++	cat >expect &&
     ++	test_cmp expect actual
     ++}
     ++
     ++test_expect_success 'remote.<name>.refmap without tracking (baseline)' '
     ++	test_when_finished "rm -fr fetch-refmap-baseline fetch-refmap-upstream" &&
     ++	git init -b main fetch-refmap-upstream &&
     ++	test_commit -C fetch-refmap-upstream base &&
     ++	git -C fetch-refmap-upstream branch other &&
     ++	git init fetch-refmap-baseline &&
     ++	(
     ++		cd fetch-refmap-baseline &&
     ++		git remote add origin ../fetch-refmap-upstream &&
     ++
     ++		# Without fetch refspec, but with fetch refmap.
     ++		git config --unset-all remote.origin.fetch &&
     ++		git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" &&
     ++
     ++		# Nothing tracked, nothing fetched, no error.
     ++		git fetch origin &&
     ++		check_fetched_refs <<-\EOF &&
     ++		EOF
     ++
     ++		# Nothing tracked, explicit ref on the command line.
     ++		git fetch origin main &&
     ++		check_fetched_refs <<-\EOF &&
     ++		refs/remotes/origin/main
     ++		EOF
     ++
     ++		# With both refmap and fetch configured, remote.<name>.fetch
     ++		# wins: a refspec-less fetch follows it as usual, and
     ++		# remote.<name>.refmap plays no part in deciding what to
     ++		# fetch, only in remapping something that is already being
     ++		# fetched by name.
     ++		git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" &&
     ++		git fetch origin &&
     ++		check_fetched_refs <<-\EOF
     ++		refs/remotes/origin/HEAD
     ++		refs/remotes/origin/main
     ++		refs/remotes/origin/other
     ++		EOF
     ++	)
     ++'
      +
       test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '
       	git branch -f side &&
 2:  74c177e3e2 ! 2:  fd6864daaf fetch: infer branches to fetch from a refmap-only remote
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       			get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
      +	} else if (inferred_branches) {
      +		/* Already fully handled above. */
     - 	} else if (effective_refmap && effective_refmap->nr) {
     + 	} else if (refmap.nr) {
       		die("--refmap option is only meaningful with command-line refspec(s)");
       	} else {
      @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
 3:  3abcc8915b = 3:  a2208875b6 remote: add "git remote add --limited-fetch"
 4:  84d192445c = 4:  c8fd073de3 remote: default to --limited-fetch in a shallow repository
Signature · 1 line
gitgitgadget
#48 lore raw

PATCH v7 1/4 fetch: add remote.<name>.refmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Like --refmap itself, it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

A refspec-less fetch with a refmap configured but nothing else to say what to fetch falls back to the same defaults as a remote with no refspec or refmap at all. Only an explicit --refmap given on the command line with no command-line refspec to go with it is still rejected, since that combination is a plain mistake to type, unlike a remote.<name>.refmap configured on its own. A remote.<name>.fetch that is also configured is used as before.

---
 Documentation/config/remote.adoc |  5 +++
 Documentation/fetch-options.adoc |  3 ++
 builtin/fetch.c                  |  6 ++-
 remote.c                         | 12 +++++-
 remote.h                         |  6 +++
 t/t5510-fetch.sh                 | 63 ++++++++++++++++++++++++++++++++
 6 files changed, 92 insertions(+), 3 deletions(-)
Documentation/config/remote.adoc +5 -0
index 3a20d0f752..103eada406 100644
@@ -33,6 +33,11 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Used to map remote refs being fetched to remote-tracking refs to
+	store. See the `--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 47dea1de8e..c2101a7b39 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +4 -2
index b2decc6cfd..b0ad8c0b5f 100644
@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -552,8 +554,8 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
remote.c +11 -1
index 71170f36a9..99a086ea5a 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	if (remote->fetch.nr)
+		return refspec_find_match(&remote->fetch, refspec);
+	return refspec_find_match(&remote->refmap, refspec);
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +63 -0

69 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#49 lore raw

PATCH v7 2/4 fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |   6 +-
 builtin/fetch.c                  |  52 ++++++++++++--
 remote.c                         |  29 ++++++++
 remote.h                         |   3 +
 t/meson.build                    |   1 +
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 205 insertions(+), 10 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..7c52e443a5 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
 	Used to map remote refs being fetched to remote-tracking refs to
-	store. See the `--refmap` entry in linkgit:git-fetch[1].
+	store. If `remote.<name>.fetch` is not set either, a refspec-less
+	fetch infers what to fetch from local branches built on this
+	remote, instead of fetching every branch it has. See the
+	`--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +4 -2
index c2101a7b39..75899d91cc 100644
@@ -245,8 +245,10 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+there is nothing to fetch, neither on the command line nor from
+`remote.<name>.fetch`, Git infers what to fetch from the local branches
+whose `@{upstream}` is on that remote.
 
 `-t`::
 `--tags`::
builtin/fetch.c +45 -7

99 lines not on this page · raw message

remote.c +29 -0
index 99a086ea5a..c26312beea 100644
@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+struct branches_tracking_remote_cb_data {
+	struct remote *remote;
+	struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+	struct branch *branch;
+
+	branch = branch_get(ref->name);
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, data->remote->name))
+		return 0;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(data->tracked, branch->merge[i]->src);
+
+	return 0;
+}
+
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(the_repository),
+				  add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +3 -0
index ac485a584d..da6c3ef53a 100644
@@ -359,6 +359,9 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index f65eb04684..e287a6b947 100644
@@ -730,6 +730,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5586-fetch-refmap.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5586-fetch-refmap.sh new file +119 -0

119 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#50 lore raw

Re: [PATCH v7 2/4] fetch: infer branches to fetch from a refmap-only remote

Quoted 7 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Quoted 5 lines

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

Nicely explained what we want out of this new feature.

Documentation/config/remote.adoc remote.<name>.fetch:: · line 42

Quoted 8 lines

remote.<name>.refmap::

Documentation/config/remote.adoc +4 -1
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
 	Used to map remote refs being fetched to remote-tracking refs to
-	store. See the `--refmap` entry in linkgit:git-fetch[1].
+	store. If `remote.<name>.fetch` is not set either, a refspec-less
+	fetch infers what to fetch from local branches built on this
+	remote, instead of fetching every branch it has. See the
+	`--refmap` entry in linkgit:git-fetch[1].

OK.

Quoted 5 lines
Documentation/fetch-options.adoc +0 -0
index c2101a7b39..75899d91cc 100644

Documentation/fetch-options.adoc endif::git-pull[] · line 251

Quoted 9 lines
Documentation/fetch-options.adoc +4 -2
@@ -245,8 +245,10 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+there is nothing to fetch, neither on the command line nor from
+`remote.<name>.fetch`, Git infers what to fetch from the local branches
+whose `@{upstream}` is on that remote.

To say "infers what to fetch" without explicitly saying how the inference is made is not sufficient in a technical manual. Even a reading like "up to three branches that our local branches have as '@{u}'" is possible, if not very probable.

Perhaps something like this instead:

... but nothing to fetch is specified on the command line, branches from the remote that are used as '@{upstream}' of our local branches are fetched.

[...]

Quoted 27 lines
Documentation/fetch-options.adoc +17 -4
@@ -1960,15 +1982,30 @@ static int do_fetch(struct transport *transport,
 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
 	} else {
 		struct branch *branch = branch_get(NULL);
-
-		if (transport->remote->fetch.nr) {
+		int tracks_this_remote = branch && branch_has_merge_config(branch) &&
+			!strcmp(branch->remote_name, transport->remote->name);
+		struct refspec *effective_refmap = refmap.nr ? &refmap :
+			&transport->remote->refmap;
+		int inferred_branches = !transport->remote->fetch.nr &&
+			effective_refmap->nr;
+
+		if (inferred_branches) {
+			struct string_list tracked = STRING_LIST_INIT_DUP;
+			struct string_list_item *item;
+
+			branches_tracking_remote(transport->remote, &tracked);
+			for_each_string_list_item(item, &tracked)
+				strvec_push(&transport_ls_refs_options.ref_prefixes,
+					    item->string);
+			string_list_clear(&tracked, 0);
+		} else if (transport->remote->fetch.nr) {
 			refspec_ref_prefixes(&transport->remote->fetch,
 					     &transport_ls_refs_options.ref_prefixes);
-			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
-				do_set_head = 1;

Documentation/fetch-options.adoc static int do_fetch(struct transport *transport, · line 2011

Quoted 10 lines
Documentation/fetch-options.adoc +4 -2
 		}
-		if (branch && branch_has_merge_config(branch) &&
-		    !strcmp(branch->remote_name, transport->remote->name)) {
+		if ((transport->remote->fetch.nr || inferred_branches) &&
+		    follow_remote_head != FOLLOW_REMOTE_NEVER)
+			do_set_head = 1;
+		if (tracks_this_remote) {
 			int i;
 			for (i = 0; i < branch->merge_nr; i++) {
 				strvec_push(&transport_ls_refs_options.ref_prefixes,

The unified diff is a bit messy to compare the before-and-after behaviour, so let's see what the preimage said first.

Documentation/fetch-options.adoc

Quoted 13 lines
Documentation/fetch-options.adoc +0 -6
 		struct branch *branch = branch_get(NULL);
-
-		if (transport->remote->fetch.nr) {
 			refspec_ref_prefixes(&transport->remote->fetch,
 					     &transport_ls_refs_options.ref_prefixes);
-			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
-				do_set_head = 1;
 		}
-		if (branch && branch_has_merge_config(branch) &&
-		    !strcmp(branch->remote_name, transport->remote->name)) {
 			int i;
 			for (i = 0; i < branch->merge_nr; i++) {
 				strvec_push(&transport_ls_refs_options.ref_prefixes,

So, when !rs->nr (i.e., nothing given on the command line to be fetched) and there is no fetch refspec, we checked the current branch and if it has merge config to merge from branches at the remote, we automatically fetched them. This is the world order before this "infer with @{u} and refmap" work, and we should behave the same way when remote.*.refmap is not set.

Let's see what the postimage says.

Documentation/fetch-options.adoc

Quoted 3 lines
Documentation/fetch-options.adoc +2 -0
 		struct branch *branch = branch_get(NULL);
+		int tracks_this_remote = branch && branch_has_merge_config(branch) &&
+			!strcmp(branch->remote_name, transport->remote->name);

This is the "does the current branch pull branches from the remote we are working with right now?" condition we saw in the original.

Documentation/fetch-options.adoc

Quoted 2 lines
Documentation/fetch-options.adoc +2 -0
+		struct refspec *effective_refmap = refmap.nr ? &refmap :
+			&transport->remote->refmap;

It is a bit annoying that refmap is a file scope static variable but here we say "The value of the --refmap option from the command line, or the value remote.*.refmap otherwise".

Documentation/fetch-options.adoc

Quoted 2 lines
Documentation/fetch-options.adoc +2 -0
+		int inferred_branches = !transport->remote->fetch.nr &&
+			effective_refmap->nr;

This variable tells the code that it must infer branches, but named as if it were a list of branches that were inferred. "When remote.*.fetch does not exist and we have the refmap to use for inferring".

Documentation/fetch-options.adoc

Quoted 13 lines
Documentation/fetch-options.adoc +10 -0
+		if (inferred_branches) {
+			struct string_list tracked = STRING_LIST_INIT_DUP;
+			struct string_list_item *item;
+
+			branches_tracking_remote(transport->remote, &tracked);
+			for_each_string_list_item(item, &tracked)
+				strvec_push(&transport_ls_refs_options.ref_prefixes,
+					    item->string);
+			string_list_clear(&tracked, 0);
+		} else if (transport->remote->fetch.nr) {
 			refspec_ref_prefixes(&transport->remote->fetch,
 					     &transport_ls_refs_options.ref_prefixes);
 		}

It would have been much easier to follow if the existing code came first to make it clear that the new code is an add-on. After all, when transport->remote->fetch.nr is true, inferred_branches is never true.

Documentation/fetch-options.adoc

Quoted 7 lines
Documentation/fetch-options.adoc +4 -0
+		if ((transport->remote->fetch.nr || inferred_branches) &&
+		    follow_remote_head != FOLLOW_REMOTE_NEVER)
+			do_set_head = 1;
+		if (tracks_this_remote) {
 			int i;
 			for (i = 0; i < branch->merge_nr; i++) {
 				strvec_push(&transport_ls_refs_options.ref_prefixes,

How does tracks_this_remote and inferred_branches interact? Doesn't the old code that grabs necessary remote-tracking branches for the current branch add the same branch from the remote? Doesn't @{u} for the current branch added twice on the list of branches to fetch?

Documentation/fetch-options.adoc static int do_fetch(struct transport *transport, · line 2051

Quoted 7 lines
Documentation/fetch-options.adoc +1 -0
@@ -2009,6 +2046,7 @@ static int do_fetch(struct transport *transport,
 
 	ref_map = get_ref_map(transport->remote, remote_refs, rs,
 			      tags, &autotags);
+
 	if (!update_head_ok)
 		check_not_current_branch(ref_map);

Useless patch noise.

Quoted 9 lines
remote.c +1 -0
index 99a086ea5a..c26312beea 100644
@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+struct branches_tracking_remote_cb_data {

remote.c int branch_merge_matches(struct branch *branch, · line 1897

Quoted 10 lines
remote.c +10 -0
+	struct remote *remote;
+	struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+	struct branch *branch;
+
+	branch = branch_get(ref->name);

I know branch_get() is defined here and allows implicit use of the_repository, but can't we pass "struct repository *" around in cb_data so that we can use repo_branch_get() here?

remote.c int branch_merge_matches(struct branch *branch, · line 1906

Quoted 9 lines
remote.c +9 -0
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, data->remote->name))
+		return 0;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(data->tracked, branch->merge[i]->src);
+
+	return 0;
+}

This is more or less identical to the "if current branch integrates with branches from the remote, then fetch them" code we saw earlier in the builtin/fetch.c:do_fetch() above. I notice that its return value is meaningless, as it always returns 0.

Stepping back a bit, because your new logic would become superset of what we already have to support the current branch when refmap is used, would it make sense to restructure the code change to do_fetch() more like this:

	if (rs->nr) {
		... use command line refspec ...
	} else if (transport->remote->fetch.nr) {
		... use remote.*.fetch refspec ...
	} else if (effective_refmap->nr) {
		... your new logic ...
	} else {
		struct string_list list = STRING_LIST_INIT;
		collect_upstream_from_remote(&list, remote, NULL);
		for_each_string_list_item(item, &list)
			strvec_push(&transport_ls_refs_options.ref_prefixes,
					item->string);
	}

where collect_upstream_from_remote() performs the bulk of what add_if_tracking_remote() does, which means add_if_tracking_remote() becomes

	static int add_if_tracking_remote(...)
	{
		struct branches_tracking_remote_cb_data *data = cb_data;

		collect_upstream_from_remote(data->tracked, data->remote, ref->name);
	}

This will mean we will have a very small preliminary patch to introduce collect_upstream_from_remote() function in remote.c and update the "help current branch by fetching what are merged into it" code in do_fetch() to use it, which will have the above ontlined if/else if/ cascade except for your new refmap code. On top, this step will insert a single "else if" block to add your new logic to do_fetch().

Doesn't it make the series (and more importantly, the resulting code) much easier to understand?

remote.c int branch_merge_matches(struct branch *branch, · line 1914

Quoted 7 lines
remote.c +7 -0
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(the_repository),
+				  add_if_tracking_remote, &data);
+}

This also hardcodes the_repository, but shouldn't this function take "struct repository *" pointer (and shove it in data structure to pass it down)?

By the way, when merged to 'seen', it seems to have some interactions with other topics and makes t5505 and t5586 fail. I didn't have time to dig down to the cause. Can you perhaps help finding the cause when I push the integration result out early this afternoon?

Thanks.

#51 lore raw

Re: [PATCH v7 2/4] fetch: infer branches to fetch from a refmap-only remote

Quoted 5 lines

By the way, when merged to 'seen', it seems to have some interactions with other topics and makes t5505 and t5586 fail. I didn't have time to dig down to the cause. Can you perhaps help finding the cause when I push the integration result out early this afternoon?

Yes, there is a conflict between 'ch/fetch-followremotehead-lazy-validation's' and my v7.

But that way it's looking now, it will go away when pushing out v8.

Harald

#52 lore raw

Re: [PATCH v7 2/4] fetch: infer branches to fetch from a refmap-only remote

In reply to Junio C Hamano
Quoted 5 lines

Perhaps something like this instead:

... but nothing to fetch is specified on the command line, branches from the remote that are used as '@{upstream}' of our local branches are fetched.

Thanks, will update.

Quoted 17 lines

The unified diff is a bit messy to compare the before-and-after behaviour, so let's see what the preimage said first.

builtin/fetch.c static int do_fetch(struct transport *transport, · line 2011

Quoted 13 lines
              struct branch *branch = branch_get(NULL);
builtin/fetch.c +0 -6
-
-             if (transport->remote->fetch.nr) {
                      refspec_ref_prefixes(&transport->remote->fetch,
                                           &transport_ls_refs_options.ref_prefixes);
-                     if (follow_remote_head != FOLLOW_REMOTE_NEVER)
-                             do_set_head = 1;
              }
-             if (branch && branch_has_merge_config(branch) &&
-                 !strcmp(branch->remote_name, transport->remote->name)) {
                      int i;
                      for (i = 0; i < branch->merge_nr; i++) {
                              strvec_push(&transport_ls_refs_options.ref_prefixes,
Quoted 12 lines

So, when !rs->nr (i.e., nothing given on the command line to be fetched) and there is no fetch refspec, we checked the current branch and if it has merge config to merge from branches at the remote, we automatically fetched them. This is the world order before this "infer with @{u} and refmap" work, and we should behave the same way when remote.*.refmap is not set.

Let's see what the postimage says.

Quoted 3 lines
              struct branch *branch = branch_get(NULL);
builtin/fetch.c +2 -0
+             int tracks_this_remote = branch && branch_has_merge_config(branch) &&
+                     !strcmp(branch->remote_name, transport->remote->name);

Yes, this can be improved.

Quoted 17 lines

This variable tells the code that it must infer branches, but named as if it were a list of branches that were inferred. "When remote.*.fetch does not exist and we have the refmap to use for inferring".

Quoted 13 lines
builtin/fetch.c +10 -0
+             if (inferred_branches) {
+                     struct string_list tracked = STRING_LIST_INIT_DUP;
+                     struct string_list_item *item;
+
+                     branches_tracking_remote(transport->remote, &tracked);
+                     for_each_string_list_item(item, &tracked)
+                             strvec_push(&transport_ls_refs_options.ref_prefixes,
+                                         item->string);
+                     string_list_clear(&tracked, 0);
+             } else if (transport->remote->fetch.nr) {
                      refspec_ref_prefixes(&transport->remote->fetch,
                                           &transport_ls_refs_options.ref_prefixes);
              }

I'll rename it to 'infer_from_refmap'.

Quoted 13 lines

It would have been much easier to follow if the existing code came first to make it clear that the new code is an add-on. After all, when transport->remote->fetch.nr is true, inferred_branches is never true.

builtin/fetch.c static int do_fetch(struct transport *transport, · line 2011

Quoted 7 lines
builtin/fetch.c +4 -0
+             if ((transport->remote->fetch.nr || inferred_branches) &&
+                 follow_remote_head != FOLLOW_REMOTE_NEVER)
+                     do_set_head = 1;
+             if (tracks_this_remote) {
                      int i;
                      for (i = 0; i < branch->merge_nr; i++) {
                              strvec_push(&transport_ls_refs_options.ref_prefixes,
Quoted 4 lines

How does tracks_this_remote and inferred_branches interact? Doesn't the old code that grabs necessary remote-tracking branches for the current branch add the same branch from the remote? Doesn't @{u} for the current branch added twice on the list of branches to fetch?

Good points.

Quoted 9 lines

builtin/fetch.c static int do_fetch(struct transport *transport, · line 2051

Quoted 7 lines
builtin/fetch.c +1 -0
@@ -2009,6 +2046,7 @@ static int do_fetch(struct transport *transport,
 
      ref_map = get_ref_map(transport->remote, remote_refs, rs,
                            tags, &autotags);
+
      if (!update_head_ok)
              check_not_current_branch(ref_map);

Useless patch noise.

Fixed.

Quoted 24 lines
Quoted 9 lines
remote.c +1 -0
index 99a086ea5a..c26312beea 100644
@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,
      return refname_match(branch->merge[i]->src, refname);
 }
 
+struct branches_tracking_remote_cb_data {

remote.c int branch_merge_matches(struct branch *branch, · line 1897

Quoted 10 lines
remote.c +10 -0
+     struct remote *remote;
+     struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+     struct branches_tracking_remote_cb_data *data = cb_data;
+     struct branch *branch;
+
+     branch = branch_get(ref->name);

I know branch_get() is defined here and allows implicit use of the_repository, but can't we pass "struct repository *" around in cb_data so that we can use repo_branch_get() here?

Quoted 14 lines

remote.c int branch_merge_matches(struct branch *branch, · line 1906

Quoted 9 lines
remote.c +9 -0
+     if (!branch_has_merge_config(branch) ||
+         strcmp(branch->remote_name, data->remote->name))
+             return 0;
+
+     for (int i = 0; i < branch->merge_nr; i++)
+             string_list_insert(data->tracked, branch->merge[i]->src);
+
+     return 0;
+}

This is more or less identical to the "if current branch integrates with branches from the remote, then fetch them" code we saw earlier in the builtin/fetch.c:do_fetch() above. I notice that its return value is meaningless, as it always returns 0.

I'll look into unifying.

The 0 return value is there so it can be used with 'refs_for_each_branch_ref' without stopping the iteration.

Quoted 7 lines
        static int add_if_tracking_remote(...)
        {
                struct branches_tracking_remote_cb_data *data = cb_data;

                collect_upstream_from_remote(data->tracked, data->remote, ref->name);
        }
Quoted 7 lines

This will mean we will have a very small preliminary patch to introduce collect_upstream_from_remote() function in remote.c and update the "help current branch by fetching what are merged into it" code in do_fetch() to use it, which will have the above ontlined if/else if/ cascade except for your new refmap code. On top, this step will insert a single "else if" block to add your new logic to do_fetch().

Yes, good idea.

Quoted 11 lines

remote.c int branch_merge_matches(struct branch *branch, · line 1914

Quoted 7 lines
remote.c +7 -0
+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)
+{
+     struct branches_tracking_remote_cb_data data = { remote, tracked };
+
+     refs_for_each_branch_ref(get_main_ref_store(the_repository),
+                               add_if_tracking_remote, &data);
+}

This also hardcodes the_repository, but shouldn't this function take "struct repository *" pointer (and shove it in data structure to pass it down)?

Good point.

Harald

#53 lore raw

PATCH v7 3/4 remote: add "git remote add --limited-fetch"

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)
Documentation/git-remote.adoc +7 -1
index eaae30aa88..4255f8b3e6 100644
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [synopsis]
 git remote [-v | --verbose]
-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>
 git remote rename [--[no-]progress] <old> <new>
 git remote remove <name>
 git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_
 is created.  You can give more than one `-t <branch>` to track
 multiple branches without grabbing all branches.
 +
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
+details.
++
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
 +
builtin/remote.c +22 -6
index de989ea3ba..f036dd5d6f 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
+	int limited_fetch = -1; /* unspecified */
 	struct string_list track = STRING_LIST_INIT_NODUP;
 	const char *master = NULL;
 	struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
 		OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
 			N_("set up remote as a mirror to push to or fetch from"),
 			PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+		OPT_BOOL(0, "limited-fetch", &limited_fetch,
+			N_("fetch only the branches we build on, instead of every branch")),
 		OPT_END()
 	};
 
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
 		die(_("specifying a master branch makes no sense with --mirror"));
 	if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
 		die(_("specifying branches to track makes sense only with fetch mirrors"));
+	if (limited_fetch == 1 && track.nr)
+		die(_("--limited-fetch does not make sense with -t/--track"));
+	if (limited_fetch == 1 && mirror)
+		die(_("--limited-fetch does not make sense with --mirror"));
 
 	name = argv[0];
 	url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
 	repo_config_set(the_repository, buf.buf, url);
 
 	if (!mirror || mirror & MIRROR_FETCH) {
+		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+			limited_fetch == 1;
+
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "remote.%s.fetch", name);
-		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
-			add_branch(buf.buf, track.items[i].string,
-				   name, mirror, &buf2);
+		if (use_limited_fetch) {
+			strbuf_addf(&buf, "remote.%s.refmap", name);
+			strbuf_reset(&buf2);
+			strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+			repo_config_set(the_repository, buf.buf, buf2.buf);
+		} else {
+			strbuf_addf(&buf, "remote.%s.fetch", name);
+			if (track.nr == 0)
+				string_list_append(&track, "*");
+			for (size_t i = 0; i < track.nr; i++)
+				add_branch(buf.buf, track.items[i].string,
+					   name, mirror, &buf2);
 		}
 	}
 
t/t5505-remote.sh +16 -0
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
 	test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+	test_when_finished "rm -rf full-add" &&
+	git clone --no-local one full-add &&
+	(
+		cd full-add &&
+		git remote add --limited-fetch upstream ../two &&
+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+			remote.upstream.refmap &&
+		test_must_fail git config get remote.upstream.fetch
+	)
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+	test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
 	(
 		cd test &&
Signature · 1 line
gitgitgadget
#54 lore raw

PATCH v7 4/4 remote: default to --limited-fetch in a shallow repository

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index 4255f8b3e6..80f8a4a183 100644
@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
-details.
+details. `--no-limited-fetch` explicitly disables this, overriding the
+shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +4 -1
index f036dd5d6f..0cf1126976 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
 	if (!mirror || mirror & MIRROR_FETCH) {
 		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-			limited_fetch == 1;
+			(limited_fetch == 1 ||
+			 (limited_fetch == -1 &&
+			  is_repository_shallow(the_repository)));
 
 		strbuf_reset(&buf);
 		if (use_limited_fetch) {
t/t5505-remote.sh +60 -0

66 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#55 lore raw

v8 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v8:

* Split into a preliminary collect_upstream_from_remote() helper commit plus a simplified refmap-inference commit.
* Fixed a regression that could drop the current branch's @{upstream} when remote.<name>.fetch was narrowed.
* Threaded struct repository * through instead of hardcoding the_repository.
* Minor naming and comment cleanups.

Changes in v7:

* Fixed fatal: --refmap option is only meaningful with command-line refspec(s) firing just from configuring remote.<name>.refmap alone, with nothing else to fetch.
* When both remote.<name>.fetch and remote.<name>.refmap are configured, a refspec-less fetch follows .fetch as usual, .refmap only remaps something already being fetched by name. Added tests.

Changes in v6:

* Remove leftover reference to deleted default-branch logic in commit message.

Changes in v5:

* Renumber test file from t5585 to t5586.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (5):
fetch: add remote.<name>.refmap
fetch: extract collect_upstream_from_remote() helper
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |   5 ++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  84 ++++++++++++++++++----
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  51 ++++++++++++-
 remote.h                         |  19 +++++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 |  63 ++++++++++++++++
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 448 insertions(+), 23 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

base-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v8
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v8
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v7:

1: b5db64d56f ! 1: 2a48c21bc8 fetch: add remote.<name>.refmap

     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       	struct ref *rm;
       	struct ref *ref_map = NULL;
       	struct ref **tail = &ref_map;
     ++	/*
     ++	 * The --refmap command line option, if given, takes precedence
     ++	 * over remote.<name>.refmap.
     ++	 */
      +	struct refspec *effective_refmap =
      +		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 -:  ---------- > 2:  08fea07a8b fetch: extract collect_upstream_from_remote() helper
 2:  fd6864daaf ! 3:  a39316d233 fetch: infer branches to fetch from a refmap-only remote
     @@ Documentation/fetch-options.adoc: endif::git-pull[]
      -`remote.<name>.refmap` provides the default value for this option, the
      -same way `remote.<name>.fetch` provides the default refspecs to fetch.
      +When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
     -+there is nothing to fetch, neither on the command line nor from
     -+`remote.<name>.fetch`, Git infers what to fetch from the local branches
     -+whose `@{upstream}` is on that remote.
     ++nothing to fetch is specified on the command line, nor is there a
     ++`remote.<name>.fetch`, branches from the remote that are used as the
     ++`@{upstream}` of our local branches are fetched.
       
       `-t`::
       `--tags`::
      
       ## builtin/fetch.c ##
      @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
     - 	struct ref **tail = &ref_map;
     + 	 */
       	struct refspec *effective_refmap =
       		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
      +	struct refspec inferred_rs;
     -+	int inferred_branches = 0;
     ++	int infer_from_refmap = 0;
       
       	/* opportunistically-updated references: */
       	struct ref *orefs = NULL, **oref_tail = &orefs;
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
      +		struct string_list tracked = STRING_LIST_INIT_DUP;
      +		struct string_list_item *item;
      +
     -+		branches_tracking_remote(remote, &tracked);
     ++		branches_tracking_remote(the_repository, remote, &tracked);
      +		for_each_string_list_item(item, &tracked)
      +			refspec_append(&inferred_rs, item->string);
      +		string_list_clear(&tracked, 0);
      +
      +		rs = &inferred_rs;
     -+		inferred_branches = 1;
     ++		infer_from_refmap = 1;
      +	}
      +
       	if (rs->nr) {
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       		for (i = 0; i < rs->nr; i++) {
      -			get_fetch_map(remote_refs, &rs->items[i], &tail, 0);
      +			get_fetch_map(remote_refs, &rs->items[i], &tail,
     -+				      inferred_branches);
     ++				      infer_from_refmap);
       			if (rs->items[i].dst && rs->items[i].dst[0])
       				*autotags = 1;
       		}
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       
       		for (i = 0; i < fetch_refspec->nr; i++)
       			get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
     -+	} else if (inferred_branches) {
     ++	} else if (infer_from_refmap) {
      +		/* Already fully handled above. */
       	} else if (refmap.nr) {
       		die("--refmap option is only meaningful with command-line refspec(s)");
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       }
       
      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
     + 				    item->string);
     + 		string_list_clear(&tracked, 0);
       	} else {
     - 		struct branch *branch = branch_get(NULL);
     --
     --		if (transport->remote->fetch.nr) {
     -+		int tracks_this_remote = branch && branch_has_merge_config(branch) &&
     -+			!strcmp(branch->remote_name, transport->remote->name);
     ++		/*
     ++		 * The --refmap command line option, if given, takes
     ++		 * precedence over remote.<name>.refmap.
     ++		 */
      +		struct refspec *effective_refmap = refmap.nr ? &refmap :
      +			&transport->remote->refmap;
     -+		int inferred_branches = !transport->remote->fetch.nr &&
     -+			effective_refmap->nr;
     -+
     -+		if (inferred_branches) {
     -+			struct string_list tracked = STRING_LIST_INIT_DUP;
     -+			struct string_list_item *item;
     -+
     -+			branches_tracking_remote(transport->remote, &tracked);
     -+			for_each_string_list_item(item, &tracked)
     -+				strvec_push(&transport_ls_refs_options.ref_prefixes,
     -+					    item->string);
     -+			string_list_clear(&tracked, 0);
     -+		} else if (transport->remote->fetch.nr) {
     - 			refspec_ref_prefixes(&transport->remote->fetch,
     - 					     &transport_ls_refs_options.ref_prefixes);
     --			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     --				do_set_head = 1;
     - 		}
     --		if (branch && branch_has_merge_config(branch) &&
     --		    !strcmp(branch->remote_name, transport->remote->name)) {
     -+		if ((transport->remote->fetch.nr || inferred_branches) &&
     -+		    follow_remote_head != FOLLOW_REMOTE_NEVER)
     -+			do_set_head = 1;
     -+		if (tracks_this_remote) {
     - 			int i;
     - 			for (i = 0; i < branch->merge_nr; i++) {
     - 				strvec_push(&transport_ls_refs_options.ref_prefixes,
     -@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 
     - 	ref_map = get_ref_map(transport->remote, remote_refs, rs,
     - 			      tags, &autotags);
     -+
     - 	if (!update_head_ok)
     - 		check_not_current_branch(ref_map);
     + 		struct string_list tracked = STRING_LIST_INIT_DUP;
     + 		struct string_list_item *item;
       
     +-		collect_upstream_from_remote(the_repository, &tracked,
     +-					      transport->remote, NULL);
     ++		if (effective_refmap->nr) {
     ++			branches_tracking_remote(the_repository,
     ++						  transport->remote, &tracked);
     ++			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     ++				do_set_head = 1;
     ++		} else {
     ++			collect_upstream_from_remote(the_repository, &tracked,
     ++						      transport->remote, NULL);
     ++		}
     + 		for_each_string_list_item(item, &tracked)
     + 			strvec_push(&transport_ls_refs_options.ref_prefixes,
     + 				    item->string);
      
       ## remote.c ##
     -@@ remote.c: int branch_merge_matches(struct branch *branch,
     - 	return refname_match(branch->merge[i]->src, refname);
     +@@ remote.c: void collect_upstream_from_remote(struct repository *repo,
     + 		string_list_insert(tracked, branch->merge[i]->src);
       }
       
      +struct branches_tracking_remote_cb_data {
     ++	struct repository *repo;
      +	struct remote *remote;
      +	struct string_list *tracked;
      +};
     @@ remote.c: int branch_merge_matches(struct branch *branch,
      +static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
      +{
      +	struct branches_tracking_remote_cb_data *data = cb_data;
     -+	struct branch *branch;
     -+
     -+	branch = branch_get(ref->name);
     -+	if (!branch_has_merge_config(branch) ||
     -+	    strcmp(branch->remote_name, data->remote->name))
     -+		return 0;
     -+
     -+	for (int i = 0; i < branch->merge_nr; i++)
     -+		string_list_insert(data->tracked, branch->merge[i]->src);
      +
     ++	collect_upstream_from_remote(data->repo, data->tracked, data->remote,
     ++				      ref->name);
      +	return 0;
      +}
      +
     -+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)
     ++void branches_tracking_remote(struct repository *repo, struct remote *remote,
     ++			       struct string_list *tracked)
      +{
     -+	struct branches_tracking_remote_cb_data data = { remote, tracked };
     ++	struct branches_tracking_remote_cb_data data = { repo, remote, tracked };
      +
     -+	refs_for_each_branch_ref(get_main_ref_store(the_repository),
     ++	refs_for_each_branch_ref(get_main_ref_store(repo),
      +				  add_if_tracking_remote, &data);
      +}
      +
     @@ remote.c: int branch_merge_matches(struct branch *branch,
       {
      
       ## remote.h ##
     -@@ remote.h: int branch_has_merge_config(struct branch *branch);
     - 
     - int branch_merge_matches(struct branch *, int n, const char *);
     +@@ remote.h: void collect_upstream_from_remote(struct repository *repo,
     + 				   struct remote *remote,
     + 				   const char *refname);
       
      +/* fills tracked with the refname of every local branch's upstream on remote */
     -+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);
     ++void branches_tracking_remote(struct repository *repo, struct remote *remote,
     ++			       struct string_list *tracked);
      +
       /* list of the remote in a group as configured */
       struct remote_group_data {
 3:  a2208875b6 = 4:  2b974bbe0a remote: add "git remote add --limited-fetch"
 4:  c8fd073de3 = 5:  db27c29086 remote: default to --limited-fetch in a shallow repository
Signature · 1 line
gitgitgadget
#56 lore raw

PATCH v8 1/5 fetch: add remote.<name>.refmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Like --refmap itself, it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

A refspec-less fetch with a refmap configured but nothing else to say what to fetch falls back to the same defaults as a remote with no refspec or refmap at all. Only an explicit --refmap given on the command line with no command-line refspec to go with it is still rejected, since that combination is a plain mistake to type, unlike a remote.<name>.refmap configured on its own. A remote.<name>.fetch that is also configured is used as before.

---
 Documentation/config/remote.adoc |  5 +++
 Documentation/fetch-options.adoc |  3 ++
 builtin/fetch.c                  | 10 ++++-
 remote.c                         | 12 +++++-
 remote.h                         |  6 +++
 t/t5510-fetch.sh                 | 63 ++++++++++++++++++++++++++++++++
 6 files changed, 96 insertions(+), 3 deletions(-)
Documentation/config/remote.adoc +5 -0
index 3a20d0f752..103eada406 100644
@@ -33,6 +33,11 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Used to map remote refs being fetched to remote-tracking refs to
+	store. See the `--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 47dea1de8e..c2101a7b39 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +8 -2
index b2decc6cfd..c1c65c7528 100644
@@ -509,6 +509,12 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	/*
+	 * The --refmap command line option, if given, takes precedence
+	 * over remote.<name>.refmap.
+	 */
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -552,8 +558,8 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
remote.c +11 -1
index 71170f36a9..99a086ea5a 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	if (remote->fetch.nr)
+		return refspec_find_match(&remote->fetch, refspec);
+	return refspec_find_match(&remote->refmap, refspec);
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +63 -0

69 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#57 lore raw

PATCH v8 2/5 fetch: extract collect_upstream_from_remote() helper

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

When a fetch has no refspec to work from, we fall back to guessing what to fetch: the remote's configured fetch refspec if there is one, and separately, whether the current branch's upstream lives on this remote. The latter is independent of the former on purpose: a narrow refspec (for example from "git remote add -t") can leave the current branch's own upstream uncovered, and we still need to be able to fetch it.

That "does this branch build on this remote" check was inlined for just the current branch. Move it into its own function, collect_upstream_from_remote(), so the same logic can be reused for branches other than just the current one.

---
 builtin/fetch.c | 43 ++++++++++++++++++++++++++++---------------
 remote.c        | 15 +++++++++++++++
 remote.h        |  9 +++++++++
 3 files changed, 52 insertions(+), 15 deletions(-)
builtin/fetch.c +28 -15
index c1c65c7528..4753656288 100644
@@ -1962,23 +1962,36 @@ static int do_fetch(struct transport *transport,
 
 	if (rs->nr) {
 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
+	} else if (transport->remote->fetch.nr) {
+		struct string_list tracked = STRING_LIST_INIT_DUP;
+		struct string_list_item *item;
+
+		refspec_ref_prefixes(&transport->remote->fetch,
+				     &transport_ls_refs_options.ref_prefixes);
+		if (follow_remote_head != FOLLOW_REMOTE_NEVER)
+			do_set_head = 1;
+
+		/*
+		 * The configured refspec may not cover the current
+		 * branch's upstream (e.g. a narrowed -t refspec), so
+		 * make sure we can still fetch it regardless.
+		 */
+		collect_upstream_from_remote(the_repository, &tracked,
+					      transport->remote, NULL);
+		for_each_string_list_item(item, &tracked)
+			strvec_push(&transport_ls_refs_options.ref_prefixes,
+				    item->string);
+		string_list_clear(&tracked, 0);
 	} else {
-		struct branch *branch = branch_get(NULL);
+		struct string_list tracked = STRING_LIST_INIT_DUP;
+		struct string_list_item *item;
 
-		if (transport->remote->fetch.nr) {
-			refspec_ref_prefixes(&transport->remote->fetch,
-					     &transport_ls_refs_options.ref_prefixes);
-			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
-				do_set_head = 1;
-		}
-		if (branch && branch_has_merge_config(branch) &&
-		    !strcmp(branch->remote_name, transport->remote->name)) {
-			int i;
-			for (i = 0; i < branch->merge_nr; i++) {
-				strvec_push(&transport_ls_refs_options.ref_prefixes,
-					    branch->merge[i]->src);
-			}
-		}
+		collect_upstream_from_remote(the_repository, &tracked,
+					      transport->remote, NULL);
+		for_each_string_list_item(item, &tracked)
+			strvec_push(&transport_ls_refs_options.ref_prefixes,
+				    item->string);
+		string_list_clear(&tracked, 0);
 
 		/*
 		 * If there are no refs specified to fetch, then we just
remote.c +15 -0
index 99a086ea5a..5e980625b8 100644
@@ -1884,6 +1884,21 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+void collect_upstream_from_remote(struct repository *repo,
+				   struct string_list *tracked,
+				   struct remote *remote,
+				   const char *refname)
+{
+	struct branch *branch = repo_branch_get(repo, refname);
+
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, remote->name))
+		return;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(tracked, branch->merge[i]->src);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +9 -0
index ac485a584d..7c86c529b6 100644
@@ -359,6 +359,15 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/*
+ * If refname's branch builds on remote, add its upstream on remote to
+ * tracked. A NULL refname means the current branch.
+ */
+void collect_upstream_from_remote(struct repository *repo,
+				   struct string_list *tracked,
+				   struct remote *remote,
+				   const char *refname);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
Signature · 1 line
gitgitgadget
#58 lore raw

Re: [PATCH v8 2/5] fetch: extract collect_upstream_from_remote() helper

Quoted 14 lines · Harald Nordgren via GitGitGadget
builtin/fetch.c +0 -8
@@ -1962,23 +1962,36 @@ static int do_fetch(struct transport *transport,
 
 	if (rs->nr) {
 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
 	} else {
-		struct branch *branch = branch_get(NULL);
 
-		if (transport->remote->fetch.nr) {
-			refspec_ref_prefixes(&transport->remote->fetch,
-					     &transport_ls_refs_options.ref_prefixes);
-			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
-				do_set_head = 1;
-		}
-		if (branch && branch_has_merge_config(branch) &&

builtin/fetch.c static int do_fetch(struct transport *transport, · line 1969

Quoted 10 lines
builtin/fetch.c +0 -7
-		    !strcmp(branch->remote_name, transport->remote->name)) {
-			int i;
-			for (i = 0; i < branch->merge_nr; i++) {
-				strvec_push(&transport_ls_refs_options.ref_prefixes,
-					    branch->merge[i]->src);
-			}
-		}
 
 		/*
 		 * If there are no refs specified to fetch, then we just

We used to say "if there is remote.*.fetch, add them. whether there is remote.*.fetch or not, if the current branch builds on their branch(es), add them too".

Quoted 24 lines
builtin/fetch.c +20 -0
@@ -1962,23 +1962,36 @@ static int do_fetch(struct transport *transport,
 
 	if (rs->nr) {
 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
+	} else if (transport->remote->fetch.nr) {
+		struct string_list tracked = STRING_LIST_INIT_DUP;
+		struct string_list_item *item;
+
+		refspec_ref_prefixes(&transport->remote->fetch,
+				     &transport_ls_refs_options.ref_prefixes);
+		if (follow_remote_head != FOLLOW_REMOTE_NEVER)
+			do_set_head = 1;
+
+		/*
+		 * The configured refspec may not cover the current
+		 * branch's upstream (e.g. a narrowed -t refspec), so
+		 * make sure we can still fetch it regardless.
+		 */
+		collect_upstream_from_remote(the_repository, &tracked,
+					      transport->remote, NULL);
+		for_each_string_list_item(item, &tracked)
+			strvec_push(&transport_ls_refs_options.ref_prefixes,
+				    item->string);
+		string_list_clear(&tracked, 0);

builtin/fetch.c static int do_fetch(struct transport *transport, · line 1994

Quoted 10 lines
builtin/fetch.c +8 -0
 	} else {
+		struct string_list tracked = STRING_LIST_INIT_DUP;
+		struct string_list_item *item;
 
+		collect_upstream_from_remote(the_repository, &tracked,
+					      transport->remote, NULL);
+		for_each_string_list_item(item, &tracked)
+			strvec_push(&transport_ls_refs_options.ref_prefixes,
+				    item->string);
+		string_list_clear(&tracked, 0);

This reorganizes it to say "if there is remote.*.fetch, do the same as before. If there is not remote.*.fetch, add their branch(es) the current branch wants".

This "duplicate what happens to the current branch" belongs to the next step where you want to add more logic to the latter part (i.e., "no remote.*.fetch" case), and not to this step.

IOW I would have expected to see

	} else {
		if (transport->remote->fetch.nr) {
			struct string_list tracked = STRING_LIST_INIT_DUP;
			struct string_list_item *item;

			refspec_ref_prefixes(&transport->remote->fetch,
					     &transport_ls_refs_options.ref_prefixes);
			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
				do_set_head = 1;
		}

		/*
		 * The configured refspec may not cover the current
		 * branch's upstream (e.g. a narrowed -t refspec), so
		 * make sure we can still fetch it regardless.
		 */
		collect_upstream_from_remote(the_repository, &tracked,
					      transport->remote, NULL);
		for_each_string_list_item(item, &tracked)
			strvec_push(&transport_ls_refs_options.ref_prefixes,
				    item->string);
		string_list_clear(&tracked, 0);
	}

that touches "help the current branch when rs->nr == 0" part and nothing else.

Of course, because the next step [3/5] wants to split the above else{} block to do different things between the case where remote.*.fetch is and is not empty, at that point the extra code may be introduced there, ending up with the shape of if/else if cascade as we see in your patch.

Quoted 11 lines
remote.c +3 -0
index 99a086ea5a..5e980625b8 100644
@@ -1884,6 +1884,21 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+void collect_upstream_from_remote(struct repository *repo,
+				   struct string_list *tracked,
+				   struct remote *remote,

remote.c int branch_merge_matches(struct branch *branch, · line 1899

Quoted 10 lines
remote.c +10 -0
+				   const char *refname)
+{
+	struct branch *branch = repo_branch_get(repo, refname);
+
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, remote->name))
+		return;
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(tracked, branch->merge[i]->src);
+}

The original tries to avoid branch == NULL causing a segfault, but the above does not. Intended or overlooked?

Other than that, this looks like a straight-forward refactoring.

#59 lore raw

Re: [PATCH v8 2/5] fetch: extract collect_upstream_from_remote() helper

Quoted 26 lines

IOW I would have expected to see

        } else {
                if (transport->remote->fetch.nr) {
                        struct string_list tracked = STRING_LIST_INIT_DUP;
                        struct string_list_item *item;

                        refspec_ref_prefixes(&transport->remote->fetch,
                                             &transport_ls_refs_options.ref_prefixes);
                        if (follow_remote_head != FOLLOW_REMOTE_NEVER)
                                do_set_head = 1;
                }

                /*
                 * The configured refspec may not cover the current
                 * branch's upstream (e.g. a narrowed -t refspec), so
                 * make sure we can still fetch it regardless.
                 */
                collect_upstream_from_remote(the_repository, &tracked,
                                              transport->remote, NULL);
                for_each_string_list_item(item, &tracked)
                        strvec_push(&transport_ls_refs_options.ref_prefixes,
                                    item->string);
                string_list_clear(&tracked, 0);
        }
Quoted 8 lines

that touches "help the current branch when rs->nr == 0" part and nothing else.

Of course, because the next step [3/5] wants to split the above else{} block to do different things between the case where remote.*.fetch is and is not empty, at that point the extra code may be introduced there, ending up with the shape of if/else if cascade as we see in your patch.

This is a good point about how commits tell their story, and I'll look into it.

Thanks for the snippet!

Quoted 8 lines
Quoted 8 lines
remote.c +0 -0
index 99a086ea5a..5e980625b8 100644
@@ -1884,6 +1884,21 @@ int branch_merge_matches(struct branch *branch,
      return refname_match(branch->merge[i]->src, refname);
 }
 
Quoted 16 lines

remote.c int branch_merge_matches(struct branch *branch, · line 1900

Quoted 13 lines
remote.c +13 -0
+void collect_upstream_from_remote(struct repository *repo,
+                                struct string_list *tracked,
+                                struct remote *remote,
+                                const char *refname)
+{
+     struct branch *branch = repo_branch_get(repo, refname);
+
+     if (!branch_has_merge_config(branch) ||
+         strcmp(branch->remote_name, remote->name))
+             return;
+     for (int i = 0; i < branch->merge_nr; i++)
+             string_list_insert(tracked, branch->merge[i]->src);
+}

The original tries to avoid branch == NULL causing a segfault, but the above does not. Intended or overlooked?

Overlooked, but I don't think the segfault would actually happen (in a way that could be reproduced by a test)?

Harald

#60 lore raw

PATCH v8 3/5 fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |   6 +-
 builtin/fetch.c                  |  41 ++++++++++-
 remote.c                         |  24 +++++++
 remote.h                         |   4 ++
 t/meson.build                    |   1 +
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 194 insertions(+), 6 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..7c52e443a5 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
 	Used to map remote refs being fetched to remote-tracking refs to
-	store. See the `--refmap` entry in linkgit:git-fetch[1].
+	store. If `remote.<name>.fetch` is not set either, a refspec-less
+	fetch infers what to fetch from local branches built on this
+	remote, instead of fetching every branch it has. See the
+	`--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +4 -2
index c2101a7b39..90b9e13ac2 100644
@@ -245,8 +245,10 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+nothing to fetch is specified on the command line, nor is there a
+`remote.<name>.fetch`, branches from the remote that are used as the
+`@{upstream}` of our local branches are fetched.
 
 `-t`::
 `--tags`::
builtin/fetch.c +38 -3

82 lines not on this page · raw message

remote.c +24 -0
index 5e980625b8..703c71ba45 100644
@@ -1899,6 +1899,30 @@ void collect_upstream_from_remote(struct repository *repo,
 		string_list_insert(tracked, branch->merge[i]->src);
 }
 
+struct branches_tracking_remote_cb_data {
+	struct repository *repo;
+	struct remote *remote;
+	struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+
+	collect_upstream_from_remote(data->repo, data->tracked, data->remote,
+				      ref->name);
+	return 0;
+}
+
+void branches_tracking_remote(struct repository *repo, struct remote *remote,
+			       struct string_list *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { repo, remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(repo),
+				  add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +4 -0
index 7c86c529b6..62265346d3 100644
@@ -368,6 +368,10 @@ void collect_upstream_from_remote(struct repository *repo,
 				   struct remote *remote,
 				   const char *refname);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct repository *repo, struct remote *remote,
+			       struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index f65eb04684..e287a6b947 100644
@@ -730,6 +730,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5586-fetch-refmap.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5586-fetch-refmap.sh new file +119 -0

119 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#61 lore raw

PATCH v8 4/5 remote: add "git remote add --limited-fetch"

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)
Documentation/git-remote.adoc +7 -1
index eaae30aa88..4255f8b3e6 100644
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [synopsis]
 git remote [-v | --verbose]
-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>
 git remote rename [--[no-]progress] <old> <new>
 git remote remove <name>
 git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_
 is created.  You can give more than one `-t <branch>` to track
 multiple branches without grabbing all branches.
 +
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
+details.
++
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
 +
builtin/remote.c +22 -6
index de989ea3ba..f036dd5d6f 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
+	int limited_fetch = -1; /* unspecified */
 	struct string_list track = STRING_LIST_INIT_NODUP;
 	const char *master = NULL;
 	struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
 		OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
 			N_("set up remote as a mirror to push to or fetch from"),
 			PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+		OPT_BOOL(0, "limited-fetch", &limited_fetch,
+			N_("fetch only the branches we build on, instead of every branch")),
 		OPT_END()
 	};
 
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
 		die(_("specifying a master branch makes no sense with --mirror"));
 	if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
 		die(_("specifying branches to track makes sense only with fetch mirrors"));
+	if (limited_fetch == 1 && track.nr)
+		die(_("--limited-fetch does not make sense with -t/--track"));
+	if (limited_fetch == 1 && mirror)
+		die(_("--limited-fetch does not make sense with --mirror"));
 
 	name = argv[0];
 	url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
 	repo_config_set(the_repository, buf.buf, url);
 
 	if (!mirror || mirror & MIRROR_FETCH) {
+		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+			limited_fetch == 1;
+
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "remote.%s.fetch", name);
-		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
-			add_branch(buf.buf, track.items[i].string,
-				   name, mirror, &buf2);
+		if (use_limited_fetch) {
+			strbuf_addf(&buf, "remote.%s.refmap", name);
+			strbuf_reset(&buf2);
+			strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+			repo_config_set(the_repository, buf.buf, buf2.buf);
+		} else {
+			strbuf_addf(&buf, "remote.%s.fetch", name);
+			if (track.nr == 0)
+				string_list_append(&track, "*");
+			for (size_t i = 0; i < track.nr; i++)
+				add_branch(buf.buf, track.items[i].string,
+					   name, mirror, &buf2);
 		}
 	}
 
t/t5505-remote.sh +16 -0
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
 	test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+	test_when_finished "rm -rf full-add" &&
+	git clone --no-local one full-add &&
+	(
+		cd full-add &&
+		git remote add --limited-fetch upstream ../two &&
+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+			remote.upstream.refmap &&
+		test_must_fail git config get remote.upstream.fetch
+	)
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+	test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
 	(
 		cd test &&
Signature · 1 line
gitgitgadget
#62 lore raw

Re: [PATCH v8 4/5] remote: add "git remote add --limited-fetch"

Quoted 15 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

Quoted 6 lines
---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)

This step looks quite straight-forward and nicely done. I didn't look at the tests, though.

#63 lore raw

PATCH v8 5/5 remote: default to --limited-fetch in a shallow repository

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index 4255f8b3e6..80f8a4a183 100644
@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
-details.
+details. `--no-limited-fetch` explicitly disables this, overriding the
+shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +4 -1
index f036dd5d6f..0cf1126976 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
 	if (!mirror || mirror & MIRROR_FETCH) {
 		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-			limited_fetch == 1;
+			(limited_fetch == 1 ||
+			 (limited_fetch == -1 &&
+			  is_repository_shallow(the_repository)));
 
 		strbuf_reset(&buf);
 		if (use_limited_fetch) {
t/t5505-remote.sh +60 -0

66 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#64 lore raw

v9 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v9:

* Moved the do_fetch() if/else-if/else split out of the preliminary helper-extraction commit and into the refmap-inference commit that needs it. Preliminary commit is now a simpler refactor.

Changes in v8:

* Split into a preliminary collect_upstream_from_remote() helper commit plus a simplified refmap-inference commit.
* Fixed a regression that could drop the current branch's @{upstream} when remote.<name>.fetch was narrowed.
* Threaded struct repository * through instead of hardcoding the_repository.
* Minor naming and comment cleanups.

Changes in v7:

* Fixed fatal: --refmap option is only meaningful with command-line refspec(s) firing just from configuring remote.<name>.refmap alone, with nothing else to fetch.
* When both remote.<name>.fetch and remote.<name>.refmap are configured, a refspec-less fetch follows .fetch as usual, .refmap only remaps something already being fetched by name. Added tests.

Changes in v6:

* Remove leftover reference to deleted default-branch logic in commit message.

Changes in v5:

* Renumber test file from t5585 to t5586.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (5):
fetch: add remote.<name>.refmap
fetch: extract collect_upstream_from_remote() helper
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |   5 ++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  84 ++++++++++++++++++----
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  51 ++++++++++++-
 remote.h                         |  19 +++++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 |  63 ++++++++++++++++
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 448 insertions(+), 23 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

base-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v9
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v9
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v8:

 1:  2a48c21bc8 = 1:  2a48c21bc8 fetch: add remote.<name>.refmap
 2:  08fea07a8b ! 2:  50382bce7e fetch: extract collect_upstream_from_remote() helper
     @@ Commit message
      
       ## builtin/fetch.c ##
      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 
       	if (rs->nr) {
       		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
     -+	} else if (transport->remote->fetch.nr) {
     -+		struct string_list tracked = STRING_LIST_INIT_DUP;
     -+		struct string_list_item *item;
     -+
     -+		refspec_ref_prefixes(&transport->remote->fetch,
     -+				     &transport_ls_refs_options.ref_prefixes);
     -+		if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     -+			do_set_head = 1;
     -+
     -+		/*
     -+		 * The configured refspec may not cover the current
     -+		 * branch's upstream (e.g. a narrowed -t refspec), so
     -+		 * make sure we can still fetch it regardless.
     -+		 */
     -+		collect_upstream_from_remote(the_repository, &tracked,
     -+					      transport->remote, NULL);
     -+		for_each_string_list_item(item, &tracked)
     -+			strvec_push(&transport_ls_refs_options.ref_prefixes,
     -+				    item->string);
     -+		string_list_clear(&tracked, 0);
       	} else {
      -		struct branch *branch = branch_get(NULL);
      +		struct string_list tracked = STRING_LIST_INIT_DUP;
      +		struct string_list_item *item;
       
     --		if (transport->remote->fetch.nr) {
     --			refspec_ref_prefixes(&transport->remote->fetch,
     --					     &transport_ls_refs_options.ref_prefixes);
     --			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     --				do_set_head = 1;
     --		}
     + 		if (transport->remote->fetch.nr) {
     + 			refspec_ref_prefixes(&transport->remote->fetch,
     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     + 				do_set_head = 1;
     + 		}
      -		if (branch && branch_has_merge_config(branch) &&
      -		    !strcmp(branch->remote_name, transport->remote->name)) {
      -			int i;
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      -					    branch->merge[i]->src);
      -			}
      -		}
     ++
     ++		/*
     ++		 * The configured refspec may not cover the current
     ++		 * branch's upstream (e.g. a narrowed -t refspec), so
     ++		 * make sure we can still fetch it regardless.
     ++		 */
      +		collect_upstream_from_remote(the_repository, &tracked,
      +					      transport->remote, NULL);
      +		for_each_string_list_item(item, &tracked)
 3:  a39316d233 ! 3:  9ad8ad3eb4 fetch: infer branches to fetch from a refmap-only remote
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       }
       
      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 
     + 	if (rs->nr) {
     + 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
     +-	} else {
     ++	} else if (transport->remote->fetch.nr) {
     + 		struct string_list tracked = STRING_LIST_INIT_DUP;
     + 		struct string_list_item *item;
     + 
     +-		if (transport->remote->fetch.nr) {
     +-			refspec_ref_prefixes(&transport->remote->fetch,
     +-					     &transport_ls_refs_options.ref_prefixes);
     +-			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     +-				do_set_head = 1;
     +-		}
     ++		refspec_ref_prefixes(&transport->remote->fetch,
     ++				     &transport_ls_refs_options.ref_prefixes);
     ++		if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     ++			do_set_head = 1;
     + 
     + 		/*
     + 		 * The configured refspec may not cover the current
     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 			strvec_push(&transport_ls_refs_options.ref_prefixes,
       				    item->string);
       		string_list_clear(&tracked, 0);
     - 	} else {
     ++	} else {
      +		/*
      +		 * The --refmap command line option, if given, takes
      +		 * precedence over remote.<name>.refmap.
      +		 */
      +		struct refspec *effective_refmap = refmap.nr ? &refmap :
      +			&transport->remote->refmap;
     - 		struct string_list tracked = STRING_LIST_INIT_DUP;
     - 		struct string_list_item *item;
     - 
     --		collect_upstream_from_remote(the_repository, &tracked,
     --					      transport->remote, NULL);
     ++		struct string_list tracked = STRING_LIST_INIT_DUP;
     ++		struct string_list_item *item;
     ++
      +		if (effective_refmap->nr) {
      +			branches_tracking_remote(the_repository,
      +						  transport->remote, &tracked);
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      +			collect_upstream_from_remote(the_repository, &tracked,
      +						      transport->remote, NULL);
      +		}
     - 		for_each_string_list_item(item, &tracked)
     - 			strvec_push(&transport_ls_refs_options.ref_prefixes,
     - 				    item->string);
     ++		for_each_string_list_item(item, &tracked)
     ++			strvec_push(&transport_ls_refs_options.ref_prefixes,
     ++				    item->string);
     ++		string_list_clear(&tracked, 0);
     + 
     + 		/*
     + 		 * If there are no refs specified to fetch, then we just
      
       ## remote.c ##
      @@ remote.c: void collect_upstream_from_remote(struct repository *repo,
 4:  2b974bbe0a = 4:  75169f668e remote: add "git remote add --limited-fetch"
 5:  db27c29086 = 5:  9ee090d3d0 remote: default to --limited-fetch in a shallow repository
Signature · 1 line
gitgitgadget
#65 lore raw

PATCH v9 1/5 fetch: add remote.<name>.refmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Like --refmap itself, it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

A refspec-less fetch with a refmap configured but nothing else to say what to fetch falls back to the same defaults as a remote with no refspec or refmap at all. Only an explicit --refmap given on the command line with no command-line refspec to go with it is still rejected, since that combination is a plain mistake to type, unlike a remote.<name>.refmap configured on its own. A remote.<name>.fetch that is also configured is used as before.

---
 Documentation/config/remote.adoc |  5 +++
 Documentation/fetch-options.adoc |  3 ++
 builtin/fetch.c                  | 10 ++++-
 remote.c                         | 12 +++++-
 remote.h                         |  6 +++
 t/t5510-fetch.sh                 | 63 ++++++++++++++++++++++++++++++++
 6 files changed, 96 insertions(+), 3 deletions(-)
Documentation/config/remote.adoc +5 -0
index 3a20d0f752..103eada406 100644
@@ -33,6 +33,11 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Used to map remote refs being fetched to remote-tracking refs to
+	store. See the `--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 47dea1de8e..c2101a7b39 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +8 -2
index b2decc6cfd..c1c65c7528 100644
@@ -509,6 +509,12 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	/*
+	 * The --refmap command line option, if given, takes precedence
+	 * over remote.<name>.refmap.
+	 */
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -552,8 +558,8 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
remote.c +11 -1
index 71170f36a9..99a086ea5a 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	if (remote->fetch.nr)
+		return refspec_find_match(&remote->fetch, refspec);
+	return refspec_find_match(&remote->refmap, refspec);
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +63 -0

69 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#66 lore raw

PATCH v9 2/5 fetch: extract collect_upstream_from_remote() helper

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

When a fetch has no refspec to work from, we fall back to guessing what to fetch: the remote's configured fetch refspec if there is one, and separately, whether the current branch's upstream lives on this remote. The latter is independent of the former on purpose: a narrow refspec (for example from "git remote add -t") can leave the current branch's own upstream uncovered, and we still need to be able to fetch it.

That "does this branch build on this remote" check was inlined for just the current branch. Move it into its own function, collect_upstream_from_remote(), so the same logic can be reused for branches other than just the current one.

---
 builtin/fetch.c | 23 ++++++++++++++---------
 remote.c        | 15 +++++++++++++++
 remote.h        |  9 +++++++++
 3 files changed, 38 insertions(+), 9 deletions(-)
builtin/fetch.c +14 -9
index c1c65c7528..2eb58195a2 100644
@@ -1963,7 +1963,8 @@ static int do_fetch(struct transport *transport,
 	if (rs->nr) {
 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
 	} else {
-		struct branch *branch = branch_get(NULL);
+		struct string_list tracked = STRING_LIST_INIT_DUP;
+		struct string_list_item *item;
 
 		if (transport->remote->fetch.nr) {
 			refspec_ref_prefixes(&transport->remote->fetch,
@@ -1971,14 +1972,18 @@ static int do_fetch(struct transport *transport,
 			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
 				do_set_head = 1;
 		}
-		if (branch && branch_has_merge_config(branch) &&
-		    !strcmp(branch->remote_name, transport->remote->name)) {
-			int i;
-			for (i = 0; i < branch->merge_nr; i++) {
-				strvec_push(&transport_ls_refs_options.ref_prefixes,
-					    branch->merge[i]->src);
-			}
-		}
+
+		/*
+		 * The configured refspec may not cover the current
+		 * branch's upstream (e.g. a narrowed -t refspec), so
+		 * make sure we can still fetch it regardless.
+		 */
+		collect_upstream_from_remote(the_repository, &tracked,
+					      transport->remote, NULL);
+		for_each_string_list_item(item, &tracked)
+			strvec_push(&transport_ls_refs_options.ref_prefixes,
+				    item->string);
+		string_list_clear(&tracked, 0);
 
 		/*
 		 * If there are no refs specified to fetch, then we just
remote.c +15 -0
index 99a086ea5a..5e980625b8 100644
@@ -1884,6 +1884,21 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+void collect_upstream_from_remote(struct repository *repo,
+				   struct string_list *tracked,
+				   struct remote *remote,
+				   const char *refname)
+{
+	struct branch *branch = repo_branch_get(repo, refname);
+
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, remote->name))
+		return;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(tracked, branch->merge[i]->src);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +9 -0
index ac485a584d..7c86c529b6 100644
@@ -359,6 +359,15 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/*
+ * If refname's branch builds on remote, add its upstream on remote to
+ * tracked. A NULL refname means the current branch.
+ */
+void collect_upstream_from_remote(struct repository *repo,
+				   struct string_list *tracked,
+				   struct remote *remote,
+				   const char *refname);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
Signature · 1 line
gitgitgadget
#67 lore raw

PATCH v9 3/5 fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |   6 +-
 builtin/fetch.c                  |  59 ++++++++++++---
 remote.c                         |  24 +++++++
 remote.h                         |   4 ++
 t/meson.build                    |   1 +
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 207 insertions(+), 11 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..7c52e443a5 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
 	Used to map remote refs being fetched to remote-tracking refs to
-	store. See the `--refmap` entry in linkgit:git-fetch[1].
+	store. If `remote.<name>.fetch` is not set either, a refspec-less
+	fetch infers what to fetch from local branches built on this
+	remote, instead of fetching every branch it has. See the
+	`--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +4 -2
index c2101a7b39..90b9e13ac2 100644
@@ -245,8 +245,10 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+nothing to fetch is specified on the command line, nor is there a
+`remote.<name>.fetch`, branches from the remote that are used as the
+`@{upstream}` of our local branches are fetched.
 
 `-t`::
 `--tags`::
builtin/fetch.c +51 -8

106 lines not on this page · raw message

remote.c +24 -0
index 5e980625b8..703c71ba45 100644
@@ -1899,6 +1899,30 @@ void collect_upstream_from_remote(struct repository *repo,
 		string_list_insert(tracked, branch->merge[i]->src);
 }
 
+struct branches_tracking_remote_cb_data {
+	struct repository *repo;
+	struct remote *remote;
+	struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+
+	collect_upstream_from_remote(data->repo, data->tracked, data->remote,
+				      ref->name);
+	return 0;
+}
+
+void branches_tracking_remote(struct repository *repo, struct remote *remote,
+			       struct string_list *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { repo, remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(repo),
+				  add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +4 -0
index 7c86c529b6..62265346d3 100644
@@ -368,6 +368,10 @@ void collect_upstream_from_remote(struct repository *repo,
 				   struct remote *remote,
 				   const char *refname);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct repository *repo, struct remote *remote,
+			       struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index f65eb04684..e287a6b947 100644
@@ -730,6 +730,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5586-fetch-refmap.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5586-fetch-refmap.sh new file +119 -0

119 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#68 lore raw

PATCH v9 4/5 remote: add "git remote add --limited-fetch"

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)
Documentation/git-remote.adoc +7 -1
index eaae30aa88..4255f8b3e6 100644
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [synopsis]
 git remote [-v | --verbose]
-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>
 git remote rename [--[no-]progress] <old> <new>
 git remote remove <name>
 git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_
 is created.  You can give more than one `-t <branch>` to track
 multiple branches without grabbing all branches.
 +
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
+details.
++
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
 +
builtin/remote.c +22 -6
index de989ea3ba..f036dd5d6f 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
+	int limited_fetch = -1; /* unspecified */
 	struct string_list track = STRING_LIST_INIT_NODUP;
 	const char *master = NULL;
 	struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
 		OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
 			N_("set up remote as a mirror to push to or fetch from"),
 			PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+		OPT_BOOL(0, "limited-fetch", &limited_fetch,
+			N_("fetch only the branches we build on, instead of every branch")),
 		OPT_END()
 	};
 
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
 		die(_("specifying a master branch makes no sense with --mirror"));
 	if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
 		die(_("specifying branches to track makes sense only with fetch mirrors"));
+	if (limited_fetch == 1 && track.nr)
+		die(_("--limited-fetch does not make sense with -t/--track"));
+	if (limited_fetch == 1 && mirror)
+		die(_("--limited-fetch does not make sense with --mirror"));
 
 	name = argv[0];
 	url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
 	repo_config_set(the_repository, buf.buf, url);
 
 	if (!mirror || mirror & MIRROR_FETCH) {
+		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+			limited_fetch == 1;
+
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "remote.%s.fetch", name);
-		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
-			add_branch(buf.buf, track.items[i].string,
-				   name, mirror, &buf2);
+		if (use_limited_fetch) {
+			strbuf_addf(&buf, "remote.%s.refmap", name);
+			strbuf_reset(&buf2);
+			strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+			repo_config_set(the_repository, buf.buf, buf2.buf);
+		} else {
+			strbuf_addf(&buf, "remote.%s.fetch", name);
+			if (track.nr == 0)
+				string_list_append(&track, "*");
+			for (size_t i = 0; i < track.nr; i++)
+				add_branch(buf.buf, track.items[i].string,
+					   name, mirror, &buf2);
 		}
 	}
 
t/t5505-remote.sh +16 -0
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
 	test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+	test_when_finished "rm -rf full-add" &&
+	git clone --no-local one full-add &&
+	(
+		cd full-add &&
+		git remote add --limited-fetch upstream ../two &&
+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+			remote.upstream.refmap &&
+		test_must_fail git config get remote.upstream.fetch
+	)
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+	test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
 	(
 		cd test &&
Signature · 1 line
gitgitgadget
#69 lore raw

PATCH v9 5/5 remote: default to --limited-fetch in a shallow repository

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index 4255f8b3e6..80f8a4a183 100644
@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
-details.
+details. `--no-limited-fetch` explicitly disables this, overriding the
+shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +4 -1
index f036dd5d6f..0cf1126976 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
 	if (!mirror || mirror & MIRROR_FETCH) {
 		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-			limited_fetch == 1;
+			(limited_fetch == 1 ||
+			 (limited_fetch == -1 &&
+			  is_repository_shallow(the_repository)));
 
 		strbuf_reset(&buf);
 		if (use_limited_fetch) {
t/t5505-remote.sh +60 -0

66 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#70 lore raw

v10 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v10:

* Resolve merge conflict after master integrated
ch/fetch-followremotehead-lazy-validation.

Changes in v9:

* Moved the do_fetch() if/else-if/else split out of the preliminary helper-extraction commit and into the refmap-inference commit that needs it. Preliminary commit is now a simpler refactor.

Changes in v8:

* Split into a preliminary collect_upstream_from_remote() helper commit plus a simplified refmap-inference commit.
* Fixed a regression that could drop the current branch's @{upstream} when remote.<name>.fetch was narrowed.
* Threaded struct repository * through instead of hardcoding the_repository.
* Minor naming and comment cleanups.

Changes in v7:

* Fixed fatal: --refmap option is only meaningful with command-line refspec(s) firing just from configuring remote.<name>.refmap alone, with nothing else to fetch.
* When both remote.<name>.fetch and remote.<name>.refmap are configured, a refspec-less fetch follows .fetch as usual, .refmap only remaps something already being fetched by name. Added tests.

Changes in v6:

* Remove leftover reference to deleted default-branch logic in commit message.

Changes in v5:

* Renumber test file from t5585 to t5586.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (5):
fetch: add remote.<name>.refmap
fetch: extract collect_upstream_from_remote() helper
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |   5 ++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  92 ++++++++++++++++++++----
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  51 ++++++++++++-
 remote.h                         |  19 +++++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 |  63 ++++++++++++++++
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 456 insertions(+), 23 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

base-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v10
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v10
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v9:

 1:  2a48c21bc8 = 1:  ba7f1354cd fetch: add remote.<name>.refmap
 2:  50382bce7e ! 2:  aa20604144 fetch: extract collect_upstream_from_remote() helper
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       		if (transport->remote->fetch.nr) {
       			refspec_ref_prefixes(&transport->remote->fetch,
      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     - 				do_set_head = 1;
     + 			else
     + 				follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
       		}
      -		if (branch && branch_has_merge_config(branch) &&
      -		    !strcmp(branch->remote_name, transport->remote->name)) {
 3:  9ad8ad3eb4 ! 3:  9fb7e77a86 fetch: infer branches to fetch from a refmap-only remote
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       
       	if (rs->nr) {
       		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
     --	} else {
      +	} else if (transport->remote->fetch.nr) {
     - 		struct string_list tracked = STRING_LIST_INIT_DUP;
     - 		struct string_list_item *item;
     - 
     --		if (transport->remote->fetch.nr) {
     --			refspec_ref_prefixes(&transport->remote->fetch,
     --					     &transport_ls_refs_options.ref_prefixes);
     --			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     --				do_set_head = 1;
     --		}
     ++		struct string_list tracked = STRING_LIST_INIT_DUP;
     ++		struct string_list_item *item;
     ++
      +		refspec_ref_prefixes(&transport->remote->fetch,
      +				     &transport_ls_refs_options.ref_prefixes);
     -+		if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     -+			do_set_head = 1;
     - 
     - 		/*
     - 		 * The configured refspec may not cover the current
     -@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 			strvec_push(&transport_ls_refs_options.ref_prefixes,
     - 				    item->string);
     - 		string_list_clear(&tracked, 0);
     -+	} else {
     ++		/*
     ++		 * See remote.c's handling of remote.<name>.followRemoteHEAD
     ++		 * for the analogous, still-unresolved case.
     ++		 */
     ++		if (transport->remote->follow_remote_head)
     ++			follow_remote_head = transport->remote->follow_remote_head;
     ++		else if (config->follow_remote_head_seen)
     ++			follow_remote_head = get_follow_remote_head(config->follow_remote_head_raw);
     ++		else
     ++			follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
     ++
     ++		/*
     ++		 * The configured refspec may not cover the current
     ++		 * branch's upstream (e.g. a narrowed -t refspec), so
     ++		 * make sure we can still fetch it regardless.
     ++		 */
     ++		collect_upstream_from_remote(the_repository, &tracked,
     ++					      transport->remote, NULL);
     ++		for_each_string_list_item(item, &tracked)
     ++			strvec_push(&transport_ls_refs_options.ref_prefixes,
     ++				    item->string);
     ++		string_list_clear(&tracked, 0);
     + 	} else {
      +		/*
      +		 * The --refmap command line option, if given, takes
      +		 * precedence over remote.<name>.refmap.
      +		 */
      +		struct refspec *effective_refmap = refmap.nr ? &refmap :
      +			&transport->remote->refmap;
     -+		struct string_list tracked = STRING_LIST_INIT_DUP;
     -+		struct string_list_item *item;
     -+
     + 		struct string_list tracked = STRING_LIST_INIT_DUP;
     + 		struct string_list_item *item;
     + 
     +-		if (transport->remote->fetch.nr) {
     +-			refspec_ref_prefixes(&transport->remote->fetch,
     +-					     &transport_ls_refs_options.ref_prefixes);
      +		if (effective_refmap->nr) {
      +			branches_tracking_remote(the_repository,
      +						  transport->remote, &tracked);
     -+			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
     -+				do_set_head = 1;
     + 			/*
     + 			 * See remote.c's handling of remote.<name>.followRemoteHEAD
     + 			 * for the analogous, still-unresolved case.
     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 				follow_remote_head = get_follow_remote_head(config->follow_remote_head_raw);
     + 			else
     + 				follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
      +		} else {
      +			collect_upstream_from_remote(the_repository, &tracked,
      +						      transport->remote, NULL);
     -+		}
     -+		for_each_string_list_item(item, &tracked)
     -+			strvec_push(&transport_ls_refs_options.ref_prefixes,
     -+				    item->string);
     -+		string_list_clear(&tracked, 0);
     - 
     - 		/*
     - 		 * If there are no refs specified to fetch, then we just
     + 		}
     +-
     +-		/*
     +-		 * The configured refspec may not cover the current
     +-		 * branch's upstream (e.g. a narrowed -t refspec), so
     +-		 * make sure we can still fetch it regardless.
     +-		 */
     +-		collect_upstream_from_remote(the_repository, &tracked,
     +-					      transport->remote, NULL);
     + 		for_each_string_list_item(item, &tracked)
     + 			strvec_push(&transport_ls_refs_options.ref_prefixes,
     + 				    item->string);
      
       ## remote.c ##
      @@ remote.c: void collect_upstream_from_remote(struct repository *repo,
 4:  75169f668e = 4:  80028d4f86 remote: add "git remote add --limited-fetch"
 5:  9ee090d3d0 = 5:  552bf08971 remote: default to --limited-fetch in a shallow repository
Signature · 1 line
gitgitgadget
#71 lore raw

PATCH v10 1/5 fetch: add remote.<name>.refmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Like --refmap itself, it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

A refspec-less fetch with a refmap configured but nothing else to say what to fetch falls back to the same defaults as a remote with no refspec or refmap at all. Only an explicit --refmap given on the command line with no command-line refspec to go with it is still rejected, since that combination is a plain mistake to type, unlike a remote.<name>.refmap configured on its own. A remote.<name>.fetch that is also configured is used as before.

---
 Documentation/config/remote.adoc |  5 +++
 Documentation/fetch-options.adoc |  3 ++
 builtin/fetch.c                  | 10 ++++-
 remote.c                         | 12 +++++-
 remote.h                         |  6 +++
 t/t5510-fetch.sh                 | 63 ++++++++++++++++++++++++++++++++
 6 files changed, 96 insertions(+), 3 deletions(-)
Documentation/config/remote.adoc +5 -0
index 3a20d0f752..103eada406 100644
@@ -33,6 +33,11 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Used to map remote refs being fetched to remote-tracking refs to
+	store. See the `--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 47dea1de8e..c2101a7b39 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +8 -2
index 1f76ae6242..ffcb5ba29a 100644
@@ -518,6 +518,12 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	/*
+	 * The --refmap command line option, if given, takes precedence
+	 * over remote.<name>.refmap.
+	 */
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -561,8 +567,8 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
remote.c +11 -1
index 49ee2ea318..c9252a90f4 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -995,7 +1003,9 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	if (remote->fetch.nr)
+		return refspec_find_match(&remote->fetch, refspec);
+	return refspec_find_match(&remote->refmap, refspec);
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +63 -0

69 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#72 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 10 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Like --refmap itself, it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

Quoted 7 lines

A refspec-less fetch with a refmap configured but nothing else to say what to fetch falls back to the same defaults as a remote with no refspec or refmap at all. Only an explicit --refmap given on the command line with no command-line refspec to go with it is still rejected, since that combination is a plain mistake to type, unlike a remote.<name>.refmap configured on its own. A remote.<name>.fetch that is also configured is used as before.

A bit wordy in the 2nd paragraph (and LLM-tasting), but… :shrug:

remote.c void ref_push_report_free(struct ref_push_report *report) · line 1009

Quoted 11 lines

---

int remote_find_tracking(struct remote *remote, struct refspec_item *refspec) {

remote.c +3 -1
-       return refspec_find_match(&remote->fetch, refspec);
+       if (remote->fetch.nr)
+               return refspec_find_match(&remote->fetch, refspec);
+       return refspec_find_match(&remote->refmap, refspec);
 }

I think this appeared in v2 a bit open-coded, then was refactored in v3, but I haven't seen any comment on it? I might just be missing some.

Anyway, let me think aloud about this. At first glance, we can't fallback to refmap if fetch *fails* to find refspec. Should we or not? I'm not sure.

remote.h says this procedure does "For the given remote, reads the refspec's src and sets the other fields." Ok, and glancing at refspec_find_match the idea is that "refspec" input is a query (with a src or dst) and we find the matching entry and fill out the input's fields.

So… I assume the usual idea would be that refmap is a "superset" of the fetch refspec, since I might want to map refs that I don't normally fetch. (Or it might be disjoint, if none of the mappings apply to my usual fetches.) But that means that remote_find_tracking() won't be able to find entries for the mapped refs that aren't part of my "normal" fetches? Are there downstream consequences of that? I'm not sure, I haven't looked at how else remote_find_tracking is used.

Could that affect, for example, --delete-merged?

Signature · 1 line
D. Ben Knoble
#73 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 14 lines

remote.h says this procedure does "For the given remote, reads the refspec's src and sets the other fields." Ok, and glancing at refspec_find_match the idea is that "refspec" input is a query (with a src or dst) and we find the matching entry and fill out the input's fields.

So… I assume the usual idea would be that refmap is a "superset" of the fetch refspec, since I might want to map refs that I don't normally fetch. (Or it might be disjoint, if none of the mappings apply to my usual fetches.) But that means that remote_find_tracking() won't be able to find entries for the mapped refs that aren't part of my "normal" fetches? Are there downstream consequences of that? I'm not sure, I haven't looked at how else remote_find_tracking is used.

Quoted 1 line

Could that affect, for example, --delete-merged?

Good point, I will change it like this:

```
 1:  ba7f1354cd ! 16:  4603c84d68 fetch: add remote.<name>.refmap
    @@ remote.c: void ref_push_report_free(struct ref_push_report *report)
      int remote_find_tracking(struct remote *remote, struct
refspec_item *refspec)
      {
     -  return refspec_find_match(&remote->fetch, refspec);
    -+  if (remote->fetch.nr)
    -+          return refspec_find_match(&remote->fetch, refspec);
    -+  return refspec_find_match(&remote->refmap, refspec);
    ++  if (!remote->fetch.nr || refspec_find_match(&remote->fetch, refspec))
    ++          return refspec_find_match(&remote->refmap, refspec);
    ++  return 0;
      }

      static struct ref *alloc_ref_with_prefix(const char *prefix,
size_t prefixlen,
```

Harald

#74 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 29 lines · Harald Nordgren
Quoted 15 lines

remote.h says this procedure does "For the given remote, reads the refspec's src and sets the other fields." Ok, and glancing at refspec_find_match the idea is that "refspec" input is a query (with a src or dst) and we find the matching entry and fill out the input's fields.

So… I assume the usual idea would be that refmap is a "superset" of the fetch refspec, since I might want to map refs that I don't normally fetch. (Or it might be disjoint, if none of the mappings apply to my usual fetches.) But that means that remote_find_tracking() won't be able to find entries for the mapped refs that aren't part of my "normal" fetches? Are there downstream consequences of that? I'm not sure, I haven't looked at how else remote_find_tracking is used.

Quoted 1 line

Could that affect, for example, --delete-merged?

Good point, I will change it like this:

```
 1:  ba7f1354cd ! 16:  4603c84d68 fetch: add remote.<name>.refmap
    @@ remote.c: void ref_push_report_free(struct ref_push_report *report)
      int remote_find_tracking(struct remote *remote, struct
refspec_item *refspec)
      {
     -  return refspec_find_match(&remote->fetch, refspec);
    -+  if (remote->fetch.nr)
    -+          return refspec_find_match(&remote->fetch, refspec);
    -+  return refspec_find_match(&remote->refmap, refspec);
Quoted 8 lines
    ++  if (!remote->fetch.nr || refspec_find_match(&remote->fetch, refspec))
    ++          return refspec_find_match(&remote->refmap, refspec);
    ++  return 0;
      }

      static struct ref *alloc_ref_with_prefix(const char *prefix,
size_t prefixlen,
```

That does look like it would fall back on the refmap, but to be honest I'm not sure what is "correct" here :) I was hoping for more discussion of the questions I raised to see some thought about what we should or shouldn't do. I suppose falling back might be important for the later step with --limited-fetch, though?

Signature · 1 line
D. Ben Knoble
#75 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 41 lines · D. Ben Knoble
Quoted 29 lines · Harald Nordgren
Quoted 15 lines

remote.h says this procedure does "For the given remote, reads the refspec's src and sets the other fields." Ok, and glancing at refspec_find_match the idea is that "refspec" input is a query (with a src or dst) and we find the matching entry and fill out the input's fields.

So… I assume the usual idea would be that refmap is a "superset" of the fetch refspec, since I might want to map refs that I don't normally fetch. (Or it might be disjoint, if none of the mappings apply to my usual fetches.) But that means that remote_find_tracking() won't be able to find entries for the mapped refs that aren't part of my "normal" fetches? Are there downstream consequences of that? I'm not sure, I haven't looked at how else remote_find_tracking is used.

Quoted 1 line

Could that affect, for example, --delete-merged?

Good point, I will change it like this:

```
 1:  ba7f1354cd ! 16:  4603c84d68 fetch: add remote.<name>.refmap
    @@ remote.c: void ref_push_report_free(struct ref_push_report *report)
      int remote_find_tracking(struct remote *remote, struct
refspec_item *refspec)
      {
     -  return refspec_find_match(&remote->fetch, refspec);
    -+  if (remote->fetch.nr)
    -+          return refspec_find_match(&remote->fetch, refspec);
    -+  return refspec_find_match(&remote->refmap, refspec);
Quoted 8 lines
    ++  if (!remote->fetch.nr || refspec_find_match(&remote->fetch, refspec))
    ++          return refspec_find_match(&remote->refmap, refspec);
    ++  return 0;
      }

      static struct ref *alloc_ref_with_prefix(const char *prefix,
size_t prefixlen,
```
Quoted 5 lines

That does look like it would fall back on the refmap, but to be honest I'm not sure what is "correct" here :) I was hoping for more discussion of the questions I raised to see some thought about what we should or shouldn't do. I suppose falling back might be important for the later step with --limited-fetch, though?

Without the fallback, @{upstream}, --delete-merged and "git branch --track" fail for refs fetched through the refmap.

I'll add tests to lock down that such refs are tracked through the refmap, and that refs excluded by a negative refspec in remote.<name>.fetch are not.

--limited-fetch does not need the fallback.

Harald

#76 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

In reply to D. Ben Knoble
Quoted 5 lines · D. Ben Knoble

That does look like it would fall back on the refmap, but to be honest I'm not sure what is "correct" here :) I was hoping for more discussion of the questions I raised to see some thought about what we should or shouldn't do. I suppose falling back might be important for the later step with --limited-fetch, though?

I just encountered a case where I wished to configure both refmap and fetch at the same time. From a second remote (let's call it "cabal"), store any branch fetched from them in refs/remotes/cabal/ but I want to fetch only their 'master', 'next' and 'seen'. I'd want to be able to write something like:

[remote "cabal"]
fetch = master next seen
refmap = +refs/heads/*:refs/remotes/cabal/*

And, no, this does not require any inferring based on what is checked out and what the checked out branch tracks.

#77 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 12 lines
Quoted 5 lines

That does look like it would fall back on the refmap, but to be honest I'm not sure what is "correct" here :) I was hoping for more discussion of the questions I raised to see some thought about what we should or shouldn't do. I suppose falling back might be important for the later step with --limited-fetch, though?

I just encountered a case where I wished to configure both refmap and fetch at the same time. From a second remote (let's call it "cabal"), store any branch fetched from them in refs/remotes/cabal/ but I want to fetch only their 'master', 'next' and 'seen'. I'd want to be able to write something like:

Quoted 6 lines

[remote "cabal"]
fetch = master next seen
refmap = +refs/heads/*:refs/remotes/cabal/*

And, no, this does not require any inferring based on what is checked out and what the checked out branch tracks.

Yes, I think we can handle this as well with some extra code.

Harald

#78 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 12 lines · Harald Nordgren
Quoted 12 lines
Quoted 5 lines

That does look like it would fall back on the refmap, but to be honest I'm not sure what is "correct" here :) I was hoping for more discussion of the questions I raised to see some thought about what we should or shouldn't do. I suppose falling back might be important for the later step with --limited-fetch, though?

I just encountered a case where I wished to configure both refmap and fetch at the same time. From a second remote (let's call it "cabal"), store any branch fetched from them in refs/remotes/cabal/ but I want to fetch only their 'master', 'next' and 'seen'. I'd want to be able to write something like:

Quoted 8 lines
Quoted 6 lines

[remote "cabal"]
fetch = master next seen
refmap = +refs/heads/*:refs/remotes/cabal/*

And, no, this does not require any inferring based on what is checked out and what the checked out branch tracks.

Yes, I think we can handle this as well with some extra code.

I actually do not think we should have master next seen on the same remote.cabal.fetch configuration variable. It is a multi-valued variable, so it should read more like

[remote "cabal"]
fetch = refs/heads/master
fetch = refs/heads/next
fetch = refs/heads/seen
refmap = +refs/heads/*:refs/remotes/cabal/*

And without these remote.cabal.fetch configured, but with remote.cabal.refmap configured, this command line

	$ git fetch cabal next

should work just like

	$ git fetch cabal \
		--refmap="+refs/heads/*:refs/remotes/cabal/*" \
		next

I think.

Thanks.

#79 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 10 lines · Junio C Hamano

I actually do not think we should have master next seen on the same remote.cabal.fetch configuration variable. It is a multi-valued variable, so it should read more like

[remote "cabal"]
fetch = refs/heads/master
fetch = refs/heads/next
fetch = refs/heads/seen
refmap = +refs/heads/*:refs/remotes/cabal/*

Doesn't that cause older versions of git to try and update the local branch when fetching, or is it only when pushing that we'll fill in the right hand side of the refspec with a copy of the left hand side? If we use a different config setting for "what to fetch" then users that want the new behavior can unset remote.<name>.fetch and older versions of git will fail rather than overwriting local branches when there is no refspec on the commandline.

Thanks

Phillip

Quoted 15 lines

And without these remote.cabal.fetch configured, but with remote.cabal.refmap configured, this command line

	$ git fetch cabal next

should work just like

	$ git fetch cabal \
		--refmap="+refs/heads/*:refs/remotes/cabal/*" \
		next

I think.

Thanks.

#80 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 12 lines · Phillip Wood
Quoted 10 lines · Junio C Hamano

I actually do not think we should have master next seen on the same remote.cabal.fetch configuration variable. It is a multi-valued variable, so it should read more like

[remote "cabal"]
fetch = refs/heads/master
fetch = refs/heads/next
fetch = refs/heads/seen
refmap = +refs/heads/*:refs/remotes/cabal/*

Quoted 3 lines

Doesn't that cause older versions of git to try and update the local branch when fetching, or is it only when pushing that we'll fill in the right hand side of the refspec with a copy of the left hand side?

"git fetch --help" is fairly clear on what should happen in its description on <refspec> command line arguments, and

I meant in the above example that remote.cabal.fetch should be used as if these three refnames are written on the fetch command line when nothing is given from the command line. Now, <refspec> that does not have :dst side for fetch stores the result nowhere but FETCH_HEAD, so without remote.cabal.refmap (i.e., older versions of git), you will get them in FETCH_HEAD, so your "git pull" will still work to create an octopus merge ;-)

I was hoping that with refmap, the users can tell the system where to store them. As specifying a shorthand also works,

[remote "cabal"]
fetch = master
fetch = next
fetch = seen
refmap = +refs/heads/*:refs/remotes/cabal/*

would be an easy layout to allow adding different branches than having to say

[remote "cabal"]
fetch = +refs/heads/master:refs/remotes/cabal/master fetch = +refs/heads/next:refs/remotes/cabal/next fetch = +refs/heads/seen:refs/remotes/cabal/seen

Thanks.

#81 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 6 lines · Junio C Hamano
Quoted 4 lines · Phillip Wood

Doesn't that cause older versions of git to try and update the local branch when fetching, or is it only when pushing that we'll fill in the right hand side of the refspec with a copy of the left hand side?

Quoted 7 lines

I meant in the above example that remote.cabal.fetch should be used as if these three refnames are written on the fetch command line when nothing is given from the command line. Now, <refspec> that does not have :dst side for fetch stores the result nowhere but FETCH_HEAD, so without remote.cabal.refmap (i.e., older versions of git), you will get them in FETCH_HEAD, so your "git pull" will still work to create an octopus merge ;-)

Thanks for clarifying that

Quoted 9 lines

I was hoping that with refmap, the users can tell the system where to store them. As specifying a shorthand also works,

[remote "cabal"]
fetch = master
fetch = next
fetch = seen
refmap = +refs/heads/*:refs/remotes/cabal/*

Quoted 7 lines

would be an easy layout to allow adding different branches than having to say

[remote "cabal"]
fetch = +refs/heads/master:refs/remotes/cabal/master fetch = +refs/heads/next:refs/remotes/cabal/next fetch = +refs/heads/seen:refs/remotes/cabal/seen

That makes sense. What should we do if "remote.cabal.refmap" exists and "remote.cabal.fetch" contains a colon? We could just ignore that entry but it might be better to error out.

Thanks

Phillip

#82 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 12 lines · Phillip Wood
Quoted 12 lines

I was hoping that with refmap, the users can tell the system where to store them. As specifying a shorthand also works,

[remote "cabal"]
fetch = master
fetch = next
fetch = seen
refmap = +refs/heads/*:refs/remotes/cabal/*

would be an easy layout to allow adding different branches than having to say

Quoted 8 lines
Quoted 4 lines

[remote "cabal"]
fetch = +refs/heads/master:refs/remotes/cabal/master fetch = +refs/heads/next:refs/remotes/cabal/next fetch = +refs/heads/seen:refs/remotes/cabal/seen

That makes sense. What should we do if "remote.cabal.refmap" exists and "remote.cabal.fetch" contains a colon? We could just ignore that entry but it might be better to error out.

Possible alternatives include

- giving fetch with dst take precedence over refmap
- storing the result in two locations
- error out not knowing what is the right thing to do

I think the command line version, i.e.

    $ git fetch $URL --refmap='refs/heads/*:refs/remotes/alt/*' \
		refs/heads/master:refs/remotes/origin/master \
		next

stores 'master' as alt/master and origin/master, and 'next' as alt/next, i.e., takes the second option.

Thanks.

#83 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 24 lines · Junio C Hamano
Quoted 12 lines · Phillip Wood
Quoted 12 lines

I was hoping that with refmap, the users can tell the system where to store them. As specifying a shorthand also works,

[remote "cabal"]
fetch = master
fetch = next
fetch = seen
refmap = +refs/heads/*:refs/remotes/cabal/*

would be an easy layout to allow adding different branches than having to say

Quoted 8 lines
Quoted 4 lines

[remote "cabal"]
fetch = +refs/heads/master:refs/remotes/cabal/master fetch = +refs/heads/next:refs/remotes/cabal/next fetch = +refs/heads/seen:refs/remotes/cabal/seen

That makes sense. What should we do if "remote.cabal.refmap" exists and "remote.cabal.fetch" contains a colon? We could just ignore that entry but it might be better to error out.

Quoted 5 lines

Possible alternatives include

- giving fetch with dst take precedence over refmap
- storing the result in two locations
- error out not knowing what is the right thing to do

The answer seems to be the second one, with my v12, and I wrote a test now to confirm it, I can include it in a v13 when it's time:

```
+test_expect_success 'remote.<name>.fetchRefmap stores an explicit-dst
remote.<name>.fetch entry in both places' '
+       test_when_finished "rm -fr fetch-refmap-upstream downstream" &&
+       git init -b master fetch-refmap-upstream &&
+       test_commit -C fetch-refmap-upstream base &&
+       git init downstream &&
+       (
+               cd downstream &&
+               git remote add cabal ../fetch-refmap-upstream &&
+               git config --unset-all remote.cabal.fetch &&
+               git config --add remote.cabal.fetch \
+                       "+refs/heads/master:refs/remotes/custom/master" &&
+               git config remote.cabal.fetchRefmap
"+refs/heads/*:refs/remotes/cabal/*" &&
+               git fetch cabal &&
+               check_fetched_refs <<-\EOF
+               refs/remotes/cabal/HEAD
+               refs/remotes/cabal/master
+               refs/remotes/custom/master
+               EOF
+       )
+'
```

Harald

#84 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

In reply to Junio C Hamano
Quoted 13 lines · Junio C Hamano
Quoted 4 lines · Phillip Wood

That makes sense. What should we do if "remote.cabal.refmap" exists and "remote.cabal.fetch" contains a colon? We could just ignore that entry but it might be better to error out.

Possible alternatives include

- giving fetch with dst take precedence over refmap
- storing the result in two locations
- error out not knowing what is the right thing to do

Quoted 8 lines

I think the command line version, i.e.

     $ git fetch $URL --refmap='refs/heads/*:refs/remotes/alt/*' \
		refs/heads/master:refs/remotes/origin/master \
		next

stores 'master' as alt/master and origin/master, and 'next' as alt/next, i.e., takes the second option.

That's surprising, I'd naively assumed a refspec on the command line would override the refmap. It feels problematic if we cannot override the fetchRefmap config with a refspec on the command line, but need to use "--refmap" instead.

I think there are at least a couple of ways of thinking about the interaction of "remote.<name>.fetchRefmap" and "remote.<name>.fetch". The first is to consider that the "fetchRefmap" config is for "where to store the fetched refs" and the "fetch" config is for "what to fetch". In that case we'd reject values containing colons in the "fetch" config as any mapping should be done by the "fetchRefmap" config. The other way is to consider that the "fetchRefmap" config holds the default argument for "--refmap" and "fetch" holds the default command line refspecs to use when the user doesn't specify any. In that case we should accept "fetch" values containing colons and behave as if those values had been given as command line arguments. I'm leaning towards the latter, but if we do decide to do that, I think we should make sure

  git -c remote.cabal.fetchRefmap='refs/heads/*:refs/remotes/cabal/*' \
    fetch cabal refs/heads/master:refs/remotes/origin/master

only updates origin/master and not cabal/master.

Thanks

Phillip

#85 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 6 lines · Phillip Wood
Quoted 6 lines

I think the command line version, i.e.

     $ git fetch $URL --refmap='refs/heads/*:refs/remotes/alt/*' \
		refs/heads/master:refs/remotes/origin/master \
		next
Quoted 7 lines
Quoted 2 lines

stores 'master' as alt/master and origin/master, and 'next' as alt/next, i.e., takes the second option.

That's surprising, I'd naively assumed a refspec on the command line would override the refmap. It feels problematic if we cannot override the fetchRefmap config with a refspec on the command line, but need to use "--refmap" instead.

Maybe, maybe not. The command line refmap was meant to primarily help use case where the "what to fetch" was to be specified without colon, so when fetching multiple branches, you do not have to repeat the refs/remotes/origin/ all over again. So if you are giving a refspec with a colon (like 'master' in the above example) to specify where to store, it is an unusual request to do something different.

Something different could be "do more than what the refmap alone would do", which led to the behaviour implemented when refmap was introduced. But it could be "refrain from what the refmap alone would do and do something else", and if you take that position, you would get the "overriding" behaviour.

#86 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

In reply to Junio C Hamano
Quoted 10 lines · Junio C Hamano

I just encountered a case where I wished to configure both refmap and fetch at the same time. From a second remote (let's call it "cabal"), store any branch fetched from them in refs/remotes/cabal/ but I want to fetch only their 'master', 'next' and 'seen'. I'd want to be able to write something like:

[remote "cabal"]
fetch = master next seen
refmap = +refs/heads/*:refs/remotes/cabal/*

That's a coincidence, I sent a message earlier [1] arguing that we should have separate config settings for "what to fetch" and "where to store it"

Thanks

Phillip

[1] https://lore.kernel.org/git/[email protected]

Quoted 3 lines

And, no, this does not require any inferring based on what is checked out and what the checked out branch tracks.

#87 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 8 lines · Phillip Wood
Quoted 7 lines · Junio C Hamano

I just encountered a case where I wished to configure both refmap and fetch at the same time. From a second remote (let's call it "cabal"), store any branch fetched from them in refs/remotes/cabal/ but I want to fetch only their 'master', 'next' and 'seen'. I'd want to be able to write something like:

Quoted 7 lines
Quoted 3 lines

[remote "cabal"]
fetch = master next seen
refmap = +refs/heads/*:refs/remotes/cabal/*

That's a coincidence, I sent a message earlier [1] arguing that we should have separate config settings for "what to fetch" and "where to store it"

In other words, "what to fetch" and "where to store them" have separate configuration. They should be made to interact correctly and the current patchset may or may not work the way we want.

What we need a separate setting is probably what should happen when we do not specify "what to fetch" at all. One heuristics is what this series proposes i.e., "what our local branches build on". But that does not have to stay the only option forever.

#88 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

In reply to Harald Nordgren via GitGitGadget

Hi Harald

Quoted 5 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch.

The second half of this sentence seems redundant to me. Can we please call the new config setting "remote.<name>.fetchRefmap" so that it is clear it applies to fetches. That would also allow us to add a matching "remote.<name>.pushRefmap" in the future to map the refnames of refs that are pushed without setting up a default set of refs to push.

Quoted 4 lines

Like --refmap itself,
it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

So the default value behaves like the option - why wouldn't it?

Quoted 7 lines

A refspec-less fetch with a refmap configured but nothing else to say what to fetch falls back to the same defaults as a remote with no refspec or refmap at all. Only an explicit --refmap given on the command line with no command-line refspec to go with it is still rejected, since that combination is a plain mistake to type, unlike a remote.<name>.refmap configured on its own. A remote.<name>.fetch that is also configured is used as before.

I'm afraid I cannot understand what this paragraph is trying to say.

Documentation/config/remote.adoc remote.<name>.fetch:: · line 39

Quoted 6 lines
Documentation/config/remote.adoc +4 -0
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Used to map remote refs being fetched to remote-tracking refs to
+	store. See the `--refmap` entry in linkgit:git-fetch[1].

I'm not sure we need to link to the git fetch documentation twice. It would be useful to explain how this interacts with remote.<name>.fetch. The documentation for "--refmap" says that if it is given the refspec config is ignored - so does setting this mean remote.<name>.fetch is ignored?

Documentation/fetch-options.adoc endif::git-pull[] · line 249

Quoted 4 lines
Documentation/fetch-options.adoc +3 -0
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.

As above the second half of this sentence doesn't really add anything.

Thanks

Phillip

Quoted 166 lines

-t::
--tags::

builtin/fetch.c +8 -2
index 1f76ae6242..ffcb5ba29a 100644
@@ -518,6 +518,12 @@ static struct ref *get_ref_map(struct remote *remote,
  	struct ref *rm;
  	struct ref *ref_map = NULL;
  	struct ref **tail = &ref_map;
+	/*
+	 * The --refmap command line option, if given, takes precedence
+	 * over remote.<name>.refmap.
+	 */
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
  	/* opportunistically-updated references: */
  	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -561,8 +567,8 @@ static struct ref *get_ref_map(struct remote *remote,
  		 * by ref_remove_duplicates() in favor of one of these
  		 * opportunistic entries with FETCH_HEAD_IGNORE.
  		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
  		else
  			fetch_refspec = &remote->fetch;
 
remote.c +11 -1
index 49ee2ea318..c9252a90f4 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
  	ret->name = xstrndup(name, len);
  	refspec_init_push(&ret->push, the_hash_algo);
  	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
  	string_list_init_dup(&ret->server_options);
  	string_list_init_dup(&ret->negotiation_restrict);
  	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
  	refspec_clear(&remote->push);
  	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
  	free((char *)remote->receivepack);
  	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
  			return -1;
  		refspec_append(&remote->fetch, v);
  		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
  	} else if (!strcmp(subkey, "receivepack")) {
  		char *v;
  		if (git_config_string(&v, key, value))
@@ -995,7 +1003,9 @@ void ref_push_report_free(struct ref_push_report *report)
 
  int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
  {
-	return refspec_find_match(&remote->fetch, refspec);
+	if (remote->fetch.nr)
+		return refspec_find_match(&remote->fetch, refspec);
+	return refspec_find_match(&remote->refmap, refspec);
  }
 
  static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
  	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
  	/*
  	 * The setting for whether to fetch tags (as a separate rule from the
  	 * configured refspecs);
t/t5510-fetch.sh +63 -0
index 17c0be499c..ec5cd9dca6 100755
@@ -927,6 +927,69 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '
  	)
  '
 
+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '
+	test_when_finished "git -C three config --unset remote.origin.refmap" &&
+	git branch -f side &&
+	git -C three config remote.origin.refmap \
+		"refs/heads/*:refs/remotes/other/*" &&
+	(
+		cd three &&
+		git update-ref refs/remotes/origin/main base-origin-main &&
+		o=$(git rev-parse --verify refs/remotes/origin/main) &&
+		git fetch origin main &&
+		n=$(git rev-parse --verify refs/remotes/origin/main) &&
+		test "$o" = "$n" &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/side &&
+		git rev-parse --verify refs/remotes/other/main
+	)
+'
+
+check_fetched_refs () {
+	git for-each-ref --format="%(refname)" refs/remotes/ >actual &&
+	cat >expect &&
+	test_cmp expect actual
+}
+
+test_expect_success 'remote.<name>.refmap without tracking (baseline)' '
+	test_when_finished "rm -fr fetch-refmap-baseline fetch-refmap-upstream" &&
+	git init -b main fetch-refmap-upstream &&
+	test_commit -C fetch-refmap-upstream base &&
+	git -C fetch-refmap-upstream branch other &&
+	git init fetch-refmap-baseline &&
+	(
+		cd fetch-refmap-baseline &&
+		git remote add origin ../fetch-refmap-upstream &&
+
+		# Without fetch refspec, but with fetch refmap.
+		git config --unset-all remote.origin.fetch &&
+		git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" &&
+
+		# Nothing tracked, nothing fetched, no error.
+		git fetch origin &&
+		check_fetched_refs <<-\EOF &&
+		EOF
+
+		# Nothing tracked, explicit ref on the command line.
+		git fetch origin main &&
+		check_fetched_refs <<-\EOF &&
+		refs/remotes/origin/main
+		EOF
+
+		# With both refmap and fetch configured, remote.<name>.fetch
+		# wins: a refspec-less fetch follows it as usual, and
+		# remote.<name>.refmap plays no part in deciding what to
+		# fetch, only in remapping something that is already being
+		# fetched by name.
+		git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" &&
+		git fetch origin &&
+		check_fetched_refs <<-\EOF
+		refs/remotes/origin/HEAD
+		refs/remotes/origin/main
+		refs/remotes/origin/other
+		EOF
+	)
+'
+
  test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '
  	git branch -f side &&
  	(
#89 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 12 lines · Phillip Wood

Hi Harald

Quoted 5 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>
Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch.

The second half of this sentence seems redundant to me. Can we please call the new config setting "remote.<name>.fetchRefmap" so that it is clear it applies to fetches. That would also allow us to add a matching "remote.<name>.pushRefmap" in the future to map the refnames of refs that are pushed without setting up a default set of refs to push.

Yes, I would love to add pushRefmap atop later! It would ease my working with notes, I think.

I’m on mobile now, but if I remember I’ll try to spell out what I mean more concretely. Thanks for the reminder!

#90 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 12 lines · Ben Knoble
Quoted 12 lines · Phillip Wood

Hi Harald

Quoted 5 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>
Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch.

Quoted 7 lines
Quoted 1 line

The second half of this sentence seems redundant to me. Can we please call the new config setting "remote.<name>.fetchRefmap" so that it is clear it applies to fetches. That would also allow us to add a matching "remote.<name>.pushRefmap" in the future to map the refnames of refs that are pushed without setting up a default set of refs to push.

Yes, I would love to add pushRefmap atop later! It would ease my working with notes, I think.

I’m on mobile now, but if I remember I’ll try to spell out what I mean more concretely. Thanks for the reminder!

It has already been renamed to fetchRefmap in v12.

Harald

#91 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

In reply to Ben Knoble
Quoted 5 lines · Ben Knoble

Yes, I would love to add pushRefmap atop later! It would ease my working with notes, I think.

Yes, I would love to add pushRefmap atop later! It would ease my working with notes, I think.

Tell us more when you are off mobile ;-).

I do not think I planned to add '--refmap' support on the push side, even from the command line, but I can imagine there would be reasonable workflows that benefit from having one.

#92 lore raw

Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap

Quoted 6 lines · Junio C Hamano
Quoted 2 lines · Ben Knoble

Yes, I would love to add pushRefmap atop later! It would ease my working with notes, I think.

Quoted 8 lines
Quoted 2 lines

Yes, I would love to add pushRefmap atop later! It would ease my working with notes, I think.

Tell us more when you are off mobile ;-).

I do not think I planned to add '--refmap' support on the push side, even from the command line, but I can imagine there would be reasonable workflows that benefit from having one.

Not sure if it's related to this specific conversation, but I realized that @{push} is not being resolved here, so I plan on adding a small fix in a v13:

```
--- a/remote.c
+++ b/remote.c
@@ -2018,6 +2018,8 @@ static char *tracking_for_push_dest(struct
repository *repo,

        remote = repo_remote_for_push_tracking(repo, remote);
        ret = apply_refspecs(&remote->fetch, refname);
+       if (!ret)
+               ret = apply_refspecs(&remote->refmap, refname);
        if (!ret)
                return error_buf(err,
                                 _("push destination '%s' on remote
'%s' has no local tracking branch"),
```

Harald

#93 lore raw

Notes, refs, etc. (was: Re: [PATCH v10 1/5] fetch: add remote.<name>.refmap)

In reply to Junio C Hamano

+Cc Kristoffer Haugsbakk as a fellow notes user ;)

Quoted 6 lines · Junio C Hamano
Quoted 2 lines · Ben Knoble

Yes, I would love to add pushRefmap atop later! It would ease my working with notes, I think.

Quoted 8 lines
Quoted 2 lines

Yes, I would love to add pushRefmap atop later! It would ease my working with notes, I think.

Tell us more when you are off mobile ;-).

I do not think I planned to add '--refmap' support on the push side, even from the command line, but I can imagine there would be reasonable workflows that benefit from having one.

In my local copy of git.git, I want to have both Junio's amlog for spelunking and my own notes for patches. I only want to push out my notes + topics to my remote, though (I work on Git from different machines and it's nice to have the notes sync'd on both). Here's the hack I ended up with, and it has some downsides.

In particular, "git push" in this configuration does too much, so I usually have to think about whether that's ok. A pushRefmap would let me separate what to push by default (push.default!) from how to map pushing when I want to push, say, notes or other things. I have to be especially careful with a "git pf" (push, force-with-lease, force-if-includes), since I usually do *not* want to forcibly overwrite the remote notes---instead I should pull those down and merge them†.

So, here's the relevant config (I have a remote "benknoble" and a remote "origin", among others):

[core]
notesRef = refs/notes/benknoble/commits
[notes]
rewriteRef = refs/notes/benknoble/commits displayRef = refs/notes/origin/amlog
[remote "origin"]
fetch = +refs/notes/amlog:refs/notes/origin/amlog

The "usual" stuff to make my notes important, but still show amlog notes when browsing history. (The origin remote section is in addition to the normal remote section with it's url and heads->origin/ mapping.)

[remote]
pushDefault = benknoble

Push to my remote.

[remote "benknoble"]
fetch = refs/notes/*:refs/notes/benknoble/* push = refs/heads/*:refs/heads/* push = refs/notes/benknoble/*:refs/notes/* push = ^refs/notes/origin/amlog

In addition to the usual config added by "git remote add", fetch remote notes into a local hierarchy, and push them out, but ignore amlog, and don't forget to also be able to push heads. Without the heads map, "git push" will not push branches!

Now, maybe I'm holding something in a way it wasn't intended and that's made my life harder, I dunno.

†: There isn't a great story for that, either :/ When "git fetch" fails to fetch notes because of divergence with local, I end up saying

git fetch benknoble refs/notes/commits
git range-diff refs/notes/benknoble/commits...FETCH_HEAD git notes merge FETCH_HEAD

or something.

It's *almost* like we need remote-tracking branches for notes, ahaha… at that point, Scott's git-meta work becomes possibly more appealing to me, even if a part of me wants to "fix" notes so everyone can use them easily.

Signature · 1 line
D. Ben Knoble
#94 lore raw

PATCH v10 2/5 fetch: extract collect_upstream_from_remote() helper

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

When a fetch has no refspec to work from, we fall back to guessing what to fetch: the remote's configured fetch refspec if there is one, and separately, whether the current branch's upstream lives on this remote. The latter is independent of the former on purpose: a narrow refspec (for example from "git remote add -t") can leave the current branch's own upstream uncovered, and we still need to be able to fetch it.

That "does this branch build on this remote" check was inlined for just the current branch. Move it into its own function, collect_upstream_from_remote(), so the same logic can be reused for branches other than just the current one.

---
 builtin/fetch.c | 23 ++++++++++++++---------
 remote.c        | 15 +++++++++++++++
 remote.h        |  9 +++++++++
 3 files changed, 38 insertions(+), 9 deletions(-)
builtin/fetch.c +14 -9
index ffcb5ba29a..136a59154a 100644
@@ -1979,7 +1979,8 @@ static int do_fetch(struct transport *transport,
 	if (rs->nr) {
 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
 	} else {
-		struct branch *branch = branch_get(NULL);
+		struct string_list tracked = STRING_LIST_INIT_DUP;
+		struct string_list_item *item;
 
 		if (transport->remote->fetch.nr) {
 			refspec_ref_prefixes(&transport->remote->fetch,
@@ -1995,14 +1996,18 @@ static int do_fetch(struct transport *transport,
 			else
 				follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
 		}
-		if (branch && branch_has_merge_config(branch) &&
-		    !strcmp(branch->remote_name, transport->remote->name)) {
-			int i;
-			for (i = 0; i < branch->merge_nr; i++) {
-				strvec_push(&transport_ls_refs_options.ref_prefixes,
-					    branch->merge[i]->src);
-			}
-		}
+
+		/*
+		 * The configured refspec may not cover the current
+		 * branch's upstream (e.g. a narrowed -t refspec), so
+		 * make sure we can still fetch it regardless.
+		 */
+		collect_upstream_from_remote(the_repository, &tracked,
+					      transport->remote, NULL);
+		for_each_string_list_item(item, &tracked)
+			strvec_push(&transport_ls_refs_options.ref_prefixes,
+				    item->string);
+		string_list_clear(&tracked, 0);
 
 		/*
 		 * If there are no refs specified to fetch, then we just
remote.c +15 -0
index c9252a90f4..965e42ff52 100644
@@ -1891,6 +1891,21 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+void collect_upstream_from_remote(struct repository *repo,
+				   struct string_list *tracked,
+				   struct remote *remote,
+				   const char *refname)
+{
+	struct branch *branch = repo_branch_get(repo, refname);
+
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, remote->name))
+		return;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		string_list_insert(tracked, branch->merge[i]->src);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +9 -0
index ac485a584d..7c86c529b6 100644
@@ -359,6 +359,15 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/*
+ * If refname's branch builds on remote, add its upstream on remote to
+ * tracked. A NULL refname means the current branch.
+ */
+void collect_upstream_from_remote(struct repository *repo,
+				   struct string_list *tracked,
+				   struct remote *remote,
+				   const char *refname);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
Signature · 1 line
gitgitgadget
#95 lore raw

Re: [PATCH v10 2/5] fetch: extract collect_upstream_from_remote() helper

Quoted 7 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

When a fetch has no refspec to work from, we fall back to guessing what to fetch: the remote's configured fetch refspec if there is one, and separately, whether the current branch's upstream lives on this remote.

It took me a couple tries to parse this because the 2 halves ("[one], and separately, [two]") are 2 different kinds of objects! I think what you are trying to tell me is

…we fall back to guessing what to fetch: the remote's …
separately, the current branch's upstream if it lives on the fetched remote.

That way [two] is also a "thing to fetch" in the same class of objects as [one]? Or have I misunderstood?

Quoted 4 lines

The latter is independent of the former on purpose: a narrow refspec (for example from "git remote add -t") can leave the current branch's own upstream uncovered, and we still need to be able to fetch it.

I don't really understand the meaning here. I can guess that you mean something like "A remote with a narrow refspec, like what you get from 'git remote add -t', can omit a branch's upstream from the remote's fetch refspec, and we still want to fetch that upstream." But it's taken me a while to put that together (after puzzling out the above) after first not knowing what "uncovered" meant.

I think the whole thing would be clearer with an example:

"""
Imagine a remote with a narrow refspec:

git remote add -t main colleague <remote>

and a branch whose upstream points at that remote, but isn't part of the remote's refspec:

git fetch colleague work:refs/remotes/colleague/work git switch -c work colleague/work

When we "git fetch colleague" without a refspec, we decide what to fetch by combining the remote's refspec (main -> remote-tracking branch, in this case) and the current branch's upstream, if it was based on that remote (work -> remote-tracking branch, in this case). """

Note: we aren't "guessing" what to fetch!

Quoted 4 lines

That "does this branch build on this remote" check was inlined for just the current branch. Move it into its own function, collect_upstream_from_remote(), so the same logic can be reused for branches other than just the current one.

And then this maybe comes next. Personally I appreciate the extra explanation, but if it exists elsewhere we could also refer to the implementing commit?

I've also made some assumptions about how that "current branch" check works, since I'm not sure how to make sense of it without this manually created remote-tracking branch?

Quoted 53 lines
---
 builtin/fetch.c | 23 ++++++++++++++---------
 remote.c        | 15 +++++++++++++++
 remote.h        |  9 +++++++++
 3 files changed, 38 insertions(+), 9 deletions(-)
builtin/fetch.c +14 -9
index ffcb5ba29a..136a59154a 100644
@@ -1979,7 +1979,8 @@ static int do_fetch(struct transport *transport,
        if (rs->nr) {
                refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
        } else {
-               struct branch *branch = branch_get(NULL);
+               struct string_list tracked = STRING_LIST_INIT_DUP;
+               struct string_list_item *item;
 
                if (transport->remote->fetch.nr) {
                        refspec_ref_prefixes(&transport->remote->fetch,
@@ -1995,14 +1996,18 @@ static int do_fetch(struct transport *transport,
                        else
                                follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
                }
-               if (branch && branch_has_merge_config(branch) &&
-                   !strcmp(branch->remote_name, transport->remote->name)) {
-                       int i;
-                       for (i = 0; i < branch->merge_nr; i++) {
-                               strvec_push(&transport_ls_refs_options.ref_prefixes,
-                                           branch->merge[i]->src);
-                       }
-               }
+
+               /*
+                * The configured refspec may not cover the current
+                * branch's upstream (e.g. a narrowed -t refspec), so
+                * make sure we can still fetch it regardless.
+                */
+               collect_upstream_from_remote(the_repository, &tracked,
+                                             transport->remote, NULL);
+               for_each_string_list_item(item, &tracked)
+                       strvec_push(&transport_ls_refs_options.ref_prefixes,
+                                   item->string);
+               string_list_clear(&tracked, 0);
 
                /*
                 * If there are no refs specified to fetch, then we just
remote.c +0 -0
index c9252a90f4..965e42ff52 100644

remote.c int branch_merge_matches(struct branch *branch, · line 1897

Quoted 7 lines
remote.c +4 -0
@@ -1891,6 +1891,21 @@ int branch_merge_matches(struct branch *branch,
        return refname_match(branch->merge[i]->src, refname);
 }
 
+void collect_upstream_from_remote(struct repository *repo,
+                                  struct string_list *tracked,
+                                  struct remote *remote,
+                                  const char *refname)

I'm not sure if we have guidance about where in the parameter list "out" parameters should go, but I would expect them to be last? Idk.

Should the "inputs" be const to better communicate that we aren't going to modify them? I'm not really sure of the const semantics of C anyway, and I know there have been topics in the recent past around const-modifier lints and so on.

remote.c int branch_merge_matches(struct branch *branch, · line 1907

Quoted 10 lines
remote.c +10 -0
+{
+       struct branch *branch = repo_branch_get(repo, refname);
+
+       if (!branch_has_merge_config(branch) ||
+           strcmp(branch->remote_name, remote->name))
+               return;
+
+       for (int i = 0; i < branch->merge_nr; i++)
+               string_list_insert(tracked, branch->merge[i]->src);
+}

We do trade some performance now, right? We first collect a string_list, then push them into a strvec, which has some overhead relative to skipping the intermediate step. Plus, string_lists sort, which the original didn't do. And the caller uses the DUP version of string_list (and IIUC strvec_push strdup's as well), so we end up copying twice?

If we used a strvec instead, we could use strvec_pushv, perhaps, being a bit more efficient? (It would be nice if we had an ownership-taking strvec_appendv or something?)

But stepping back, if the caller just provides a strvec instead of a string_list, we can push to it directly, since that's what happens anyway! We don't do any filtering or anything in the new caller.

Quoted 26 lines
remote.c +0 -0
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +9 -1
index ac485a584d..7c86c529b6 100644
@@ -359,6 +359,15 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/*
+ * If refname's branch builds on remote, add its upstream on remote to
+ * tracked. A NULL refname means the current branch.
+ */
+void collect_upstream_from_remote(struct repository *repo,
+                                  struct string_list *tracked,
+                                  struct remote *remote,
+                                  const char *refname);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
        const char *name;
--

gitgitgadget

Signature · 1 line
D. Ben Knoble
#96 lore raw

Re: [PATCH v10 2/5] fetch: extract collect_upstream_from_remote() helper

Quoted 9 lines
Quoted 4 lines

When a fetch has no refspec to work from, we fall back to guessing what to fetch: the remote's configured fetch refspec if there is one, and separately, whether the current branch's upstream lives on this remote.

It took me a couple tries to parse this because the 2 halves ("[one], and separately, [two]") are 2 different kinds of objects! I think what you are trying to tell me is

Quoted 6 lines

…we fall back to guessing what to fetch: the remote's …
separately, the current branch's upstream if it lives on the fetched remote.

That way [two] is also a "thing to fetch" in the same class of objects as [one]? Or have I misunderstood?

I'll take a look!

Quoted 25 lines
Quoted 4 lines

The latter is independent of the former on purpose: a narrow refspec (for example from "git remote add -t") can leave the current branch's own upstream uncovered, and we still need to be able to fetch it.

I don't really understand the meaning here. I can guess that you mean something like "A remote with a narrow refspec, like what you get from 'git remote add -t', can omit a branch's upstream from the remote's fetch refspec, and we still want to fetch that upstream." But it's taken me a while to put that together (after puzzling out the above) after first not knowing what "uncovered" meant.

I think the whole thing would be clearer with an example:

"""
Imagine a remote with a narrow refspec:

git remote add -t main colleague <remote>

and a branch whose upstream points at that remote, but isn't part of the remote's refspec:

git fetch colleague work:refs/remotes/colleague/work git switch -c work colleague/work

Quoted 7 lines

When we "git fetch colleague" without a refspec, we decide what to fetch by combining the remote's refspec (main -> remote-tracking branch, in this case) and the current branch's upstream, if it was based on that remote (work -> remote-tracking branch, in this case). """

Note: we aren't "guessing" what to fetch!

Thanks! Maybe this should be the example instead?

git clone --single-branch <url>
git switch -c work
git push -u origin work

Quoted 7 lines

I'm not sure if we have guidance about where in the parameter list "out" parameters should go, but I would expect them to be last? Idk.

Should the "inputs" be const to better communicate that we aren't going to modify them? I'm not really sure of the const semantics of C anyway, and I know there have been topics in the recent past around const-modifier lints and so on.

Yes.

Quoted 18 lines

remote.c int branch_merge_matches(struct branch *branch, · line 1907

Quoted 10 lines
remote.c +10 -0
+{
+       struct branch *branch = repo_branch_get(repo, refname);
+
+       if (!branch_has_merge_config(branch) ||
+           strcmp(branch->remote_name, remote->name))
+               return;
+
+       for (int i = 0; i < branch->merge_nr; i++)
+               string_list_insert(tracked, branch->merge[i]->src);
+}

We do trade some performance now, right? We first collect a string_list, then push them into a strvec, which has some overhead relative to skipping the intermediate step. Plus, string_lists sort, which the original didn't do. And the caller uses the DUP version of string_list (and IIUC strvec_push strdup's as well), so we end up copying twice?

Quoted 7 lines

If we used a strvec instead, we could use strvec_pushv, perhaps, being a bit more efficient? (It would be nice if we had an ownership-taking strvec_appendv or something?)

But stepping back, if the caller just provides a strvec instead of a string_list, we can push to it directly, since that's what happens anyway! We don't do any filtering or anything in the new caller.

Yes, I'll handle this.

Harald

#97 lore raw

Re: [PATCH v10 2/5] fetch: extract collect_upstream_from_remote() helper

Quoted 32 lines · Harald Nordgren
Quoted 32 lines
Quoted 5 lines

The latter is independent of the former on purpose: a narrow refspec (for example from "git remote add -t") can leave the current branch's own upstream uncovered, and we still need to be able to fetch it.

I don't really understand the meaning here. I can guess that you mean something like "A remote with a narrow refspec, like what you get from 'git remote add -t', can omit a branch's upstream from the remote's fetch refspec, and we still want to fetch that upstream." But it's taken me a while to put that together (after puzzling out the above) after first not knowing what "uncovered" meant.

I think the whole thing would be clearer with an example:

"""
Imagine a remote with a narrow refspec:

git remote add -t main colleague <remote>

and a branch whose upstream points at that remote, but isn't part of the remote's refspec:

git fetch colleague work:refs/remotes/colleague/work git switch -c work colleague/work

When we "git fetch colleague" without a refspec, we decide what to fetch by combining the remote's refspec (main -> remote-tracking branch, in this case) and the current branch's upstream, if it was based on that remote (work -> remote-tracking branch, in this case). """

Quoted 8 lines
Quoted 1 line

Note: we aren't "guessing" what to fetch!

Thanks! Maybe this should be the example instead?

git clone --single-branch <url>
git switch -c work
git push -u origin work

I'm not sure if that captures the same "narrow refspec" remote or not, to be honest, though it might be simpler? I'm not sure what the push has to do with it, though, and I don't know what you think about the rest of the proposed wording ;)

[later]

Reading the new version, I think I see what you're after. I personally would love to see an example adding a remote, since I tend to want "big" fetches from my primary remotes, but that's fine.

#98 lore raw

PATCH v10 3/5 fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |   6 +-
 builtin/fetch.c                  |  75 +++++++++++++++----
 remote.c                         |  24 +++++++
 remote.h                         |   4 ++
 t/meson.build                    |   1 +
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 219 insertions(+), 15 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..7c52e443a5 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
 	Used to map remote refs being fetched to remote-tracking refs to
-	store. See the `--refmap` entry in linkgit:git-fetch[1].
+	store. If `remote.<name>.fetch` is not set either, a refspec-less
+	fetch infers what to fetch from local branches built on this
+	remote, instead of fetching every branch it has. See the
+	`--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +4 -2
index c2101a7b39..90b9e13ac2 100644
@@ -245,8 +245,10 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+nothing to fetch is specified on the command line, nor is there a
+`remote.<name>.fetch`, branches from the remote that are used as the
+`@{upstream}` of our local branches are fetched.
 
 `-t`::
 `--tags`::
builtin/fetch.c +63 -12

124 lines not on this page · raw message

remote.c +24 -0
index 965e42ff52..6467496a0f 100644
@@ -1906,6 +1906,30 @@ void collect_upstream_from_remote(struct repository *repo,
 		string_list_insert(tracked, branch->merge[i]->src);
 }
 
+struct branches_tracking_remote_cb_data {
+	struct repository *repo;
+	struct remote *remote;
+	struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+
+	collect_upstream_from_remote(data->repo, data->tracked, data->remote,
+				      ref->name);
+	return 0;
+}
+
+void branches_tracking_remote(struct repository *repo, struct remote *remote,
+			       struct string_list *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { repo, remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(repo),
+				  add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +4 -0
index 7c86c529b6..62265346d3 100644
@@ -368,6 +368,10 @@ void collect_upstream_from_remote(struct repository *repo,
 				   struct remote *remote,
 				   const char *refname);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct repository *repo, struct remote *remote,
+			       struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index 11b3752f00..0953a4112f 100644
@@ -736,6 +736,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5586-fetch-refmap.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5586-fetch-refmap.sh new file +119 -0

119 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#99 lore raw

Re: [PATCH v10 3/5] fetch: infer branches to fetch from a refmap-only remote

Quoted 7 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

This makes this commit sound like a fixup of [1/5], but I think it's actually one of the main goals of the series! Can we flip the justification around to say something like "we want to allow refmaps to do XYZ […] Incidentally, we also make sense of the case when refmap is configured without fetch" ? That would make the purpose clearer to me, at least.

Quoted 39 lines

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |   6 +-
 builtin/fetch.c                  |  75 +++++++++++++++----
 remote.c                         |  24 +++++++
 remote.h                         |   4 ++
 t/meson.build                    |   1 +
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 219 insertions(+), 15 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..7c52e443a5 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
        The default value of the `--refmap` option for linkgit:git-fetch[1].
        Used to map remote refs being fetched to remote-tracking refs to
-       store. See the `--refmap` entry in linkgit:git-fetch[1].
+       store. If `remote.<name>.fetch` is not set either, a refspec-less
+       fetch infers what to fetch from local branches built on this
+       remote, instead of fetching every branch it has. See the
+       `--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
        The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +0 -0
index c2101a7b39..90b9e13ac2 100644

Documentation/fetch-options.adoc endif::git-pull[] · line 250

Quoted 8 lines
Documentation/fetch-options.adoc +3 -2
@@ -245,8 +245,10 @@ endif::git-pull[]
        command-line arguments. See section on "Configured Remote-tracking
        Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+nothing to fetch is specified on the command line, nor is there a
+`remote.<name>.fetch`, branches from the remote that are used as the

"remote-tracking branches" ??

Documentation/fetch-options.adoc endif::git-pull[] · line 251

Quoted 1 line
Documentation/fetch-options.adoc +1 -0
+`@{upstream}` of our local branches are fetched.

The clause here is complicated ':D I found "infer what to fetch from local branches built on this remote" to be a bit clearer, but we definitely still need to get to the point, which is that remote-tracking branches from this remote that are also configured as upstreams of local branches are fetched.

Now, I wonder a bit: the doc says "when a refmap is active" we do that "fetch remote-tracking branches that are upstream of local branches" [all scoped to the current remote]---but it doesn't really say how the refmap affects that? I mean, I imagine it's used for mapping (duh), but I don't know if it also narrows the scope, or if the scope is deliberately all remote-tracking branches that meet the requirements, or…?

IOW, it seems to beg the question "why does the presence of a refmap control this if it has no effect on the set of selected branches," and I think the answer is partly "because we designed it that way" and possibly found in the mails leading up to this point, but we might want to try to connect the dots for folks? Idk.

Quoted 25 lines
Documentation/fetch-options.adoc +0 -0
 `-t`::
 `--tags`::
builtin/fetch.c +4 -0
index 136a59154a..0fc1fb66d5 100644
@@ -524,6 +524,8 @@ static struct ref *get_ref_map(struct remote *remote,
         */
        struct refspec *effective_refmap =
                refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
+       struct refspec inferred_rs;
+       int infer_from_refmap = 0;
 
        /* opportunistically-updated references: */
        struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -531,15 +533,32 @@ static struct ref *get_ref_map(struct remote *remote,
        struct hashmap existing_refs;
        int existing_refs_populated = 0;
 
+       refspec_init_fetch(&inferred_rs, the_hash_algo);
+
        filter_prefetch_refspec(rs);
        if (remote)
                filter_prefetch_refspec(&remote->fetch);

builtin/fetch.c static struct ref *get_ref_map(struct remote *remote, · line 550

Quoted 10 lines
builtin/fetch.c +9 -0
 
+       if (!rs->nr && remote && !remote->fetch.nr &&
+           effective_refmap && effective_refmap->nr) {
+               struct string_list tracked = STRING_LIST_INIT_DUP;
+               struct string_list_item *item;
+
+               branches_tracking_remote(the_repository, remote, &tracked);
+               for_each_string_list_item(item, &tracked)
+                       refspec_append(&inferred_rs, item->string);
+               string_list_clear(&tracked, 0);

Interesting choice of string_list again ;) do we need the sorting behavior? (In fact, since it could come from an "ordered" config, perhaps we should use that order?) A strvec seems more appropriate then…

Quoted 35 lines
builtin/fetch.c +10 -1
+
+               rs = &inferred_rs;
+               infer_from_refmap = 1;
+       }
+
        if (rs->nr) {
                struct refspec *fetch_refspec;
 
                for (i = 0; i < rs->nr; i++) {
-                       get_fetch_map(remote_refs, &rs->items[i], &tail, 0);
+                       get_fetch_map(remote_refs, &rs->items[i], &tail,
+                                     infer_from_refmap);
                        if (rs->items[i].dst && rs->items[i].dst[0])
                                *autotags = 1;
                }
@@ -574,6 +593,8 @@ static struct ref *get_ref_map(struct remote *remote,
 
                for (i = 0; i < fetch_refspec->nr; i++)
                        get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
+       } else if (infer_from_refmap) {
+               /* Already fully handled above. */
        } else if (refmap.nr) {
                die("--refmap option is only meaningful with command-line refspec(s)");
        } else {
@@ -669,6 +690,7 @@ static struct ref *get_ref_map(struct remote *remote,
        if (existing_refs_populated)
                hashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent);
 
+       refspec_clear(&inferred_rs);
        return ref_map;
 }
 
@@ -1978,13 +2000,47 @@ static int do_fetch(struct transport *transport,
 
        if (rs->nr) {

builtin/fetch.c static int do_fetch(struct transport *transport, · line 2011

Quoted 10 lines
builtin/fetch.c +9 -0
                refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
+       } else if (transport->remote->fetch.nr) {
+               struct string_list tracked = STRING_LIST_INIT_DUP;
+               struct string_list_item *item;
+
+               refspec_ref_prefixes(&transport->remote->fetch,
+                                    &transport_ls_refs_options.ref_prefixes);
+               /*
+                * See remote.c's handling of remote.<name>.followRemoteHEAD
+                * for the analogous, still-unresolved case.

Is this a "we haven't resolved what to do" case, or a "some remote (or something) hasn't resolved" case? I'm not sure what I should be looking at here.

Oh, I see. It's a duplicate of comment and code from the else{effective_refmap->nr} block below. Hrm… probably it would be nice not to duplicate that? I can't spot any differences between the code blocks.

Quoted 97 lines
builtin/fetch.c +31 -11
+                */
+               if (transport->remote->follow_remote_head)
+                       follow_remote_head = transport->remote->follow_remote_head;
+               else if (config->follow_remote_head_seen)
+                       follow_remote_head = get_follow_remote_head(config->follow_remote_head_raw);
+               else
+                       follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
+
+               /*
+                * The configured refspec may not cover the current
+                * branch's upstream (e.g. a narrowed -t refspec), so
+                * make sure we can still fetch it regardless.
+                */
+               collect_upstream_from_remote(the_repository, &tracked,
+                                             transport->remote, NULL);
+               for_each_string_list_item(item, &tracked)
+                       strvec_push(&transport_ls_refs_options.ref_prefixes,
+                                   item->string);
+               string_list_clear(&tracked, 0);
        } else {
+               /*
+                * The --refmap command line option, if given, takes
+                * precedence over remote.<name>.refmap.
+                */
+               struct refspec *effective_refmap = refmap.nr ? &refmap :
+                       &transport->remote->refmap;
                struct string_list tracked = STRING_LIST_INIT_DUP;
                struct string_list_item *item;
 
-               if (transport->remote->fetch.nr) {
-                       refspec_ref_prefixes(&transport->remote->fetch,
-                                            &transport_ls_refs_options.ref_prefixes);
+               if (effective_refmap->nr) {
+                       branches_tracking_remote(the_repository,
+                                                 transport->remote, &tracked);
                        /*
                         * See remote.c's handling of remote.<name>.followRemoteHEAD
                         * for the analogous, still-unresolved case.
@@ -1995,15 +2051,10 @@ static int do_fetch(struct transport *transport,
                                follow_remote_head = get_follow_remote_head(config->follow_remote_head_raw);
                        else
                                follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
+               } else {
+                       collect_upstream_from_remote(the_repository, &tracked,
+                                                     transport->remote, NULL);
                }
-
-               /*
-                * The configured refspec may not cover the current
-                * branch's upstream (e.g. a narrowed -t refspec), so
-                * make sure we can still fetch it regardless.
-                */
-               collect_upstream_from_remote(the_repository, &tracked,
-                                             transport->remote, NULL);
                for_each_string_list_item(item, &tracked)
                        strvec_push(&transport_ls_refs_options.ref_prefixes,
                                    item->string);
remote.c +24 -0
index 965e42ff52..6467496a0f 100644
@@ -1906,6 +1906,30 @@ void collect_upstream_from_remote(struct repository *repo,
                string_list_insert(tracked, branch->merge[i]->src);
 }
 
+struct branches_tracking_remote_cb_data {
+       struct repository *repo;
+       struct remote *remote;
+       struct string_list *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+       struct branches_tracking_remote_cb_data *data = cb_data;
+
+       collect_upstream_from_remote(data->repo, data->tracked, data->remote,
+                                     ref->name);
+       return 0;
+}
+
+void branches_tracking_remote(struct repository *repo, struct remote *remote,
+                              struct string_list *tracked)
+{
+       struct branches_tracking_remote_cb_data data = { repo, remote, tracked };
+
+       refs_for_each_branch_ref(get_main_ref_store(repo),
+                                 add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +0 -0
index 7c86c529b6..62265346d3 100644

remote.h void collect_upstream_from_remote(struct repository *repo, · line 377

Quoted 10 lines
remote.h +4 -0
@@ -368,6 +368,10 @@ void collect_upstream_from_remote(struct repository *repo,
                                   struct remote *remote,
                                   const char *refname);
 
+/* fills tracked with the refname of every local branch's upstream on remote */
+void branches_tracking_remote(struct repository *repo, struct remote *remote,
+                              struct string_list *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
        const char *name;

I stopped reading here, but it made a bit of sense up to this point. I wonder if the remote.h comment on the new function could be more clear, but :shrug:

Signature · 1 line
D. Ben Knoble
#100 lore raw

Re: [PATCH v10 3/5] fetch: infer branches to fetch from a refmap-only remote

Quoted 11 lines
Quoted 4 lines

Configuring remote.<name>.refmap without remote.<name>.fetch used to make a refspec-less "git fetch <name>" fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only says where to put fetched refs, not what to fetch.

This makes this commit sound like a fixup of [1/5], but I think it's actually one of the main goals of the series! Can we flip the justification around to say something like "we want to allow refmaps to do XYZ […] Incidentally, we also make sense of the case when refmap is configured without fetch" ? That would make the purpose clearer to me, at least.

Good idea.

Quoted 41 lines
Quoted 41 lines

Make that case infer what to fetch: the local branches whose @{upstream} is already on that remote. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote.<name>.fetch, and without needing to touch the command line every time.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |   6 +-
 builtin/fetch.c                  |  75 +++++++++++++++----
 remote.c                         |  24 +++++++
 remote.h                         |   4 ++
 t/meson.build                    |   1 +
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 219 insertions(+), 15 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..7c52e443a5 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
        The default value of the `--refmap` option for linkgit:git-fetch[1].
        Used to map remote refs being fetched to remote-tracking refs to
-       store. See the `--refmap` entry in linkgit:git-fetch[1].
+       store. If `remote.<name>.fetch` is not set either, a refspec-less
+       fetch infers what to fetch from local branches built on this
+       remote, instead of fetching every branch it has. See the
+       `--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
        The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +0 -0
index c2101a7b39..90b9e13ac2 100644
@@ -245,8 +245,10 @@ endif::git-pull[]
        command-line arguments. See section on "Configured Remote-tracking
        Branches" for details.
Quoted 8 lines

Documentation/fetch-options.adoc endif::git-pull[] · line 250

Quoted 6 lines
Documentation/fetch-options.adoc +3 -2
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+nothing to fetch is specified on the command line, nor is there a
+`remote.<name>.fetch`, branches from the remote that are used as the

"remote-tracking branches" ??

Yes, the next version will say "remote-tracking branches from this remote that are also configured as the @{upstream} of a local branch are fetched".

Quoted 16 lines

Documentation/fetch-options.adoc endif::git-pull[] · line 251

Quoted 1 line
Documentation/fetch-options.adoc +1 -0
+`@{upstream}` of our local branches are fetched.

The clause here is complicated ':D I found "infer what to fetch from local branches built on this remote" to be a bit clearer, but we definitely still need to get to the point, which is that remote-tracking branches from this remote that are also configured as upstreams of local branches are fetched.

Now, I wonder a bit: the doc says "when a refmap is active" we do that "fetch remote-tracking branches that are upstream of local branches" [all scoped to the current remote]---but it doesn't really say how the refmap affects that? I mean, I imagine it's used for mapping (duh), but I don't know if it also narrows the scope, or if the scope is deliberately all remote-tracking branches that meet the requirements, or…?

Quoted 5 lines

IOW, it seems to beg the question "why does the presence of a refmap control this if it has no effect on the set of selected branches," and I think the answer is partly "because we designed it that way" and possibly found in the mails leading up to this point, but we might want to try to connect the dots for folks? Idk.

I'll take a look at improving this part.

Quoted 26 lines
Quoted 26 lines
Documentation/fetch-options.adoc +0 -0
 `-t`::
 `--tags`::
builtin/fetch.c +4 -0
index 136a59154a..0fc1fb66d5 100644
@@ -524,6 +524,8 @@ static struct ref *get_ref_map(struct remote *remote,
         */
        struct refspec *effective_refmap =
                refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
+       struct refspec inferred_rs;
+       int infer_from_refmap = 0;
 
        /* opportunistically-updated references: */
        struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -531,15 +533,32 @@ static struct ref *get_ref_map(struct remote *remote,
        struct hashmap existing_refs;
        int existing_refs_populated = 0;
 
+       refspec_init_fetch(&inferred_rs, the_hash_algo);
+
        filter_prefetch_refspec(rs);
        if (remote)
                filter_prefetch_refspec(&remote->fetch);
 
Quoted 14 lines

builtin/fetch.c static struct ref *get_ref_map(struct remote *remote, · line 550

Quoted 9 lines
builtin/fetch.c +9 -0
+       if (!rs->nr && remote && !remote->fetch.nr &&
+           effective_refmap && effective_refmap->nr) {
+               struct string_list tracked = STRING_LIST_INIT_DUP;
+               struct string_list_item *item;
+
+               branches_tracking_remote(the_repository, remote, &tracked);
+               for_each_string_list_item(item, &tracked)
+                       refspec_append(&inferred_rs, item->string);
+               string_list_clear(&tracked, 0);

Interesting choice of string_list again ;) do we need the sorting behavior? (In fact, since it could come from an "ordered" config, perhaps we should use that order?) A strvec seems more appropriate then…

Agreed, the next version uses a strvec here too. Nothing is sorted
anymore: the branches come in refname order, and each branch's merge entries in config order.

Quoted 50 lines
Quoted 35 lines
builtin/fetch.c +10 -1
+
+               rs = &inferred_rs;
+               infer_from_refmap = 1;
+       }
+
        if (rs->nr) {
                struct refspec *fetch_refspec;
 
                for (i = 0; i < rs->nr; i++) {
-                       get_fetch_map(remote_refs, &rs->items[i], &tail, 0);
+                       get_fetch_map(remote_refs, &rs->items[i], &tail,
+                                     infer_from_refmap);
                        if (rs->items[i].dst && rs->items[i].dst[0])
                                *autotags = 1;
                }
@@ -574,6 +593,8 @@ static struct ref *get_ref_map(struct remote *remote,
 
                for (i = 0; i < fetch_refspec->nr; i++)
                        get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
+       } else if (infer_from_refmap) {
+               /* Already fully handled above. */
        } else if (refmap.nr) {
                die("--refmap option is only meaningful with command-line refspec(s)");
        } else {
@@ -669,6 +690,7 @@ static struct ref *get_ref_map(struct remote *remote,
        if (existing_refs_populated)
                hashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent);
 
+       refspec_clear(&inferred_rs);
        return ref_map;
 }
 
@@ -1978,13 +2000,47 @@ static int do_fetch(struct transport *transport,
 
        if (rs->nr) {

builtin/fetch.c static int do_fetch(struct transport *transport, · line 2011

Quoted 10 lines
builtin/fetch.c +9 -0
                refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
+       } else if (transport->remote->fetch.nr) {
+               struct string_list tracked = STRING_LIST_INIT_DUP;
+               struct string_list_item *item;
+
+               refspec_ref_prefixes(&transport->remote->fetch,
+                                    &transport_ls_refs_options.ref_prefixes);
+               /*
+                * See remote.c's handling of remote.<name>.followRemoteHEAD
+                * for the analogous, still-unresolved case.

Is this a "we haven't resolved what to do" case, or a "some remote (or something) hasn't resolved" case? I'm not sure what I should be looking at here.

Quoted 4 lines

Oh, I see. It's a duplicate of comment and code from the else{effective_refmap->nr} block below. Hrm… probably it would be nice not to duplicate that? I can't spot any differences between the code blocks.

Yes, good idea!

Harald

#101 lore raw

PATCH v10 4/5 remote: add "git remote add --limited-fetch"

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)
Documentation/git-remote.adoc +7 -1
index 315100409d..f8764394e7 100644
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [synopsis]
 git remote [-v | --verbose]
-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>
 git remote rename [--[no-]progress] <old> <new>
 git remote remove <name>
 git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
@@ -75,6 +75,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_
 is created.  You can give more than one `-t <branch>` to track
 multiple branches without grabbing all branches.
 +
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
+details.
++
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
 +
builtin/remote.c +22 -6
index fb7e5b114f..a7c13946d1 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
+	int limited_fetch = -1; /* unspecified */
 	struct string_list track = STRING_LIST_INIT_NODUP;
 	const char *master = NULL;
 	struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
 		OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
 			N_("set up remote as a mirror to push to or fetch from"),
 			PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+		OPT_BOOL(0, "limited-fetch", &limited_fetch,
+			N_("fetch only the branches we build on, instead of every branch")),
 		OPT_END()
 	};
 
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
 		die(_("specifying a master branch makes no sense with --mirror"));
 	if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
 		die(_("specifying branches to track makes sense only with fetch mirrors"));
+	if (limited_fetch == 1 && track.nr)
+		die(_("--limited-fetch does not make sense with -t/--track"));
+	if (limited_fetch == 1 && mirror)
+		die(_("--limited-fetch does not make sense with --mirror"));
 
 	name = argv[0];
 	url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
 	repo_config_set(the_repository, buf.buf, url);
 
 	if (!mirror || mirror & MIRROR_FETCH) {
+		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+			limited_fetch == 1;
+
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "remote.%s.fetch", name);
-		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
-			add_branch(buf.buf, track.items[i].string,
-				   name, mirror, &buf2);
+		if (use_limited_fetch) {
+			strbuf_addf(&buf, "remote.%s.refmap", name);
+			strbuf_reset(&buf2);
+			strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+			repo_config_set(the_repository, buf.buf, buf2.buf);
+		} else {
+			strbuf_addf(&buf, "remote.%s.fetch", name);
+			if (track.nr == 0)
+				string_list_append(&track, "*");
+			for (size_t i = 0; i < track.nr; i++)
+				add_branch(buf.buf, track.items[i].string,
+					   name, mirror, &buf2);
 		}
 	}
 
t/t5505-remote.sh +16 -0
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
 	test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+	test_when_finished "rm -rf full-add" &&
+	git clone --no-local one full-add &&
+	(
+		cd full-add &&
+		git remote add --limited-fetch upstream ../two &&
+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+			remote.upstream.refmap &&
+		test_must_fail git config get remote.upstream.fetch
+	)
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+	test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
 	(
 		cd test &&
Signature · 1 line
gitgitgadget
#102 lore raw

Re: [PATCH v10 4/5] remote: add "git remote add --limited-fetch"

Quoted 41 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---

builtin/remote.c +3 -0
index fb7e5b114f..a7c13946d1 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
        int fetch = 0, fetch_tags = TAGS_DEFAULT;
        unsigned mirror = MIRROR_NONE;
+       int limited_fetch = -1; /* unspecified */
        struct string_list track = STRING_LIST_INIT_NODUP;
        const char *master = NULL;
        struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
                OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
                        N_("set up remote as a mirror to push to or fetch from"),
                        PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+               OPT_BOOL(0, "limited-fetch", &limited_fetch,
+                       N_("fetch only the branches we build on, instead of every branch")),
                OPT_END()
        };
 

builtin/remote.c static int add(int argc, const char **argv, const char *prefix, · line 220

Quoted 7 lines
builtin/remote.c +4 -0
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
                die(_("specifying a master branch makes no sense with --mirror"));
        if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
                die(_("specifying branches to track makes sense only with fetch mirrors"));
+       if (limited_fetch == 1 && track.nr)
+               die(_("--limited-fetch does not make sense with -t/--track"));
+       if (limited_fetch == 1 && mirror)
+               die(_("--limited-fetch does not make sense with --mirror"));

We need "== 1" because default to "-1" for unspecified, which is a bit atypical of boolean flags unless we need to know about that case.

Reading ahead, 5/5 wants to know if limited-fetch was unspecified so we can act on it. Ok.

builtin/remote.c static int add(int argc, const char **argv, const char *prefix, · line 242

Quoted 10 lines
builtin/remote.c +3 -0
        name = argv[0];
        url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
        repo_config_set(the_repository, buf.buf, url);
 
        if (!mirror || mirror & MIRROR_FETCH) {
+               int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+                       limited_fetch == 1;
+

:\ I don't have a better suggestion for formatting this line, but it's… odd.

Quoted 50 lines
builtin/remote.c +12 -6
                strbuf_reset(&buf);
-               strbuf_addf(&buf, "remote.%s.fetch", name);
-               if (track.nr == 0)
-                       string_list_append(&track, "*");
-               for (size_t i = 0; i < track.nr; i++) {
-                       add_branch(buf.buf, track.items[i].string,
-                                  name, mirror, &buf2);
+               if (use_limited_fetch) {
+                       strbuf_addf(&buf, "remote.%s.refmap", name);
+                       strbuf_reset(&buf2);
+                       strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+                       repo_config_set(the_repository, buf.buf, buf2.buf);
+               } else {
+                       strbuf_addf(&buf, "remote.%s.fetch", name);
+                       if (track.nr == 0)
+                               string_list_append(&track, "*");
+                       for (size_t i = 0; i < track.nr; i++)
+                               add_branch(buf.buf, track.items[i].string,
+                                          name, mirror, &buf2);
                }
        }
 
t/t5505-remote.sh +16 -1
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
        test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+       test_when_finished "rm -rf full-add" &&
+       git clone --no-local one full-add &&
+       (
+               cd full-add &&
+               git remote add --limited-fetch upstream ../two &&
+               test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+                       remote.upstream.refmap &&
+               test_must_fail git config get remote.upstream.fetch
+       )
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+       test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
        (
                cd test &&
--
Quoted 1 line

gitgitgadget

Looks reasonable.

Signature · 1 line
D. Ben Knoble
#103 lore raw

PATCH v10 5/5 remote: default to --limited-fetch in a shallow repository

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index f8764394e7..f43d707c91 100644
@@ -57,6 +57,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -79,7 +84,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
-details.
+details. `--no-limited-fetch` explicitly disables this, overriding the
+shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +4 -1
index a7c13946d1..8c4497cccd 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
 	if (!mirror || mirror & MIRROR_FETCH) {
 		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-			limited_fetch == 1;
+			(limited_fetch == 1 ||
+			 (limited_fetch == -1 &&
+			  is_repository_shallow(the_repository)));
 
 		strbuf_reset(&buf);
 		if (use_limited_fetch) {
t/t5505-remote.sh +60 -0

66 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#104 lore raw

Re: [PATCH v10 5/5] remote: default to --limited-fetch in a shallow repository

Quoted 10 lines · Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

First we focus on "single-branch shallow clones", but…

Quoted 5 lines

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

…now we only care about shallow. I think that's reasonable, since we don't have a way to track "was cloned with a single-branch" (and it's not really the key criterion anyway), but the wording begged the question ;)

Quoted 46 lines
---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index f8764394e7..f43d707c91 100644
@@ -57,6 +57,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -79,7 +84,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
-details.
+details. `--no-limited-fetch` explicitly disables this, overriding the
+shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +1 -0
index a7c13946d1..8c4497cccd 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"

builtin/remote.c static int add(int argc, const char **argv, const char *prefix, · line 247

Quoted 10 lines
builtin/remote.c +3 -1
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
        if (!mirror || mirror & MIRROR_FETCH) {
                int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-                       limited_fetch == 1;
+                       (limited_fetch == 1 ||
+                        (limited_fetch == -1 &&
+                         is_repository_shallow(the_repository)));
 
                strbuf_reset(&buf);
                if (use_limited_fetch) {

I didn't read the tests, but the code looks short and sweet ;)

#105 lore raw

Re: [PATCH v10 5/5] remote: default to --limited-fetch in a shallow repository

Quoted 16 lines
Quoted 7 lines

Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

First we focus on "single-branch shallow clones", but…

Quoted 5 lines

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

Quoted 4 lines

…now we only care about shallow. I think that's reasonable, since we don't have a way to track "was cloned with a single-branch" (and it's not really the key criterion anyway), but the wording begged the question ;)

Good point, I'll improve.

Harald

#106 lore raw

Re: [PATCH v10 0/5] fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget
Quoted 14 lines · Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v10:

* Resolve merge conflict after master integrated
ch/fetch-followremotehead-lazy-validation.

Harald Nordgren (5):
fetch: add remote.<name>.refmap
fetch: extract collect_upstream_from_remote() helper
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

Quoted 13 lines
 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |   5 ++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  92 ++++++++++++++++++++----
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  51 ++++++++++++-
 remote.h                         |  19 +++++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 |  63 ++++++++++++++++
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 456 insertions(+), 23 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

This topic seeems fairly well done, without seeing any additional comments in the last two rounds, but I am highly disturbed that I was practically the only person who reviewd its later iterations. Because I am not a target audience of the feature, I would not be surprised if I am missing something large.

I would very much appreciate if somebody else can give the series one more read and Ack them, so that we can move it forward.

Thanks.

#107 lore raw

Re: [PATCH v10 0/5] fetch: avoid fetching every branch of a new remote in a shallow repo

Quoted 37 lines · Junio C Hamano
Quoted 14 lines · Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v10:

* Resolve merge conflict after master integrated
ch/fetch-followremotehead-lazy-validation.

Harald Nordgren (5):
fetch: add remote.<name>.refmap
fetch: extract collect_upstream_from_remote() helper
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

Quoted 13 lines
 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |   5 ++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  92 ++++++++++++++++++++----
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  51 ++++++++++++-
 remote.h                         |  19 +++++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 |  63 ++++++++++++++++
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 456 insertions(+), 23 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

This topic seeems fairly well done, without seeing any additional comments in the last two rounds, but I am highly disturbed that I was practically the only person who reviewd its later iterations. Because I am not a target audience of the feature, I would not be surprised if I am missing something large.

Quoted 4 lines

I would very much appreciate if somebody else can give the series one more read and Ack them, so that we can move it forward.

Thanks.

I gave it a read; it's possible the things I wrote were already seen by you and not worth commenting on, but I'm not sure.

As for the reviewing… well, it was nice to review when it felt like things had settled somewhat :)

Signature · 1 line
D. Ben Knoble
#108 lore raw

Re: [PATCH v10 0/5] fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Junio C Hamano
Quoted 10 lines · Junio C Hamano

"Harald Nordgren via GitGitGadget" <[email protected]> writes:

This topic seeems fairly well done, without seeing any additional comments in the last two rounds, but I am highly disturbed that I was practically the only person who reviewd its later iterations. Because I am not a target audience of the feature, I would not be surprised if I am missing something large.

I would very much appreciate if somebody else can give the series one more read and Ack them, so that we can move it forward.

I've just read through the commit messages and documentation changes and I think it is a mistake to use a single config variable to configure where fetched refs are stored and which refs are fetched. The problem this series tries to solve is that the default refspec fetches all the branches from the remote and it's not possible to use that mapping and also restrict which branches are fetched. To fix that it introduces a new config setting to configure where the refs are stored which is good, but it then uses the presence of that setting to implicitly configure the default set of refs to fetch. To me that repeats the mistake of the existing fetch config.

It would be much more flexible if we had a separate config setting (maybe remote.<name>.fetchRefs) to describe which refs are fetched. That setting could accept something like "*@{upstream}" to "mean fetch the upstream branches of my local branches that live on this remote" which would cover what this series wants. I think it would be useful to support "+" and "^" prefixes on the refs to fetch as well, so that the refmap config controls the mapping and the "what to fetch" config handles negation and forced updates on a per-ref basis.

Setting

remote.<name>.fetchRefmap refs/heads/*:refs/remotes/<name>/* remote.<name>.fetchRefs +refs/heads/*

would be equivalent to

    remote.<name>.fetch +refs/heads/*:refs/remotes/<name>/*

Ideally when we're seeing which refs to fetch we'd use the last entry that matches so that

remote.<name>.fetchRefs +refs/head/*
remote.<name>.fetchRefs refs/heads/master

would refuse non-fast-forward updates for master. Currently we use the first refspec we find that matches which goes against the usual "last one wins" behavior for config settings and makes it impossible to add per-branch refspecs with "git config" because the default setting comes first in the config file and matches all branches.

Thanks

Phillip

#109 lore raw

Re: [PATCH v10 0/5] fetch: avoid fetching every branch of a new remote in a shallow repo

Quoted 11 lines · Phillip Wood

I've just read through the commit messages and documentation changes and I think it is a mistake to use a single config variable to configure where fetched refs are stored and which refs are fetched. The problem this series tries to solve is that the default refspec fetches all the branches from the remote and it's not possible to use that mapping and also restrict which branches are fetched. To fix that it introduces a new config setting to configure where the refs are stored which is good, but it then uses the presence of that setting to implicitly configure the default set of refs to fetch. To me that repeats the mistake of the existing fetch config.

Quoted 2 lines

It would be much more flexible if we had a separate config setting (maybe remote.<name>.fetchRefs) to describe which refs are fetched.

Good point.

When I wrote the refmap feature (without configuration variable) long time ago, one of the goals was to allow me to say

    $ git fetch cabal \
        --refmap="+refs/heads/*:refs/remotes/cabal/*" \
	maint master next

That is, "here is the place I grab from there are stored" and "maint master next are the things I grab from there" are specified separately. With the configuration variable added in this series,

    $ git config set remote.cabal.refmap "+refs/heads/*:refs/remotes/cabal/*"
    $ git fetch cabal master next seen

should become possible with the same behaviour. I am not sure offhand if this series does that, but I would consider it a bug if it does not.

In any case, this series, since it lacks the separate setting, takes the lack of "what to fetch" on the command line (and perhaps lack of remote.cabal.fetch???) as a sign to trigger this new feature to guess what to fetch based on what is tracked by the local branches.

For now, as this "what is tracked by the local branches" is the only kind of smart that was designed to decide what to fetch, it may be OK to use "you gave me nothing to specify what to fetch, so I'll guess using the smart". But when we introduce more kinds of smart later, this becomes a problem. So I do agree with you that we should have something to specifically say "when I do not explicitly give you what to fetch, decide based on what local branches track".

Quoted 7 lines

That
setting could accept something like "*@{upstream}" to "mean fetch the upstream branches of my local branches that live on this remote" which would cover what this series wants. I think it would be useful to support "+" and "^" prefixes on the refs to fetch as well, so that the refmap config controls the mapping and the "what to fetch" config handles negation and forced updates on a per-ref basis.

The reasoning here is slightly wrong. "^" (not) may be good thing to consider as it is to decide what to fetch, but "+" (overwrite) should be on the refmap side, i.e., where to store. If you are not storing, then "overwrite what I have with non-fast-forward" becomes a nonsense request, and that is why it does not belong to "what to fetch".

#110 lore raw

Re: [PATCH v10 0/5] fetch: avoid fetching every branch of a new remote in a shallow repo

Quoted 11 lines
Quoted 11 lines

I've just read through the commit messages and documentation changes and I think it is a mistake to use a single config variable to configure where fetched refs are stored and which refs are fetched. The problem this series tries to solve is that the default refspec fetches all the branches from the remote and it's not possible to use that mapping and also restrict which branches are fetched. To fix that it introduces a new config setting to configure where the refs are stored which is good, but it then uses the presence of that setting to implicitly configure the default set of refs to fetch. To me that repeats the mistake of the existing fetch config.

Quoted 4 lines
Quoted 2 lines

It would be much more flexible if we had a separate config setting (maybe remote.<name>.fetchRefs) to describe which refs are fetched.

Good point.

Ok.

Quoted 11 lines

When I wrote the refmap feature (without configuration variable) long time ago, one of the goals was to allow me to say

    $ git fetch cabal \
        --refmap="+refs/heads/*:refs/remotes/cabal/*" \
        maint master next

That is, "here is the place I grab from there are stored" and "maint master next are the things I grab from there" are specified separately. With the configuration variable added in this series,

Quoted 6 lines
    $ git config set remote.cabal.refmap "+refs/heads/*:refs/remotes/cabal/*"
    $ git fetch cabal master next seen

should become possible with the same behaviour. I am not sure offhand if this series does that, but I would consider it a bug if it does not.

It will do that in my next version and I added a test specifically for this.

Harald

#111 lore raw

Re: [PATCH v10 0/5] fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Junio C Hamano
Quoted 9 lines · Junio C Hamano

Phillip Wood <[email protected]> writes:

When I wrote the refmap feature (without configuration variable) long time ago, one of the goals was to allow me to say

     $ git fetch cabal \
         --refmap="+refs/heads/*:refs/remotes/cabal/*" \
	maint master next
Quoted 8 lines

That is, "here is the place I grab from there are stored" and "maint master next are the things I grab from there" are specified separately. With the configuration variable added in this series,

     $ git config set remote.cabal.refmap "+refs/heads/*:refs/remotes/cabal/*"
     $ git fetch cabal master next seen

should become possible with the same behaviour.

That makes sense

Quoted 8 lines
Quoted 7 lines

That
setting could accept something like "*@{upstream}" to "mean fetch the upstream branches of my local branches that live on this remote" which would cover what this series wants. I think it would be useful to support "+" and "^" prefixes on the refs to fetch as well, so that the refmap config controls the mapping and the "what to fetch" config handles negation and forced updates on a per-ref basis.

Quoted 6 lines

The reasoning here is slightly wrong. "^" (not) may be good thing to consider as it is to decide what to fetch, but "+" (overwrite) should be on the refmap side, i.e., where to store. If you are not storing, then "overwrite what I have with non-fast-forward" becomes a nonsense request, and that is why it does not belong to "what to fetch".

I see what you're saying. It is unfortunate though that we then need separate refspecs if want to reject non fast-forward updates of some branches but map the refs in the same way for the fast-forward and non-fast-forward cases.

Thanks

Phillip

#112 lore raw

v11 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v11:

* remote_find_tracking() now falls back to remote.<name>.refmap when remote.<name>.fetch doesn't match. Negative refspecs in fetch still exclude refs, and both cases have tests
* The helpers write straight into a strvec, take the output parameter last and the remote as const
* The followRemoteHEAD lookup is done once, not duplicated in two branches
* Clearer commit messages and docs: a working example plus a reference to 49ca2fba39, the goal stated first, an incorrect "used to die" claim dropped, and an explanation of why a refmap turns on the inference
* use_limited_fetch is a bool

Changes in v10:

* Resolve merge conflict after master integrated
ch/fetch-followremotehead-lazy-validation.

Changes in v9:

* Moved the do_fetch() if/else-if/else split out of the preliminary helper-extraction commit and into the refmap-inference commit that needs it. Preliminary commit is now a simpler refactor.

Changes in v8:

* Split into a preliminary collect_upstream_from_remote() helper commit plus a simplified refmap-inference commit.
* Fixed a regression that could drop the current branch's @{upstream} when remote.<name>.fetch was narrowed.
* Threaded struct repository * through instead of hardcoding the_repository.
* Minor naming and comment cleanups.

Changes in v7:

* Fixed fatal: --refmap option is only meaningful with command-line refspec(s) firing just from configuring remote.<name>.refmap alone, with nothing else to fetch.
* When both remote.<name>.fetch and remote.<name>.refmap are configured, a refspec-less fetch follows .fetch as usual, .refmap only remaps something already being fetched by name. Added tests.

Changes in v6:

* Remove leftover reference to deleted default-branch logic in commit message.

Changes in v5:

* Renumber test file from t5585 to t5586.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (5):
fetch: add remote.<name>.refmap
fetch: extract collect_upstream_from_remote() helper
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   8 +++
 Documentation/fetch-options.adoc |  10 +++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  |  94 +++++++++++++++++-------
 builtin/remote.c                 |  31 ++++++--
 remote.c                         |  60 +++++++++++++++-
 remote.h                         |  20 ++++++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++++
 t/t5510-fetch.sh                 | 101 ++++++++++++++++++++++++++
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 11 files changed, 501 insertions(+), 33 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

base-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v11
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v11
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v10:

1: ba7f1354cd ! 1: d66b3c0bb1 fetch: add remote.<name>.refmap

     @@ remote.c: void ref_push_report_free(struct ref_push_report *report)
       int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
       {
      -	return refspec_find_match(&remote->fetch, refspec);
     -+	if (remote->fetch.nr)
     -+		return refspec_find_match(&remote->fetch, refspec);
     -+	return refspec_find_match(&remote->refmap, refspec);
     ++	char **result = refspec->src ? &refspec->dst : &refspec->src;
     ++
     ++	if (remote->fetch.nr && !refspec_find_match(&remote->fetch, refspec))
     ++		return 0;
     ++	if (refspec_find_match(&remote->refmap, refspec))
     ++		return -1;
     ++	if (refname_matches_negative_refspec_item(refspec->src, &remote->fetch)) {
     ++		FREE_AND_NULL(*result);
     ++		return -1;
     ++	}
     ++	return 0;
       }
       
       static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
      +		EOF
      +	)
      +'
     ++
     ++test_expect_success 'remote.<name>.refmap tracks a branch outside remote.<name>.fetch' '
     ++	test_when_finished "rm -fr fetch-refmap-upstream downstream" &&
     ++	git init -b main fetch-refmap-upstream &&
     ++	test_commit -C fetch-refmap-upstream base &&
     ++	git -C fetch-refmap-upstream branch other &&
     ++	git init downstream &&
     ++	(
     ++		cd downstream &&
     ++		git remote add -t main origin ../fetch-refmap-upstream &&
     ++		git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" &&
     ++		git fetch origin other &&
     ++		git branch --track topic origin/other &&
     ++		test_cmp_config origin branch.topic.remote &&
     ++		test_cmp_config refs/heads/other branch.topic.merge &&
     ++		echo refs/remotes/origin/other >expect &&
     ++		git rev-parse --symbolic-full-name topic@{upstream} >actual &&
     ++		test_cmp expect actual
     ++	)
     ++'
     ++
     ++test_expect_success 'remote.<name>.refmap does not track a branch excluded by remote.<name>.fetch' '
     ++	test_when_finished "rm -fr fetch-refmap-upstream downstream" &&
     ++	git init -b main fetch-refmap-upstream &&
     ++	test_commit -C fetch-refmap-upstream base &&
     ++	git -C fetch-refmap-upstream branch secret &&
     ++	git init downstream &&
     ++	(
     ++		cd downstream &&
     ++		git remote add origin ../fetch-refmap-upstream &&
     ++		git config --add remote.origin.fetch "^refs/heads/secret" &&
     ++		git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" &&
     ++		git fetch origin secret &&
     ++		git rev-parse --verify refs/remotes/origin/secret &&
     ++		test_must_fail git branch --track topic origin/secret 2>err &&
     ++		test_grep "not a branch" err
     ++	)
     ++'
      +
       test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '
       	git branch -f side &&
 2:  aa20604144 ! 2:  f685051033 fetch: extract collect_upstream_from_remote() helper
     @@ Metadata
       ## Commit message ##
          fetch: extract collect_upstream_from_remote() helper
      
     -    When a fetch has no refspec to work from, we fall back to guessing
     -    what to fetch: the remote's configured fetch refspec if there is one,
     -    and separately, whether the current branch's upstream lives on this
     -    remote. The latter is independent of the former on purpose: a narrow
     -    refspec (for example from "git remote add -t") can leave the current
     -    branch's own upstream uncovered, and we still need to be able to fetch
     -    it.
     +    When a fetch has no refspec to work from, we decide what to fetch from
     +    two sources: the remote's configured fetch refspec, and the current
     +    branch's upstream, if it lives on that remote. For example, after
     +
     +        git clone --single-branch <url>
     +        git switch -c work
     +        git push -u origin work
     +
     +    the remote's refspec only covers "main", but a refspec-less "git fetch
     +    origin" still fetches "work" into FETCH_HEAD, so that a plain "git
     +    pull" finds the branch to merge. See 49ca2fba39 (fetch: add
     +    branch.*.merge to default ref-prefix extension, 2022-09-08).
      
          That "does this branch build on this remote" check was inlined for
          just the current branch. Move it into its own function,
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
       	} else {
      -		struct branch *branch = branch_get(NULL);
     -+		struct string_list tracked = STRING_LIST_INIT_DUP;
     -+		struct string_list_item *item;
     - 
     +-
       		if (transport->remote->fetch.nr) {
       			refspec_ref_prefixes(&transport->remote->fetch,
     + 					     &transport_ls_refs_options.ref_prefixes);
      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       			else
       				follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      +		 * branch's upstream (e.g. a narrowed -t refspec), so
      +		 * make sure we can still fetch it regardless.
      +		 */
     -+		collect_upstream_from_remote(the_repository, &tracked,
     -+					      transport->remote, NULL);
     -+		for_each_string_list_item(item, &tracked)
     -+			strvec_push(&transport_ls_refs_options.ref_prefixes,
     -+				    item->string);
     -+		string_list_clear(&tracked, 0);
     ++		collect_upstream_from_remote(the_repository, transport->remote,
     ++					      NULL, &transport_ls_refs_options.ref_prefixes);
       
       		/*
       		 * If there are no refs specified to fetch, then we just
     @@ remote.c: int branch_merge_matches(struct branch *branch,
       }
       
      +void collect_upstream_from_remote(struct repository *repo,
     -+				   struct string_list *tracked,
     -+				   struct remote *remote,
     -+				   const char *refname)
     ++				   const struct remote *remote,
     ++				   const char *refname,
     ++				   struct strvec *tracked)
      +{
      +	struct branch *branch = repo_branch_get(repo, refname);
      +
     @@ remote.c: int branch_merge_matches(struct branch *branch,
      +		return;
      +
      +	for (int i = 0; i < branch->merge_nr; i++)
     -+		string_list_insert(tracked, branch->merge[i]->src);
     ++		strvec_push(tracked, branch->merge[i]->src);
      +}
      +
       __attribute__((format (printf,2,3)))
     @@ remote.h: int branch_has_merge_config(struct branch *branch);
      + * tracked. A NULL refname means the current branch.
      + */
      +void collect_upstream_from_remote(struct repository *repo,
     -+				   struct string_list *tracked,
     -+				   struct remote *remote,
     -+				   const char *refname);
     ++				   const struct remote *remote,
     ++				   const char *refname,
     ++				   struct strvec *tracked);
      +
       /* list of the remote in a group as configured */
       struct remote_group_data {
 3:  9fb7e77a86 ! 3:  5c76b3b88d fetch: infer branches to fetch from a refmap-only remote
     @@ Metadata
       ## Commit message ##
          fetch: infer branches to fetch from a refmap-only remote
      
     -    Configuring remote.<name>.refmap without remote.<name>.fetch used to
     -    make a refspec-less "git fetch <name>" fail with "--refmap option is
     -    only meaningful with command-line refspec(s)", since a refmap only
     -    says where to put fetched refs, not what to fetch.
     +    We want remote.<name>.refmap to work on its own, without also setting
     +    remote.<name>.fetch, so a remote can be limited to the branches
     +    already in use locally, without listing them by hand or touching the
     +    command line every time.
      
     -    Make that case infer what to fetch: the local branches whose
     -    @{upstream} is already on that remote. This lets a remote be
     -    configured to fetch only the branches actually in use, without
     -    listing them by hand in remote.<name>.fetch, and without needing to
     -    touch the command line every time.
     +    Infer what to fetch from the local branches whose @{upstream} is
     +    already on that remote. The refmap itself still only controls where
     +    those fetched refs land, not which branches are selected.
      
          Signed-off-by: Harald Nordgren <[email protected]>
      
     @@ Documentation/config/remote.adoc: remote.<name>.fetch::
      -	store. See the `--refmap` entry in linkgit:git-fetch[1].
      +	store. If `remote.<name>.fetch` is not set either, a refspec-less
      +	fetch infers what to fetch from local branches built on this
     -+	remote, instead of fetching every branch it has. See the
     -+	`--refmap` entry in linkgit:git-fetch[1].
     ++	remote, instead of needing `remote.<name>.fetch` listed by hand.
     ++	See the `--refmap` entry in linkgit:git-fetch[1].
       
       remote.<name>.push::
       	The default set of "refspec" for linkgit:git-push[1]. See
     @@ Documentation/fetch-options.adoc: endif::git-pull[]
      -same way `remote.<name>.fetch` provides the default refspecs to fetch.
      +When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
      +nothing to fetch is specified on the command line, nor is there a
     -+`remote.<name>.fetch`, branches from the remote that are used as the
     -+`@{upstream}` of our local branches are fetched.
     ++`remote.<name>.fetch`, infer what to fetch from local branches built on
     ++this remote: remote-tracking branches from this remote that are also
     ++configured as the `@{upstream}` of a local branch are fetched. The
     ++refmap only decides where those fetched refs land, the same as it
     ++would for an explicit refspec, and does not narrow which branches are
     ++selected. It is what turns this on, since without it the fetched
     ++branches would have nowhere to land.
       
       `-t`::
       `--tags`::
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       
      +	if (!rs->nr && remote && !remote->fetch.nr &&
      +	    effective_refmap && effective_refmap->nr) {
     -+		struct string_list tracked = STRING_LIST_INIT_DUP;
     -+		struct string_list_item *item;
     ++		struct strvec tracked = STRVEC_INIT;
      +
      +		branches_tracking_remote(the_repository, remote, &tracked);
     -+		for_each_string_list_item(item, &tracked)
     -+			refspec_append(&inferred_rs, item->string);
     -+		string_list_clear(&tracked, 0);
     ++		for (size_t i = 0; i < tracked.nr; i++)
     ++			refspec_append(&inferred_rs, tracked.v[i]);
     ++		strvec_clear(&tracked);
      +
      +		rs = &inferred_rs;
      +		infer_from_refmap = 1;
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       	return ref_map;
       }
       
     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 	struct strmap rejected_refs = STRMAP_INIT;
     + 	int summary_width = 0;
     + 	int follow_remote_head = FOLLOW_REMOTE_NEVER;
     ++	bool apply_follow_remote_head_config = false;
     + 
     + 	if (tags == TAGS_DEFAULT) {
     + 		if (transport->remote->fetch_tags == 2)
      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
       
       	if (rs->nr) {
       		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
     +-	} else {
     +-		if (transport->remote->fetch.nr) {
     +-			refspec_ref_prefixes(&transport->remote->fetch,
     +-					     &transport_ls_refs_options.ref_prefixes);
     +-			/*
     +-			 * See remote.c's handling of remote.<name>.followRemoteHEAD
     +-			 * for the analogous, still-unresolved case.
     +-			 */
     +-			if (transport->remote->follow_remote_head)
     +-				follow_remote_head = transport->remote->follow_remote_head;
     +-			else if (config->follow_remote_head_seen)
     +-				follow_remote_head = get_follow_remote_head(config->follow_remote_head_raw);
     +-			else
     +-				follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
     +-		}
      +	} else if (transport->remote->fetch.nr) {
     -+		struct string_list tracked = STRING_LIST_INIT_DUP;
     -+		struct string_list_item *item;
     -+
      +		refspec_ref_prefixes(&transport->remote->fetch,
      +				     &transport_ls_refs_options.ref_prefixes);
     ++		apply_follow_remote_head_config = true;
     + 
     + 		/*
     + 		 * The configured refspec may not cover the current
     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 		 */
     + 		collect_upstream_from_remote(the_repository, transport->remote,
     + 					      NULL, &transport_ls_refs_options.ref_prefixes);
     ++	} else {
     ++		/*
     ++		 * The --refmap command line option, if given, takes
     ++		 * precedence over remote.<name>.refmap.
     ++		 */
     ++		struct refspec *effective_refmap = refmap.nr ? &refmap :
     ++			&transport->remote->refmap;
     ++
     ++		if (effective_refmap->nr) {
     ++			branches_tracking_remote(the_repository, transport->remote,
     ++						  &transport_ls_refs_options.ref_prefixes);
     ++			apply_follow_remote_head_config = true;
     ++		} else {
     ++			collect_upstream_from_remote(the_repository, transport->remote,
     ++						      NULL, &transport_ls_refs_options.ref_prefixes);
     ++		}
     + 
     + 		/*
     + 		 * If there are no refs specified to fetch, then we just
     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     + 				    "HEAD");
     + 	}
     + 
     ++	if (apply_follow_remote_head_config) {
      +		/*
      +		 * See remote.c's handling of remote.<name>.followRemoteHEAD
      +		 * for the analogous, still-unresolved case.
     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,
      +			follow_remote_head = get_follow_remote_head(config->follow_remote_head_raw);
      +		else
      +			follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
     ++	}
      +
     -+		/*
     -+		 * The configured refspec may not cover the current
     -+		 * branch's upstream (e.g. a narrowed -t refspec), so
     -+		 * make sure we can still fetch it regardless.
     -+		 */
     -+		collect_upstream_from_remote(the_repository, &tracked,
     -+					      transport->remote, NULL);
     -+		for_each_string_list_item(item, &tracked)
     -+			strvec_push(&transport_ls_refs_options.ref_prefixes,
     -+				    item->string);
     -+		string_list_clear(&tracked, 0);
     - 	} else {
     -+		/*
     -+		 * The --refmap command line option, if given, takes
     -+		 * precedence over remote.<name>.refmap.
     -+		 */
     -+		struct refspec *effective_refmap = refmap.nr ? &refmap :
     -+			&transport->remote->refmap;
     - 		struct string_list tracked = STRING_LIST_INIT_DUP;
     - 		struct string_list_item *item;
     - 
     --		if (transport->remote->fetch.nr) {
     --			refspec_ref_prefixes(&transport->remote->fetch,
     --					     &transport_ls_refs_options.ref_prefixes);
     -+		if (effective_refmap->nr) {
     -+			branches_tracking_remote(the_repository,
     -+						  transport->remote, &tracked);
     - 			/*
     - 			 * See remote.c's handling of remote.<name>.followRemoteHEAD
     - 			 * for the analogous, still-unresolved case.
     -@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 				follow_remote_head = get_follow_remote_head(config->follow_remote_head_raw);
     - 			else
     - 				follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
     -+		} else {
     -+			collect_upstream_from_remote(the_repository, &tracked,
     -+						      transport->remote, NULL);
     - 		}
     --
     --		/*
     --		 * The configured refspec may not cover the current
     --		 * branch's upstream (e.g. a narrowed -t refspec), so
     --		 * make sure we can still fetch it regardless.
     --		 */
     --		collect_upstream_from_remote(the_repository, &tracked,
     --					      transport->remote, NULL);
     - 		for_each_string_list_item(item, &tracked)
     - 			strvec_push(&transport_ls_refs_options.ref_prefixes,
     - 				    item->string);
     + 	if (tags == TAGS_SET || tags == TAGS_DEFAULT)
     + 		strvec_push(&transport_ls_refs_options.ref_prefixes,
     + 			    "refs/tags/");
      
       ## remote.c ##
      @@ remote.c: void collect_upstream_from_remote(struct repository *repo,
     - 		string_list_insert(tracked, branch->merge[i]->src);
     + 		strvec_push(tracked, branch->merge[i]->src);
       }
       
      +struct branches_tracking_remote_cb_data {
      +	struct repository *repo;
     -+	struct remote *remote;
     -+	struct string_list *tracked;
     ++	const struct remote *remote;
     ++	struct strvec *tracked;
      +};
      +
      +static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
      +{
      +	struct branches_tracking_remote_cb_data *data = cb_data;
      +
     -+	collect_upstream_from_remote(data->repo, data->tracked, data->remote,
     -+				      ref->name);
     ++	collect_upstream_from_remote(data->repo, data->remote, ref->name,
     ++				      data->tracked);
      +	return 0;
      +}
      +
     -+void branches_tracking_remote(struct repository *repo, struct remote *remote,
     -+			       struct string_list *tracked)
     ++void branches_tracking_remote(struct repository *repo,
     ++			       const struct remote *remote,
     ++			       struct strvec *tracked)
      +{
      +	struct branches_tracking_remote_cb_data data = { repo, remote, tracked };
      +
     @@ remote.c: void collect_upstream_from_remote(struct repository *repo,
      
       ## remote.h ##
      @@ remote.h: void collect_upstream_from_remote(struct repository *repo,
     - 				   struct remote *remote,
     - 				   const char *refname);
     + 				   const char *refname,
     + 				   struct strvec *tracked);
       
     -+/* fills tracked with the refname of every local branch's upstream on remote */
     -+void branches_tracking_remote(struct repository *repo, struct remote *remote,
     -+			       struct string_list *tracked);
     ++/* Same as collect_upstream_from_remote(), but checked for every local branch. */
     ++void branches_tracking_remote(struct repository *repo,
     ++			       const struct remote *remote,
     ++			       struct strvec *tracked);
      +
       /* list of the remote in a group as configured */
       struct remote_group_data {
 4:  80028d4f86 ! 4:  417f14d708 remote: add "git remote add --limited-fetch"
     @@ builtin/remote.c: static int add(int argc, const char **argv, const char *prefix
       	repo_config_set(the_repository, buf.buf, url);
       
       	if (!mirror || mirror & MIRROR_FETCH) {
     -+		int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
     ++		bool use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
      +			limited_fetch == 1;
      +
       		strbuf_reset(&buf);

5: 552bf08971 ! 5: de0daa8623 remote: default to --limited-fetch in a shallow repository
@@ Metadata
## Commit message ##
remote: default to --limited-fetch in a shallow repository

- Adding a second remote to a shallow, single-branch clone used to
- still fetch every branch that remote has, since "git remote add"
- always set up a wildcard remote.<name>.fetch refspec regardless of
- how shallow the repository already was. That defeats the purpose of
- having cloned shallow and single-branch in the first place, and can
- make a plain "git fetch" on that remote hang or take a very long
+ Adding a second remote to a shallow clone used to still fetch every
+ branch that remote has, since "git remote add" always set up a
+ wildcard remote.<name>.fetch refspec regardless of how shallow the
+ repository already was. That defeats the purpose of having cloned
+ shallow in the first place (for example with --single-branch), and
+ can make a plain "git fetch" on that remote hang or take a very long
time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already
@@ builtin/remote.c
@@ builtin/remote.c: static int add(int argc, const char **argv, const char *prefix,

if (!mirror || mirror & MIRROR_FETCH) {
- int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+ bool use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
- limited_fetch == 1;
+ (limited_fetch == 1 ||
+ (limited_fetch == -1 &&

Signature · 1 line
gitgitgadget
#113 lore raw

PATCH v11 1/5 fetch: add remote.<name>.refmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.refmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Like --refmap itself, it only maps refs that are actually being fetched, so it has nothing to do when there is nothing explicit to fetch, on the command line or via remote.<name>.fetch.

A refspec-less fetch with a refmap configured but nothing else to say what to fetch falls back to the same defaults as a remote with no refspec or refmap at all. Only an explicit --refmap given on the command line with no command-line refspec to go with it is still rejected, since that combination is a plain mistake to type, unlike a remote.<name>.refmap configured on its own. A remote.<name>.fetch that is also configured is used as before.

---
 Documentation/config/remote.adoc |   5 ++
 Documentation/fetch-options.adoc |   3 +
 builtin/fetch.c                  |  10 ++-
 remote.c                         |  20 +++++-
 remote.h                         |   6 ++
 t/t5510-fetch.sh                 | 101 +++++++++++++++++++++++++++++++
 6 files changed, 142 insertions(+), 3 deletions(-)
Documentation/config/remote.adoc +5 -0
index 3a20d0f752..103eada406 100644
@@ -33,6 +33,11 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.refmap::
+	The default value of the `--refmap` option for linkgit:git-fetch[1].
+	Used to map remote refs being fetched to remote-tracking refs to
+	store. See the `--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 47dea1de8e..c2101a7b39 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +8 -2
index 1f76ae6242..ffcb5ba29a 100644
@@ -518,6 +518,12 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	/*
+	 * The --refmap command line option, if given, takes precedence
+	 * over remote.<name>.refmap.
+	 */
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -561,8 +567,8 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
remote.c +19 -1
index 49ee2ea318..06c61bbc5f 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "refmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -995,7 +1003,17 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	char **result = refspec->src ? &refspec->dst : &refspec->src;
+
+	if (remote->fetch.nr && !refspec_find_match(&remote->fetch, refspec))
+		return 0;
+	if (refspec_find_match(&remote->refmap, refspec))
+		return -1;
+	if (refname_matches_negative_refspec_item(refspec->src, &remote->fetch)) {
+		FREE_AND_NULL(*result);
+		return -1;
+	}
+	return 0;
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +101 -0

107 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#114 lore raw

PATCH v11 2/5 fetch: extract collect_upstream_from_remote() helper

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

When a fetch has no refspec to work from, we decide what to fetch from two sources: the remote's configured fetch refspec, and the current branch's upstream, if it lives on that remote. For example, after

git clone --single-branch <url>
git switch -c work
git push -u origin work

the remote's refspec only covers "main", but a refspec-less "git fetch origin" still fetches "work" into FETCH_HEAD, so that a plain "git pull" finds the branch to merge. See 49ca2fba39 (fetch: add branch.*.merge to default ref-prefix extension, 2022-09-08).

That "does this branch build on this remote" check was inlined for just the current branch. Move it into its own function, collect_upstream_from_remote(), so the same logic can be reused for branches other than just the current one.

---
 builtin/fetch.c | 18 ++++++++----------
 remote.c        | 15 +++++++++++++++
 remote.h        |  9 +++++++++
 3 files changed, 32 insertions(+), 10 deletions(-)
builtin/fetch.c +8 -10
index ffcb5ba29a..51bd5b0838 100644
@@ -1979,8 +1979,6 @@ static int do_fetch(struct transport *transport,
 	if (rs->nr) {
 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
 	} else {
-		struct branch *branch = branch_get(NULL);
-
 		if (transport->remote->fetch.nr) {
 			refspec_ref_prefixes(&transport->remote->fetch,
 					     &transport_ls_refs_options.ref_prefixes);
@@ -1995,14 +1993,14 @@ static int do_fetch(struct transport *transport,
 			else
 				follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
 		}
-		if (branch && branch_has_merge_config(branch) &&
-		    !strcmp(branch->remote_name, transport->remote->name)) {
-			int i;
-			for (i = 0; i < branch->merge_nr; i++) {
-				strvec_push(&transport_ls_refs_options.ref_prefixes,
-					    branch->merge[i]->src);
-			}
-		}
+
+		/*
+		 * The configured refspec may not cover the current
+		 * branch's upstream (e.g. a narrowed -t refspec), so
+		 * make sure we can still fetch it regardless.
+		 */
+		collect_upstream_from_remote(the_repository, transport->remote,
+					      NULL, &transport_ls_refs_options.ref_prefixes);
 
 		/*
 		 * If there are no refs specified to fetch, then we just
remote.c +15 -0
index 06c61bbc5f..43441945cf 100644
@@ -1899,6 +1899,21 @@ int branch_merge_matches(struct branch *branch,
 	return refname_match(branch->merge[i]->src, refname);
 }
 
+void collect_upstream_from_remote(struct repository *repo,
+				   const struct remote *remote,
+				   const char *refname,
+				   struct strvec *tracked)
+{
+	struct branch *branch = repo_branch_get(repo, refname);
+
+	if (!branch_has_merge_config(branch) ||
+	    strcmp(branch->remote_name, remote->name))
+		return;
+
+	for (int i = 0; i < branch->merge_nr; i++)
+		strvec_push(tracked, branch->merge[i]->src);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +9 -0
index ac485a584d..003c8fd180 100644
@@ -359,6 +359,15 @@ int branch_has_merge_config(struct branch *branch);
 
 int branch_merge_matches(struct branch *, int n, const char *);
 
+/*
+ * If refname's branch builds on remote, add its upstream on remote to
+ * tracked. A NULL refname means the current branch.
+ */
+void collect_upstream_from_remote(struct repository *repo,
+				   const struct remote *remote,
+				   const char *refname,
+				   struct strvec *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
Signature · 1 line
gitgitgadget
#115 lore raw

PATCH v11 3/5 fetch: infer branches to fetch from a refmap-only remote

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

We want remote.<name>.refmap to work on its own, without also setting remote.<name>.fetch, so a remote can be limited to the branches already in use locally, without listing them by hand or touching the command line every time.

Infer what to fetch from the local branches whose @{upstream} is already on that remote. The refmap itself still only controls where those fetched refs land, not which branches are selected.

---
 Documentation/config/remote.adoc |   5 +-
 Documentation/fetch-options.adoc |  11 ++-
 builtin/fetch.c                  |  72 ++++++++++++++-----
 remote.c                         |  25 +++++++
 remote.h                         |   5 ++
 t/meson.build                    |   1 +
 t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++
 7 files changed, 219 insertions(+), 19 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh
Documentation/config/remote.adoc +4 -1
index 103eada406..d524d599bc 100644
@@ -36,7 +36,10 @@ remote.<name>.fetch::
 remote.<name>.refmap::
 	The default value of the `--refmap` option for linkgit:git-fetch[1].
 	Used to map remote refs being fetched to remote-tracking refs to
-	store. See the `--refmap` entry in linkgit:git-fetch[1].
+	store. If `remote.<name>.fetch` is not set either, a refspec-less
+	fetch infers what to fetch from local branches built on this
+	remote, instead of needing `remote.<name>.fetch` listed by hand.
+	See the `--refmap` entry in linkgit:git-fetch[1].
 
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
Documentation/fetch-options.adoc +9 -2
index c2101a7b39..9c86570f8d 100644
@@ -245,8 +245,15 @@ endif::git-pull[]
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
 +
-`remote.<name>.refmap` provides the default value for this option, the
-same way `remote.<name>.fetch` provides the default refspecs to fetch.
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+nothing to fetch is specified on the command line, nor is there a
+`remote.<name>.fetch`, infer what to fetch from local branches built on
+this remote: remote-tracking branches from this remote that are also
+configured as the `@{upstream}` of a local branch are fetched. The
+refmap only decides where those fetched refs land, the same as it
+would for an explicit refspec, and does not narrow which branches are
+selected. It is what turns this on, since without it the fetched
+branches would have nowhere to land.
 
 `-t`::
 `--tags`::
builtin/fetch.c +56 -16

128 lines not on this page · raw message

remote.c +25 -0
index 43441945cf..d9a14114a1 100644
@@ -1914,6 +1914,31 @@ void collect_upstream_from_remote(struct repository *repo,
 		strvec_push(tracked, branch->merge[i]->src);
 }
 
+struct branches_tracking_remote_cb_data {
+	struct repository *repo;
+	const struct remote *remote;
+	struct strvec *tracked;
+};
+
+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)
+{
+	struct branches_tracking_remote_cb_data *data = cb_data;
+
+	collect_upstream_from_remote(data->repo, data->remote, ref->name,
+				      data->tracked);
+	return 0;
+}
+
+void branches_tracking_remote(struct repository *repo,
+			       const struct remote *remote,
+			       struct strvec *tracked)
+{
+	struct branches_tracking_remote_cb_data data = { repo, remote, tracked };
+
+	refs_for_each_branch_ref(get_main_ref_store(repo),
+				  add_if_tracking_remote, &data);
+}
+
 __attribute__((format (printf,2,3)))
 static char *error_buf(struct strbuf *err, const char *fmt, ...)
 {
remote.h +5 -0
index 003c8fd180..6fd9b05a79 100644
@@ -368,6 +368,11 @@ void collect_upstream_from_remote(struct repository *repo,
 				   const char *refname,
 				   struct strvec *tracked);
 
+/* Same as collect_upstream_from_remote(), but checked for every local branch. */
+void branches_tracking_remote(struct repository *repo,
+			       const struct remote *remote,
+			       struct strvec *tracked);
+
 /* list of the remote in a group as configured */
 struct remote_group_data {
 	const char *name;
t/meson.build +1 -0
index 11b3752f00..0953a4112f 100644
@@ -736,6 +736,7 @@ integration_tests = [
   't5582-fetch-negative-refspec.sh',
   't5583-push-branches.sh',
   't5584-http-429-retry.sh',
+  't5586-fetch-refmap.sh',
   't5600-clone-fail-cleanup.sh',
   't5601-clone.sh',
   't5602-clone-remote-exec.sh',
t/t5586-fetch-refmap.sh new file +119 -0

119 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#116 lore raw

PATCH v11 4/5 remote: add "git remote add --limited-fetch"

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

A remote added the ordinary way tracks every branch it has, via a wildcard remote.<name>.fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up remote.<name>.refmap instead of remote.<name>.fetch, so that "git fetch <name>" only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch.

---
 Documentation/git-remote.adoc |  8 +++++++-
 builtin/remote.c              | 28 ++++++++++++++++++++++------
 t/t5505-remote.sh             | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)
Documentation/git-remote.adoc +7 -1
index 315100409d..f8764394e7 100644
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [synopsis]
 git remote [-v | --verbose]
-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>
 git remote rename [--[no-]progress] <old> <new>
 git remote remove <name>
 git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
@@ -75,6 +75,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_
 is created.  You can give more than one `-t <branch>` to track
 multiple branches without grabbing all branches.
 +
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
+details.
++
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
 +
builtin/remote.c +22 -6
index fb7e5b114f..7e81a91aef 100644
@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,
 {
 	int fetch = 0, fetch_tags = TAGS_DEFAULT;
 	unsigned mirror = MIRROR_NONE;
+	int limited_fetch = -1; /* unspecified */
 	struct string_list track = STRING_LIST_INIT_NODUP;
 	const char *master = NULL;
 	struct remote *remote;
@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,
 		OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)",
 			N_("set up remote as a mirror to push to or fetch from"),
 			PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),
+		OPT_BOOL(0, "limited-fetch", &limited_fetch,
+			N_("fetch only the branches we build on, instead of every branch")),
 		OPT_END()
 	};
 
@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,
 		die(_("specifying a master branch makes no sense with --mirror"));
 	if (mirror && !(mirror & MIRROR_FETCH) && track.nr)
 		die(_("specifying branches to track makes sense only with fetch mirrors"));
+	if (limited_fetch == 1 && track.nr)
+		die(_("--limited-fetch does not make sense with -t/--track"));
+	if (limited_fetch == 1 && mirror)
+		die(_("--limited-fetch does not make sense with --mirror"));
 
 	name = argv[0];
 	url = argv[1];
@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,
 	repo_config_set(the_repository, buf.buf, url);
 
 	if (!mirror || mirror & MIRROR_FETCH) {
+		bool use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
+			limited_fetch == 1;
+
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "remote.%s.fetch", name);
-		if (track.nr == 0)
-			string_list_append(&track, "*");
-		for (size_t i = 0; i < track.nr; i++) {
-			add_branch(buf.buf, track.items[i].string,
-				   name, mirror, &buf2);
+		if (use_limited_fetch) {
+			strbuf_addf(&buf, "remote.%s.refmap", name);
+			strbuf_reset(&buf2);
+			strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+			repo_config_set(the_repository, buf.buf, buf2.buf);
+		} else {
+			strbuf_addf(&buf, "remote.%s.fetch", name);
+			if (track.nr == 0)
+				string_list_append(&track, "*");
+			for (size_t i = 0; i < track.nr; i++)
+				add_branch(buf.buf, track.items[i].string,
+					   name, mirror, &buf2);
 		}
 	}
 
t/t5505-remote.sh +16 -0
index 5fbfcb0848..0168d5abfe 100755
@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '
 	test_grep ! "\[blob:none\]" out
 '
 
+test_expect_success '--limited-fetch works in a full repository too' '
+	test_when_finished "rm -rf full-add" &&
+	git clone --no-local one full-add &&
+	(
+		cd full-add &&
+		git remote add --limited-fetch upstream ../two &&
+		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+			remote.upstream.refmap &&
+		test_must_fail git config get remote.upstream.fetch
+	)
+'
+
+test_expect_success '--limited-fetch conflicts with -t' '
+	test_must_fail git remote add --limited-fetch -t main upstream ../two
+'
+
 test_expect_success 'check remote-tracking' '
 	(
 		cd test &&
Signature · 1 line
gitgitgadget
#117 lore raw

PATCH v11 5/5 remote: default to --limited-fetch in a shallow repository

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Adding a second remote to a shallow clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote.<name>.fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow in the first place (for example with --single-branch), and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches.

Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it.

---
 Documentation/git-remote.adoc |  8 ++++-
 builtin/remote.c              |  5 ++-
 t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)
Documentation/git-remote.adoc +7 -1
index f8764394e7..f43d707c91 100644
@@ -57,6 +57,11 @@ Add a remote named _<name>_ for the repository at
 _<URL>_.  The command `git fetch <name>` can then be used to create and
 update remote-tracking branches `<name>/<branch>`.
 +
+If the repository is already a shallow repository (see linkgit:git-clone[1]
+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,
+`--limited-fetch` is turned on by default, so that `git fetch <name>` does
+not need to negotiate history for every branch the remote has.
++
 With `-f` option, `git fetch <name>` is run immediately after
 the remote information is set up.
 +
@@ -79,7 +84,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
 that tracks all branches, `remote.<name>.refmap` is set up so that a
 refspec-less `git fetch <name>` only fetches branches our local branches
 are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
-details.
+details. `--no-limited-fetch` explicitly disables this, overriding the
+shallow-repository default described above.
 +
 With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
 up to point at remote's _<master>_ branch. See also the set-head command.
builtin/remote.c +4 -1
index 7e81a91aef..0104f23761 100644
@@ -16,6 +16,7 @@
 #include "rebase.h"
 #include "refs.h"
 #include "refspec.h"
+#include "shallow.h"
 #include "odb.h"
 #include "strvec.h"
 #include "commit-reach.h"
@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,
 
 	if (!mirror || mirror & MIRROR_FETCH) {
 		bool use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&
-			limited_fetch == 1;
+			(limited_fetch == 1 ||
+			 (limited_fetch == -1 &&
+			  is_repository_shallow(the_repository)));
 
 		strbuf_reset(&buf);
 		if (use_limited_fetch) {
t/t5505-remote.sh +60 -0

66 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#118 lore raw

v12 cover fetch: avoid fetching every branch of a new remote in a shallow repo

In reply to Harald Nordgren via GitGitGadget

Avoid fetching every branch of a new remote in a shallow repo.

Changes in v12:

* Renamed remote.<name>.refmap to remote.<name>.fetchRefmap throughout code, docs, and tests, so it reads unambiguously as fetch-only.
* Added the missing code path in get_ref_map() so a branch fetched by name via remote.<name>.fetch is also stored through remote.<name>.fetchRefmap, with a new test (the "cabal" example: fetch = master/next/seen plus a refmap storing them all under one namespace).
* Reworded the fetchRefmap commit message and doc entry to drop a redundant sentence, fix a confusing paragraph, and spell out that remote.<name>.fetch, when configured, still decides what's fetched while the refmap only remaps it.
* Added a prefetch coverage test to t5586-fetch-refmap.sh.

Changes in v11:

* remote_find_tracking() now falls back to remote.<name>.refmap when remote.<name>.fetch doesn't match. Negative refspecs in fetch still exclude refs, and both cases have tests
* The helpers write straight into a strvec, take the output parameter last and the remote as const
* The followRemoteHEAD lookup is done once, not duplicated in two branches
* Clearer commit messages and docs: a working example plus a reference to 49ca2fba39, the goal stated first, an incorrect "used to die" claim dropped, and an explanation of why a refmap turns on the inference
* use_limited_fetch is a bool

Changes in v10:

* Resolve merge conflict after master integrated
ch/fetch-followremotehead-lazy-validation.

Changes in v9:

* Moved the do_fetch() if/else-if/else split out of the preliminary helper-extraction commit and into the refmap-inference commit that needs it. Preliminary commit is now a simpler refactor.

Changes in v8:

* Split into a preliminary collect_upstream_from_remote() helper commit plus a simplified refmap-inference commit.
* Fixed a regression that could drop the current branch's @{upstream} when remote.<name>.fetch was narrowed.
* Threaded struct repository * through instead of hardcoding the_repository.
* Minor naming and comment cleanups.

Changes in v7:

* Fixed fatal: --refmap option is only meaningful with command-line refspec(s) firing just from configuring remote.<name>.refmap alone, with nothing else to fetch.
* When both remote.<name>.fetch and remote.<name>.refmap are configured, a refspec-less fetch follows .fetch as usual, .refmap only remaps something already being fetched by name. Added tests.

Changes in v6:

* Remove leftover reference to deleted default-branch logic in commit message.

Changes in v5:

* Renumber test file from t5585 to t5586.

Changes in v4:

* Removed the automatic default-branch fetch. A fresh remote fetches nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.

Changes in v3:

* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap, reusing git's existing --refmap mechanism instead of inventing new refspec syntax.
* Split the change into 4 commits.

Changes in v2:

* Replaced the opt-in fetch.shallow config entirely with a new special fetch refspec (+:) that git remote add now defaults new remotes to in a shallow repository. The new refspec fetches whichever branches any local branch tracks at that remote, plus the remote's default branch.

Harald Nordgren (5):
fetch: add remote.<name>.fetchRefmap
fetch: extract collect_upstream_from_remote() helper
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository

 Documentation/config/remote.adoc |   9 +++
 Documentation/fetch-options.adoc |  10 +++
 Documentation/git-remote.adoc    |  14 +++-
 builtin/fetch.c                  | 100 +++++++++++++++++------
 builtin/remote.c                 |  31 +++++--
 remote.c                         |  60 +++++++++++++-
 remote.h                         |  20 +++++
 t/meson.build                    |   1 +
 t/t5505-remote.sh                |  76 ++++++++++++++++++
 t/t5510-fetch.sh                 | 130 ++++++++++++++++++++++++++++++
 t/t5586-fetch-refmap.sh          | 133 +++++++++++++++++++++++++++++++
 11 files changed, 551 insertions(+), 33 deletions(-)
 create mode 100755 t/t5586-fetch-refmap.sh

base-commit: 37b3475fb530c14109211faca33716a173db61ed
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v12
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v12
Pull-Request: https://github.com/git/git/pull/2412

Range-diff vs v11:

1: d66b3c0bb1 ! 1: a1f5e81fdc fetch: add remote.<name>.refmap

     @@ Metadata
      Author: Harald Nordgren <[email protected]>
      
       ## Commit message ##
     -    fetch: add remote.<name>.refmap
     +    fetch: add remote.<name>.fetchRefmap
      
     -    Add a per-remote config variable, remote.<name>.refmap, that provides
     -    the default value for --refmap the same way remote.<name>.fetch
     -    already provides the default refspecs to fetch. Like --refmap itself,
     -    it only maps refs that are actually being fetched, so it has nothing
     -    to do when there is nothing explicit to fetch, on the command line or
     -    via remote.<name>.fetch.
     +    Add a per-remote config variable, remote.<name>.fetchRefmap, that
     +    provides the default value for --refmap the same way
     +    remote.<name>.fetch already provides the default refspecs to fetch.
      
     -    A refspec-less fetch with a refmap configured but nothing else to say
     -    what to fetch falls back to the same defaults as a remote with no
     -    refspec or refmap at all. Only an explicit --refmap given on the
     -    command line with no command-line refspec to go with it is still
     -    rejected, since that combination is a plain mistake to type, unlike a
     -    remote.<name>.refmap configured on its own. A remote.<name>.fetch
     -    that is also configured is used as before.
     +    Unlike passing --refmap on the command line, which requires an
     +    explicit refspec to go with it, configuring remote.<name>.fetchRefmap
     +    on its own does not change what a refspec-less fetch does: it falls
     +    back to the same defaults as a remote with no fetchRefmap at all.
     +
     +    When remote.<name>.fetch is also configured, it still decides what to
     +    fetch, and the refmap maps what it fetches. This lets a remote name
     +    only a few branches and store them all through one refmap:
     +
     +        [remote "cabal"]
     +            fetch = master
     +            fetch = next
     +            fetch = seen
     +            fetchRefmap = +refs/heads/*:refs/remotes/cabal/*
     +
     +    Looking up the remote-tracking branch for a remote branch, as
     +    @{upstream} and "git branch --track" do, falls back to the refmap when
     +    remote.<name>.fetch has no match, so that these work for such
     +    branches too. A branch excluded by a negative refspec in
     +    remote.<name>.fetch stays excluded.
      
          Signed-off-by: Harald Nordgren <[email protected]>
      
     @@ Documentation/config/remote.adoc: remote.<name>.fetch::
       	The default set of "refspec" for linkgit:git-fetch[1]. See
       	linkgit:git-fetch[1].
       
     -+remote.<name>.refmap::
     -+	The default value of the `--refmap` option for linkgit:git-fetch[1].
     -+	Used to map remote refs being fetched to remote-tracking refs to
     -+	store. See the `--refmap` entry in linkgit:git-fetch[1].
     ++remote.<name>.fetchRefmap::
     ++	The default value of the `--refmap` option, used to map refs being
     ++	fetched to remote-tracking refs to store, the same way
     ++	`remote.<name>.fetch` does for an explicit refspec.
     ++	`remote.<name>.fetch`, if also configured, still decides what is
     ++	fetched. See the `--refmap` entry in linkgit:git-fetch[1].
      +
       remote.<name>.push::
       	The default set of "refspec" for linkgit:git-push[1]. See
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       	struct ref **tail = &ref_map;
      +	/*
      +	 * The --refmap command line option, if given, takes precedence
     -+	 * over remote.<name>.refmap.
     ++	 * over remote.<name>.fetchRefmap.
      +	 */
      +	struct refspec *effective_refmap =
      +		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
       
       	/* opportunistically-updated references: */
       	struct ref *orefs = NULL, **oref_tail = &orefs;
     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
     + 	filter_prefetch_refspec(rs);
     + 	if (remote)
     + 		filter_prefetch_refspec(&remote->fetch);
     ++	if (effective_refmap)
     ++		filter_prefetch_refspec(effective_refmap);
     + 
     + 	if (rs->nr) {
     + 		struct refspec *fetch_refspec;
      @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       		 * by ref_remove_duplicates() in favor of one of these
       		 * opportunistic entries with FETCH_HEAD_IGNORE.
     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
       		else
       			fetch_refspec = &remote->fetch;
       
     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
     + 			if (has_merge &&
     + 			    !strcmp(branch->remote_name, remote->name))
     + 				add_merge_config(&ref_map, remote_refs, branch, &tail);
     ++
     ++			for (i = 0; i < remote->refmap.nr; i++)
     ++				get_fetch_map(ref_map, &remote->refmap.items[i],
     ++					      &oref_tail, 1);
     + 		} else if (!prefetch) {
     + 			ref_map = get_remote_ref(remote_refs, "HEAD");
     + 			if (!ref_map)
      
       ## remote.c ##
      @@ remote.c: static struct remote *make_remote(struct remote_state *remote_state,
     @@ remote.c: static int handle_config(const char *key, const char *value,
       			return -1;
       		refspec_append(&remote->fetch, v);
       		free(v);
     -+	} else if (!strcmp(subkey, "refmap")) {
     ++	} else if (!strcmp(subkey, "fetchrefmap")) {
      +		char *v;
      +		if (git_config_string(&v, key, value))
      +			return -1;
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
       	)
       '
       
     -+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '
     -+	test_when_finished "git -C three config --unset remote.origin.refmap" &&
     ++test_expect_success 'remote.*.fetchRefmap acts like --refmap on the command line' '
     ++	test_when_finished "git -C three config --unset remote.origin.fetchRefmap" &&
      +	git branch -f side &&
     -+	git -C three config remote.origin.refmap \
     ++	git -C three config remote.origin.fetchRefmap \
      +		"refs/heads/*:refs/remotes/other/*" &&
      +	(
      +		cd three &&
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
      +	test_cmp expect actual
      +}
      +
     -+test_expect_success 'remote.<name>.refmap without tracking (baseline)' '
     ++test_expect_success 'remote.<name>.fetchRefmap without tracking (baseline)' '
      +	test_when_finished "rm -fr fetch-refmap-baseline fetch-refmap-upstream" &&
      +	git init -b main fetch-refmap-upstream &&
      +	test_commit -C fetch-refmap-upstream base &&
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
      +
      +		# Without fetch refspec, but with fetch refmap.
      +		git config --unset-all remote.origin.fetch &&
     -+		git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" &&
     ++		git config remote.origin.fetchRefmap "+refs/heads/*:refs/remotes/origin/*" &&
      +
      +		# Nothing tracked, nothing fetched, no error.
      +		git fetch origin &&
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
      +
      +		# With both refmap and fetch configured, remote.<name>.fetch
      +		# wins: a refspec-less fetch follows it as usual, and
     -+		# remote.<name>.refmap plays no part in deciding what to
     -+		# fetch, only in remapping something that is already being
     -+		# fetched by name.
     ++		# remote.<name>.fetchRefmap plays no part in deciding what to
     ++		# fetch, only in mapping what is fetched.
      +		git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" &&
      +		git fetch origin &&
      +		check_fetched_refs <<-\EOF
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
      +	)
      +'
      +
     -+test_expect_success 'remote.<name>.refmap tracks a branch outside remote.<name>.fetch' '
     ++test_expect_success 'remote.<name>.fetchRefmap stores branches named in remote.<name>.fetch' '
     ++	test_when_finished "rm -fr fetch-refmap-upstream downstream" &&
     ++	git init -b master fetch-refmap-upstream &&
     ++	test_commit -C fetch-refmap-upstream base &&
     ++	git -C fetch-refmap-upstream branch next &&
     ++	git -C fetch-refmap-upstream branch seen &&
     ++	git -C fetch-refmap-upstream branch other &&
     ++	git init downstream &&
     ++	(
     ++		cd downstream &&
     ++		git remote add cabal ../fetch-refmap-upstream &&
     ++		git config --unset-all remote.cabal.fetch &&
     ++		git config --add remote.cabal.fetch master &&
     ++		git config --add remote.cabal.fetch next &&
     ++		git config --add remote.cabal.fetch seen &&
     ++		git config remote.cabal.fetchRefmap "+refs/heads/*:refs/remotes/cabal/*" &&
     ++		git fetch cabal &&
     ++		check_fetched_refs <<-\EOF &&
     ++		refs/remotes/cabal/HEAD
     ++		refs/remotes/cabal/master
     ++		refs/remotes/cabal/next
     ++		refs/remotes/cabal/seen
     ++		EOF
     ++		git branch --track topic cabal/next &&
     ++		echo refs/remotes/cabal/next >expect &&
     ++		git rev-parse --symbolic-full-name topic@{upstream} >actual &&
     ++		test_cmp expect actual
     ++	)
     ++'
     ++
     ++test_expect_success 'remote.<name>.fetchRefmap tracks a branch outside remote.<name>.fetch' '
      +	test_when_finished "rm -fr fetch-refmap-upstream downstream" &&
      +	git init -b main fetch-refmap-upstream &&
      +	test_commit -C fetch-refmap-upstream base &&
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
      +	(
      +		cd downstream &&
      +		git remote add -t main origin ../fetch-refmap-upstream &&
     -+		git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" &&
     ++		git config remote.origin.fetchRefmap "+refs/heads/*:refs/remotes/origin/*" &&
      +		git fetch origin other &&
      +		git branch --track topic origin/other &&
      +		test_cmp_config origin branch.topic.remote &&
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
      +	)
      +'
      +
     -+test_expect_success 'remote.<name>.refmap does not track a branch excluded by remote.<name>.fetch' '
     ++test_expect_success 'remote.<name>.fetchRefmap does not track a branch excluded by remote.<name>.fetch' '
      +	test_when_finished "rm -fr fetch-refmap-upstream downstream" &&
      +	git init -b main fetch-refmap-upstream &&
      +	test_commit -C fetch-refmap-upstream base &&
     @@ t/t5510-fetch.sh: test_expect_success 'explicit --refmap option overrides remote
      +		cd downstream &&
      +		git remote add origin ../fetch-refmap-upstream &&
      +		git config --add remote.origin.fetch "^refs/heads/secret" &&
     -+		git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" &&
     ++		git config remote.origin.fetchRefmap "+refs/heads/*:refs/remotes/origin/*" &&
      +		git fetch origin secret &&
      +		git rev-parse --verify refs/remotes/origin/secret &&
      +		test_must_fail git branch --track topic origin/secret 2>err &&
 2:  f685051033 = 2:  e8af5bc76b fetch: extract collect_upstream_from_remote() helper
 3:  5c76b3b88d ! 3:  aad6e5f66f fetch: infer branches to fetch from a refmap-only remote

@@ Metadata
## Commit message ##
fetch: infer branches to fetch from a refmap-only remote

- We want remote.<name>.refmap to work on its own, without also setting
- remote.<name>.fetch, so a remote can be limited to the branches
- already in use locally, without listing them by hand or touching the
- command line every time.
+ We want remote.<name>.fetchRefmap to work on its own, without also
+ setting remote.<name>.fetch, so a remote can be limited to the
+ branches already in use locally, without listing them by hand or
+ touching the command line every time.

Infer what to fetch from the local branches whose @{upstream} is already on that remote. The refmap itself still only controls where
@@ Commit message

## Documentation/config/remote.adoc ##
-@@ Documentation/config/remote.adoc: remote.<name>.fetch::
- remote.<name>.refmap::
- The default value of the --refmap option for linkgit:git-fetch[1].
- Used to map remote refs being fetched to remote-tracking refs to
-- store. See the --refmap entry in linkgit:git-fetch[1].
-+ store. If remote.<name>.fetch is not set either, a refspec-less
-+ fetch infers what to fetch from local branches built on this
-+ remote, instead of needing remote.<name>.fetch listed by hand.
+@@ Documentation/config/remote.adoc: remote.<name>.fetchRefmap::
+ fetched to remote-tracking refs to store, the same way
+ remote.<name>.fetch does for an explicit refspec.
+ remote.<name>.fetch, if also configured, still decides what is
+- fetched. See the --refmap entry in linkgit:git-fetch[1].
++ fetched. If it is not set either, a refspec-less fetch instead
++ infers what to fetch from local branches built on this remote.
+ See the --refmap entry in linkgit:git-fetch[1].

remote.<name>.push::
@@ Documentation/fetch-options.adoc: endif::git-pull[]
+
-remote.<name>.refmap provides the default value for this option, the -same way remote.<name>.fetch provides the default refspecs to fetch.
-+When a refmap is active (from --refmap or remote.<name>.refmap) but ++When a refmap is active (from --refmap or remote.<name>.fetchRefmap) but
+nothing to fetch is specified on the command line, nor is there a +remote.<name>.fetch, infer what to fetch from local branches built on +this remote: remote-tracking branches from this remote that are also
@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
filter_prefetch_refspec(rs);
if (remote)
filter_prefetch_refspec(&remote->fetch);
+ if (effective_refmap)
+ filter_prefetch_refspec(effective_refmap);

+ if (!rs->nr && remote && !remote->fetch.nr &&
+ effective_refmap && effective_refmap->nr) {
@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
+ } else {
+ /*
+ * The --refmap command line option, if given, takes
-+ * precedence over remote.<name>.refmap.
++ * precedence over remote.<name>.fetchRefmap.
+ */
+ struct refspec *effective_refmap = refmap.nr ? &refmap :
+ &transport->remote->refmap;
@@ t/t5586-fetch-refmap.sh (new)
@@
+#!/bin/sh
+
-+test_description='"git fetch" with a remote.<name>.refmap but no remote.<name>.fetch ++test_description='"git fetch" with a remote.<name>.fetchRefmap but no remote.<name>.fetch
+
+When a remote has a refmap configured but no fetch refspec, a +refspec-less fetch infers what to fetch from the local branches whose
@@ t/t5586-fetch-refmap.sh (new)
+ cd client &&
+ git remote add upstream .. &&
+ test_might_fail git config unset remote.upstream.fetch &&
-+ git config remote.upstream.refmap \
++ git config remote.upstream.fetchRefmap \
+ "+refs/heads/*:refs/remotes/upstream/*"
+ )
+'
@@ t/t5586-fetch-refmap.sh (new)
+ )
+'
+
++test_expect_success 'prefetch stores tracked branches under refs/prefetch/' '
++ test_commit main-3 &&
++ (
++ cd client &&
++ git rev-parse refs/remotes/upstream/main >expect &&
++ git fetch --prefetch upstream &&
++ git rev-parse refs/remotes/upstream/main >actual &&
++ test_cmp expect actual &&
++ git -C .. rev-parse main >expect &&
++ git rev-parse refs/prefetch/remotes/upstream/main >actual &&
++ test_cmp expect actual
++ )
++'
++
+test_expect_success 'git remote show does not choke on a refmap-only remote' '
+ (
+ cd client &&
4: 417f14d708 ! 4: 97650da870 remote: add "git remote add --limited-fetch"
@@ Commit message
branches nobody asked for.

Give "git remote add" a --limited-fetch option that sets up
- remote.<name>.refmap instead of remote.<name>.fetch, so that
+ remote.<name>.fetchRefmap instead of remote.<name>.fetch, so that
"git fetch <name>" only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to
@@ Documentation/git-remote.adoc: the refs/remotes/<name>/ namespace, a refspec t
multiple branches without grabbing all branches.
+
+With --limited-fetch option, instead of a remote.<name>.fetch refspec
-+that tracks all branches, remote.<name>.refmap is set up so that a ++that tracks all branches, remote.<name>.fetchRefmap is set up so that a
+refspec-less git fetch <name> only fetches branches our local branches +are built on. See the --refmap entry in linkgit:git-fetch[1] for +details.
@@ builtin/remote.c: static int add(int argc, const char **argv, const char *prefix
- add_branch(buf.buf, track.items[i].string,
- name, mirror, &buf2);
+ if (use_limited_fetch) {
-+ strbuf_addf(&buf, "remote.%s.refmap", name);
++ strbuf_addf(&buf, "remote.%s.fetchRefmap", name);
+ strbuf_reset(&buf2);
+ strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name);
+ repo_config_set(the_repository, buf.buf, buf2.buf);
@@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only
+ cd full-add &&
+ git remote add --limited-fetch upstream ../two &&
+ test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
-+ remote.upstream.refmap &&
++ remote.upstream.fetchRefmap &&
+ test_must_fail git config get remote.upstream.fetch
+ )
+'
5: de0daa8623 ! 5: a802c9f82f remote: default to --limited-fetch in a shallow repository

     @@ Documentation/git-remote.adoc: Add a remote named _<name>_ for the repository at
       the remote information is set up.
       +
      @@ Documentation/git-remote.adoc: With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
     - that tracks all branches, `remote.<name>.refmap` is set up so that a
     + that tracks all branches, `remote.<name>.fetchRefmap` is set up so that a
       refspec-less `git fetch <name>` only fetches branches our local branches
       are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
      -details.
     @@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only
      +		cd shallow-add &&
      +		git remote add upstream ../two &&
      +		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
     -+			remote.upstream.refmap &&
     ++			remote.upstream.fetchRefmap &&
      +		test_must_fail git config get remote.upstream.fetch &&
      +		git fetch upstream main &&
      +		git branch --set-upstream-to=upstream/main &&
     @@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only
      +		git remote add --no-limited-fetch upstream ../two &&
      +		test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
      +			remote.upstream.fetch &&
     -+		test_must_fail git config get remote.upstream.refmap
     ++		test_must_fail git config get remote.upstream.fetchRefmap
      +	)
      +'
      +
Signature · 1 line
gitgitgadget
#119 lore raw

PATCH v12 1/5 fetch: add remote.<name>.fetchRefmap

From: Harald Nordgren <[email protected]>

Add a per-remote config variable, remote.<name>.fetchRefmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch.

Unlike passing --refmap on the command line, which requires an explicit refspec to go with it, configuring remote.<name>.fetchRefmap on its own does not change what a refspec-less fetch does: it falls back to the same defaults as a remote with no fetchRefmap at all.

When remote.<name>.fetch is also configured, it still decides what to fetch, and the refmap maps what it fetches. This lets a remote name only a few branches and store them all through one refmap:

[remote "cabal"]
fetch = master
fetch = next
fetch = seen
fetchRefmap = +refs/heads/*:refs/remotes/cabal/*

Looking up the remote-tracking branch for a remote branch, as @{upstream} and "git branch --track" do, falls back to the refmap when remote.<name>.fetch has no match, so that these work for such branches too. A branch excluded by a negative refspec in remote.<name>.fetch stays excluded.

---
 Documentation/config/remote.adoc |   7 ++
 Documentation/fetch-options.adoc |   3 +
 builtin/fetch.c                  |  16 +++-
 remote.c                         |  20 ++++-
 remote.h                         |   6 ++
 t/t5510-fetch.sh                 | 130 +++++++++++++++++++++++++++++++
 6 files changed, 179 insertions(+), 3 deletions(-)
Documentation/config/remote.adoc +7 -0
index 3a20d0f752..236ccca93e 100644
@@ -33,6 +33,13 @@ remote.<name>.fetch::
 	The default set of "refspec" for linkgit:git-fetch[1]. See
 	linkgit:git-fetch[1].
 
+remote.<name>.fetchRefmap::
+	The default value of the `--refmap` option, used to map refs being
+	fetched to remote-tracking refs to store, the same way
+	`remote.<name>.fetch` does for an explicit refspec.
+	`remote.<name>.fetch`, if also configured, still decides what is
+	fetched. See the `--refmap` entry in linkgit:git-fetch[1].
+
 remote.<name>.push::
 	The default set of "refspec" for linkgit:git-push[1]. See
 	linkgit:git-push[1].
Documentation/fetch-options.adoc +3 -0
index 47dea1de8e..c2101a7b39 100644
@@ -244,6 +244,9 @@ endif::git-pull[]
 	refspecs and rely entirely on the refspecs supplied as
 	command-line arguments. See section on "Configured Remote-tracking
 	Branches" for details.
++
+`remote.<name>.refmap` provides the default value for this option, the
+same way `remote.<name>.fetch` provides the default refspecs to fetch.
 
 `-t`::
 `--tags`::
builtin/fetch.c +14 -2
index 1f76ae6242..305934f5f4 100644
@@ -518,6 +518,12 @@ static struct ref *get_ref_map(struct remote *remote,
 	struct ref *rm;
 	struct ref *ref_map = NULL;
 	struct ref **tail = &ref_map;
+	/*
+	 * The --refmap command line option, if given, takes precedence
+	 * over remote.<name>.fetchRefmap.
+	 */
+	struct refspec *effective_refmap =
+		refmap.nr ? &refmap : remote ? &remote->refmap : NULL;
 
 	/* opportunistically-updated references: */
 	struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -528,6 +534,8 @@ static struct ref *get_ref_map(struct remote *remote,
 	filter_prefetch_refspec(rs);
 	if (remote)
 		filter_prefetch_refspec(&remote->fetch);
+	if (effective_refmap)
+		filter_prefetch_refspec(effective_refmap);
 
 	if (rs->nr) {
 		struct refspec *fetch_refspec;
@@ -561,8 +569,8 @@ static struct ref *get_ref_map(struct remote *remote,
 		 * by ref_remove_duplicates() in favor of one of these
 		 * opportunistic entries with FETCH_HEAD_IGNORE.
 		 */
-		if (refmap.nr)
-			fetch_refspec = &refmap;
+		if (effective_refmap && effective_refmap->nr)
+			fetch_refspec = effective_refmap;
 		else
 			fetch_refspec = &remote->fetch;
 
@@ -597,6 +605,10 @@ static struct ref *get_ref_map(struct remote *remote,
 			if (has_merge &&
 			    !strcmp(branch->remote_name, remote->name))
 				add_merge_config(&ref_map, remote_refs, branch, &tail);
+
+			for (i = 0; i < remote->refmap.nr; i++)
+				get_fetch_map(ref_map, &remote->refmap.items[i],
+					      &oref_tail, 1);
 		} else if (!prefetch) {
 			ref_map = get_remote_ref(remote_refs, "HEAD");
 			if (!ref_map)
remote.c +19 -1
index 49ee2ea318..9acab2e01a 100644
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
 	ret->name = xstrndup(name, len);
 	refspec_init_push(&ret->push, the_hash_algo);
 	refspec_init_fetch(&ret->fetch, the_hash_algo);
+	refspec_init_fetch(&ret->refmap, the_hash_algo);
 	string_list_init_dup(&ret->server_options);
 	string_list_init_dup(&ret->negotiation_restrict);
 	string_list_init_dup(&ret->negotiation_include);
@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)
 
 	refspec_clear(&remote->push);
 	refspec_clear(&remote->fetch);
+	refspec_clear(&remote->refmap);
 
 	free((char *)remote->receivepack);
 	free((char *)remote->uploadpack);
@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,
 			return -1;
 		refspec_append(&remote->fetch, v);
 		free(v);
+	} else if (!strcmp(subkey, "fetchrefmap")) {
+		char *v;
+		if (git_config_string(&v, key, value))
+			return -1;
+		refspec_append(&remote->refmap, v);
+		free(v);
 	} else if (!strcmp(subkey, "receivepack")) {
 		char *v;
 		if (git_config_string(&v, key, value))
@@ -995,7 +1003,17 @@ void ref_push_report_free(struct ref_push_report *report)
 
 int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
 {
-	return refspec_find_match(&remote->fetch, refspec);
+	char **result = refspec->src ? &refspec->dst : &refspec->src;
+
+	if (remote->fetch.nr && !refspec_find_match(&remote->fetch, refspec))
+		return 0;
+	if (refspec_find_match(&remote->refmap, refspec))
+		return -1;
+	if (refname_matches_negative_refspec_item(refspec->src, &remote->fetch)) {
+		FREE_AND_NULL(*result);
+		return -1;
+	}
+	return 0;
 }
 
 static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
remote.h +6 -0
index cca02033b9..ac485a584d 100644
@@ -90,6 +90,12 @@ struct remote {
 
 	struct refspec fetch;
 
+	/*
+	 * How to map refs fetched without an explicit destination into our
+	 * own namespace, the same as the --refmap command line option.
+	 */
+	struct refspec refmap;
+
 	/*
 	 * The setting for whether to fetch tags (as a separate rule from the
 	 * configured refspecs);
t/t5510-fetch.sh +130 -0

136 lines not on this page · raw message

Signature · 1 line
gitgitgadget