threads / patch / 64603

patch, 2 partsshallow: handling fetch relative-deepen

Subject: [PATCH 0/2] shallow: handling fetch relative-deepen

## tl;dr

27 messages between Dec 9, 2025 and Feb 20, 2026. Diffs are folded; open one to read it.

replies: 26people: 4as markdown or json

Samo Pogačnik via GitGitGadget· Dec 9, 2025, 18:11 UTC · lore

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may endup with some shallows, that are behind the reachable ones. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can allways summ 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening.

Samo Pogačnik (2):
  shallow: free local object_array allocations
  shallow: handling fetch relative-deepen
 shallow.c             |   1 +
 t/t5500-fetch-pack.sh |  24 +++++++
 upload-pack.c         | 142 +++++++++++++++++++++++-------------------
 3 files changed, 103 insertions(+), 64 deletions(-)
base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v1
Pull-Request: https://github.com/git/git/pull/2121
-- 
gitgitgadget
Samo Pogačnik via GitGitGadget· Dec 9, 2025, 18:11 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH 1/2] shallow: free local object_array allocations

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

The local object_array 'stack' in get_shallow_commits() function does not free its dynamic elements before the function returns. As a result elements remain allocated and their reference forgotten.

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
---
 shallow.c | 1 +
 1 file changed, 1 insertion(+)
Show changes to shallow.c +1 −0
diff --git a/shallow.c b/shallow.c
index 55b9cd9d3f..497a25836b 100644
--- a/shallow.c
+++ b/shallow.c
@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 		}
 	}
 	deep_clear_commit_depth(&depths, free_depth_in_slab);
+	object_array_clear(&stack);
 
 	return result;
 }
-- 
gitgitgadget
Patrick Steinhardt· Jan 6, 2026, 07:44 UTC · re: Samo Pogačnik via GitGitGadget · lore

Re: [PATCH 1/2] shallow: free local object_array allocations

On Tue, Dec 09, 2025 at 06:11:19PM +0000, Samo Pogačnik via GitGitGadget wrote:
Show 5 quoted lines
> From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>
> 
> The local object_array 'stack' in get_shallow_commits() function
> does not free its dynamic elements before the function returns.
> As a result elements remain allocated and their reference forgotten.

I think the elements themselves are actually fine. We have the following loop:

	while (commit || i < heads->nr || stack.nr) {

So while the stack still has entries, we'll keep on iteration. Furthermore, there is no `break` or early return in the loop, so we can sure that we actually pop every single element from the array.

That being said, what we _don't_ do is to free the array itself. So I'm mostly splitting hairs with how the commit message is phrased, the change looks correct to me.

What I'm wondering though is why we never hit this memory leak in our test suite. I guess the reason is simply that we ain't got enough test coverage around shallow clones. Have you seen this leak in the wild? And if so, can we add a test case that surfaces it?

Thanks!
Patrick
Samo Pogačnik· Jan 9, 2026, 16:21 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 1/2] shallow: free local object_array allocations

Hi Patrick, thanks a lot for the reply.

On Tue, 2026-01-06 at 08:44 +0100, Patrick Steinhardt wrote:
Show 26 quoted lines
> On Tue, Dec 09, 2025 at 06:11:19PM +0000, Samo Pogačnik via GitGitGadget
> wrote:
> > From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>
> > 
> > The local object_array 'stack' in get_shallow_commits() function
> > does not free its dynamic elements before the function returns.
> > As a result elements remain allocated and their reference forgotten.
> 
> I think the elements themselves are actually fine. We have the following
> loop:
> 
> 	while (commit || i < heads->nr || stack.nr) {
> 
> So while the stack still has entries, we'll keep on iteration.
> Furthermore, there is no `break` or early return in the loop, so we can
> sure that we actually pop every single element from the array.
> 
> That being said, what we _don't_ do is to free the array itself. So I'm
> mostly splitting hairs with how the commit message is phrased, the
> change looks correct to me.
> 
> What I'm wondering though is why we never hit this memory leak in our
> test suite. I guess the reason is simply that we ain't got enough test
> coverage around shallow clones. Have you seen this leak in the wild? And
> if so, can we add a test case that surfaces it?
> 

Actually, the test I've added with the patch 2/2 does not pass without this memory fix in linux-leaks and linux-reftable-leaks test runs.

best regards, Samo
Patrick Steinhardt· Jan 9, 2026, 16:33 UTC · re: Samo Pogačnik · lore

Re: [PATCH 1/2] shallow: free local object_array allocations

On Fri, Jan 09, 2026 at 05:21:45PM +0100, Samo Pogačnik wrote:
Show 33 quoted lines
> Hi Patrick,
> thanks a lot for the reply.
> 
> On Tue, 2026-01-06 at 08:44 +0100, Patrick Steinhardt wrote:
> > On Tue, Dec 09, 2025 at 06:11:19PM +0000, Samo Pogačnik via GitGitGadget
> > wrote:
> > > From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>
> > > 
> > > The local object_array 'stack' in get_shallow_commits() function
> > > does not free its dynamic elements before the function returns.
> > > As a result elements remain allocated and their reference forgotten.
> > 
> > I think the elements themselves are actually fine. We have the following
> > loop:
> > 
> > 	while (commit || i < heads->nr || stack.nr) {
> > 
> > So while the stack still has entries, we'll keep on iteration.
> > Furthermore, there is no `break` or early return in the loop, so we can
> > sure that we actually pop every single element from the array.
> > 
> > That being said, what we _don't_ do is to free the array itself. So I'm
> > mostly splitting hairs with how the commit message is phrased, the
> > change looks correct to me.
> > 
> > What I'm wondering though is why we never hit this memory leak in our
> > test suite. I guess the reason is simply that we ain't got enough test
> > coverage around shallow clones. Have you seen this leak in the wild? And
> > if so, can we add a test case that surfaces it?
> > 
> 
> Actually, the test I've added with the patch 2/2 does not pass without this
> memory fix in linux-leaks and linux-reftable-leaks test runs.

In that case it would make sense to point out this detail in the commit message to make it a bit easier for the reviewer. Thanks!

Patrick
Samo Pogačnik via GitGitGadget· Dec 9, 2025, 18:11 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH 2/2] shallow: handling fetch relative-deepen

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may endup with some shallows, that are behind the reachable ones. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can allways summ 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening.

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
---
 t/t5500-fetch-pack.sh |  24 +++++++
 upload-pack.c         | 142 +++++++++++++++++++++++-------------------
 2 files changed, 102 insertions(+), 64 deletions(-)
Show changes to 2 files +102 −64

t/t5500-fetch-pack.sh, upload-pack.c

diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
index 2677cd5faa..d05c45e32b 100755
--- a/t/t5500-fetch-pack.sh
+++ b/t/t5500-fetch-pack.sh
@@ -955,6 +955,30 @@ test_expect_success 'fetching deepen' '
 	)
 '
 
+test_expect_success 'fetching deepen beyond merged branch' '
+	test_create_repo shallow-deepen-merged &&
+	(
+		cd shallow-deepen-merged &&
+		git commit --allow-empty -m one &&
+		git commit --allow-empty -m two &&
+		git commit --allow-empty -m three &&
+		git switch -c branch &&
+		git commit --allow-empty -m four &&
+		git commit --allow-empty -m five &&
+		git switch main &&
+		git merge --no-ff branch &&
+		cd - &&
+		git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-merged" deepen.git &&
+		git -C deepen.git fetch origin --deepen=1 &&
+		echo "Shallow:" && cat deepen.git/shallow &&
+		git -C deepen.git rev-list --all >actual &&
+		echo "All rev-lis:" && cat actual &&
+		for commit in $(sed "/^$/d" deepen.git/shallow); do
+			test_grep "$commit" actual || exit 1
+		done
+	)
+'
+
 test_negotiation_algorithm_default () {
 	test_when_finished rm -rf clientv0 clientv2 &&
 	rm -rf server client &&
diff --git a/upload-pack.c b/upload-pack.c
index 2d2b70cbf2..ecd3e7f5ef 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -33,6 +33,7 @@
 #include "json-writer.h"
 #include "strmap.h"
 #include "promisor-remote.h"
+#include "tag.h"
 
 /* Remember to update object flag allocation in object.h */
 #define THEY_HAVE	(1u << 11)
@@ -704,54 +705,82 @@ error:
 	return -1;
 }
 
-static int get_reachable_list(struct upload_pack_data *data,
-			      struct object_array *reachable)
+define_commit_slab(commit_depth, int *);
+static void free_depth_in_slab(int **ptr)
 {
-	struct child_process cmd = CHILD_PROCESS_INIT;
-	int i;
-	struct object *o;
-	char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
-	const unsigned hexsz = the_hash_algo->hexsz;
-	int ret;
-
-	if (do_reachable_revlist(&cmd, &data->shallows, reachable,
-				 data->allow_uor) < 0) {
-		ret = -1;
-		goto out;
-	}
-
-	while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {
-		struct object_id oid;
-		const char *p;
-
-		if (parse_oid_hex(namebuf, &oid, &p) || *p != '\n')
-			break;
-
-		o = lookup_object(the_repository, &oid);
-		if (o && o->type == OBJ_COMMIT) {
-			o->flags &= ~TMP_MARK;
+	FREE_AND_NULL(*ptr);
+}
+static void get_shallows_depth(struct upload_pack_data *data)
+{
+	size_t i = 0, j;
+	int cur_depth = 0, cur_depth_shallow = 0;
+	struct object_array stack = OBJECT_ARRAY_INIT;
+	struct commit *commit = NULL;
+	struct commit_graft *graft;
+	struct commit_depth depths;
+	struct object_array *heads = &data->want_obj;
+	struct object_array *shallows = &data->shallows;
+
+	init_commit_depth(&depths);
+	while (commit || i < heads->nr || stack.nr) {
+		struct commit_list *p;
+		if (!commit) {
+			if (i < heads->nr) {
+				int **depth_slot;
+				commit = (struct commit *)
+					deref_tag(the_repository,
+						  heads->objects[i++].item,
+						  NULL, 0);
+				if (!commit || commit->object.type != OBJ_COMMIT) {
+					commit = NULL;
+					continue;
+				}
+				depth_slot = commit_depth_at(&depths, commit);
+				if (!*depth_slot)
+					*depth_slot = xmalloc(sizeof(int));
+				**depth_slot = 0;
+				cur_depth = 0;
+			} else {
+				commit = (struct commit *)
+					object_array_pop(&stack);
+				cur_depth = **commit_depth_at(&depths, commit);
+			}
 		}
-	}
-	for (i = get_max_object_index(the_repository); 0 < i; i--) {
-		o = get_indexed_object(the_repository, i - 1);
-		if (o && o->type == OBJ_COMMIT &&
-		    (o->flags & TMP_MARK)) {
-			add_object_array(o, NULL, reachable);
-				o->flags &= ~TMP_MARK;
+		parse_commit_or_die(commit);
+		cur_depth++;
+		for (j = 0; j < shallows->nr; j++)
+			if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
+				if ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))
+					cur_depth_shallow = cur_depth;
+
+		if ((is_repository_shallow(the_repository) && !commit->parents &&
+		     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
+		     graft->nr_parent < 0)) {
+			commit = NULL;
+			continue;
+		}
+		for (p = commit->parents, commit = NULL; p; p = p->next) {
+			int **depth_slot = commit_depth_at(&depths, p->item);
+			if (!*depth_slot) {
+				*depth_slot = xmalloc(sizeof(int));
+				**depth_slot = cur_depth;
+			} else {
+				if (cur_depth >= **depth_slot)
+					continue;
+				**depth_slot = cur_depth;
+			}
+			if (p->next)
+				add_object_array(&p->item->object,
+						NULL, &stack);
+			else {
+				commit = p->item;
+				cur_depth = **commit_depth_at(&depths, commit);
+			}
 		}
 	}
-	close(cmd.out);
-
-	if (finish_command(&cmd)) {
-		ret = -1;
-		goto out;
-	}
-
-	ret = 0;
-
-out:
-	child_process_clear(&cmd);
-	return ret;
+	deep_clear_commit_depth(&depths, free_depth_in_slab);
+	object_array_clear(&stack);
+	data->deepen_relative = cur_depth_shallow;
 }
 
 static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)
@@ -881,29 +910,14 @@ static void deepen(struct upload_pack_data *data, int depth)
 			struct object *object = data->shallows.objects[i].item;
 			object->flags |= NOT_SHALLOW;
 		}
-	} else if (data->deepen_relative) {
-		struct object_array reachable_shallows = OBJECT_ARRAY_INIT;
-		struct commit_list *result;
-
-		/*
-		 * Checking for reachable shallows requires that our refs be
-		 * marked with OUR_REF.
-		 */
-		refs_head_ref_namespaced(get_main_ref_store(the_repository),
-					 check_ref, data);
-		for_each_namespaced_ref_1(check_ref, data);
-
-		get_reachable_list(data, &reachable_shallows);
-		result = get_shallow_commits(&reachable_shallows,
-					     depth + 1,
-					     SHALLOW, NOT_SHALLOW);
-		send_shallow(data, result);
-		free_commit_list(result);
-		object_array_clear(&reachable_shallows);
 	} else {
 		struct commit_list *result;
 
-		result = get_shallow_commits(&data->want_obj, depth,
+		if (data->deepen_relative)
+			get_shallows_depth(data);
+
+		result = get_shallow_commits(&data->want_obj,
+					     data->deepen_relative + depth,
 					     SHALLOW, NOT_SHALLOW);
 		send_shallow(data, result);
 		free_commit_list(result);
-- 
gitgitgadget
Patrick Steinhardt· Jan 6, 2026, 07:44 UTC · re: Samo Pogačnik via GitGitGadget · lore

Re: [PATCH 2/2] shallow: handling fetch relative-deepen

On Tue, Dec 09, 2025 at 06:11:20PM +0000, Samo Pogačnik via GitGitGadget wrote:
> From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>
> 
> When a shallowed repository gets deepened beyond the beginning of a
> merged branch, we may endup with some shallows, that are behind the

s/endup/end up/ s/shallows, that/shallows that/

> reachable ones.

Hm, which reachable ones? Sorry, I can't quite follow, it would help the reviewer to add a bit more context.

Show 19 quoted lines
> Added test 'fetching deepen beyond merged branch' exposes that
> behaviour.
> 
> On the other hand, it seems that equivalent absolute depth driven
> fetches result in all the correct shallows. That led to this proposal,
> which unifies absolute and relative deepening in a way that the same
> get_shallow_commits() call is used in both cases. The difference is
> only that depth is adapted for relative deepening by measuring
> equivalent depth of current local shallow commits in the current remote
> repo. Thus a new function get_shallows_depth() has been added and the
> function get_reachable_list() became redundant / removed.
> 
> The get_shallows_depth() function also shares the logic of the
> get_shallow_commits() function, but it focuses on counting depth of
> each existing shallow commit. The minimum result is stored as
> 'data->deepen_relative', which is set not to be zero for relative
> deepening anyway. That way we can allways summ 'data->deepen_relative'
> and 'depth' values, because 'data->deepen_relative' is always 0 in
> absolute deepening.

I think the commit message needs some polishing. I myself am not that familiar with our shallow logic, so I'm a bit lost here to be honest.

Typically, a commit message should be self-explanatory and guide the reader through the problem space as well as the solution. It should, in the following order:

  - Explain what the actual issue is as observed by the user. I'm not
    really sure about this part, only that it's something related to
    shallow clones, deepening and merge commits.
  - Explain what the root cause of the issue is.
  - Explain how the root cause is being fixed. Ideally, it should also
    explain why that is the correct fix, potentially referencing other
    code like you do.

Your commit message on the other hand explains more of the "what" and less of the "why", which makes it hard to follow. Also, an ASCII commit graph would probably go a long way in explaining the issue :)

Show 26 quoted lines
> diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
> index 2677cd5faa..d05c45e32b 100755
> --- a/t/t5500-fetch-pack.sh
> +++ b/t/t5500-fetch-pack.sh
> @@ -955,6 +955,30 @@ test_expect_success 'fetching deepen' '
>  	)
>  '
>  
> +test_expect_success 'fetching deepen beyond merged branch' '
> +	test_create_repo shallow-deepen-merged &&
> +	(
> +		cd shallow-deepen-merged &&
> +		git commit --allow-empty -m one &&
> +		git commit --allow-empty -m two &&
> +		git commit --allow-empty -m three &&
> +		git switch -c branch &&
> +		git commit --allow-empty -m four &&
> +		git commit --allow-empty -m five &&
> +		git switch main &&
> +		git merge --no-ff branch &&
> +		cd - &&
> +		git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-merged" deepen.git &&
> +		git -C deepen.git fetch origin --deepen=1 &&
> +		echo "Shallow:" && cat deepen.git/shallow &&
> +		git -C deepen.git rev-list --all >actual &&
> +		echo "All rev-lis:" && cat actual &&

This statement and the one two lines further up look like debug code to me.

> +		for commit in $(sed "/^$/d" deepen.git/shallow); do
Nit: loops should be formatted like this:
    for commit in ...
    do
        ...
    done
Show 39 quoted lines
> diff --git a/upload-pack.c b/upload-pack.c
> index 2d2b70cbf2..ecd3e7f5ef 100644
> --- a/upload-pack.c
> +++ b/upload-pack.c
> @@ -704,54 +705,82 @@ error:
>  	return -1;
>  }
>  
> -static int get_reachable_list(struct upload_pack_data *data,
> -			      struct object_array *reachable)
> +define_commit_slab(commit_depth, int *);
> +static void free_depth_in_slab(int **ptr)
>  {
> -	struct child_process cmd = CHILD_PROCESS_INIT;
> -	int i;
> -	struct object *o;
> -	char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
> -	const unsigned hexsz = the_hash_algo->hexsz;
> -	int ret;
> -
> -	if (do_reachable_revlist(&cmd, &data->shallows, reachable,
> -				 data->allow_uor) < 0) {
> -		ret = -1;
> -		goto out;
> -	}
> -
> -	while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {
> -		struct object_id oid;
> -		const char *p;
> -
> -		if (parse_oid_hex(namebuf, &oid, &p) || *p != '\n')
> -			break;
> -
> -		o = lookup_object(the_repository, &oid);
> -		if (o && o->type == OBJ_COMMIT) {
> -			o->flags &= ~TMP_MARK;
> +	FREE_AND_NULL(*ptr);
> +}
> +static void get_shallows_depth(struct upload_pack_data *data)

This function looks very similar to `get_shallow_commits()`. Is it possible to deduplicate the logic?

Thanks!
Patrick
Samo Pogačnik· Jan 9, 2026, 16:48 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 2/2] shallow: handling fetch relative-deepen

On Tue, 2026-01-06 at 08:44 +0100, Patrick Steinhardt wrote:
Show 139 quoted lines
> On Tue, Dec 09, 2025 at 06:11:20PM +0000, Samo Pogačnik via GitGitGadget
> wrote:
> > From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>
> > 
> > When a shallowed repository gets deepened beyond the beginning of a
> > merged branch, we may endup with some shallows, that are behind the
> 
> s/endup/end up/
> s/shallows, that/shallows that/
> 
> > reachable ones.
> 
> Hm, which reachable ones? Sorry, I can't quite follow, it would help the
> reviewer to add a bit more context.
> 
> > Added test 'fetching deepen beyond merged branch' exposes that
> > behaviour.
> > 
> > On the other hand, it seems that equivalent absolute depth driven
> > fetches result in all the correct shallows. That led to this proposal,
> > which unifies absolute and relative deepening in a way that the same
> > get_shallow_commits() call is used in both cases. The difference is
> > only that depth is adapted for relative deepening by measuring
> > equivalent depth of current local shallow commits in the current remote
> > repo. Thus a new function get_shallows_depth() has been added and the
> > function get_reachable_list() became redundant / removed.
> > 
> > The get_shallows_depth() function also shares the logic of the
> > get_shallow_commits() function, but it focuses on counting depth of
> > each existing shallow commit. The minimum result is stored as
> > 'data->deepen_relative', which is set not to be zero for relative
> > deepening anyway. That way we can allways summ 'data->deepen_relative'
> > and 'depth' values, because 'data->deepen_relative' is always 0 in
> > absolute deepening.
> 
> I think the commit message needs some polishing. I myself am not that
> familiar with our shallow logic, so I'm a bit lost here to be honest.
> 
> Typically, a commit message should be self-explanatory and guide the
> reader through the problem space as well as the solution. It should, in
> the following order:
> 
>   - Explain what the actual issue is as observed by the user. I'm not
>     really sure about this part, only that it's something related to
>     shallow clones, deepening and merge commits.
> 
>   - Explain what the root cause of the issue is.
> 
>   - Explain how the root cause is being fixed. Ideally, it should also
>     explain why that is the correct fix, potentially referencing other
>     code like you do.
> 
> Your commit message on the other hand explains more of the "what" and
> less of the "why", which makes it hard to follow. Also, an ASCII commit
> graph would probably go a long way in explaining the issue :)
> 
> > diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
> > index 2677cd5faa..d05c45e32b 100755
> > --- a/t/t5500-fetch-pack.sh
> > +++ b/t/t5500-fetch-pack.sh
> > @@ -955,6 +955,30 @@ test_expect_success 'fetching deepen' '
> >  	)
> >  '
> >  
> > +test_expect_success 'fetching deepen beyond merged branch' '
> > +	test_create_repo shallow-deepen-merged &&
> > +	(
> > +		cd shallow-deepen-merged &&
> > +		git commit --allow-empty -m one &&
> > +		git commit --allow-empty -m two &&
> > +		git commit --allow-empty -m three &&
> > +		git switch -c branch &&
> > +		git commit --allow-empty -m four &&
> > +		git commit --allow-empty -m five &&
> > +		git switch main &&
> > +		git merge --no-ff branch &&
> > +		cd - &&
> > +		git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-
> > merged" deepen.git &&
> > +		git -C deepen.git fetch origin --deepen=1 &&
> > +		echo "Shallow:" && cat deepen.git/shallow &&
> > +		git -C deepen.git rev-list --all >actual &&
> > +		echo "All rev-lis:" && cat actual &&
> 
> This statement and the one two lines further up look like debug code to
> me.
> 
> > +		for commit in $(sed "/^$/d" deepen.git/shallow); do
> 
> Nit: loops should be formatted like this:
> 
>     for commit in ...
>     do
>         ...
>     done
> 
> > diff --git a/upload-pack.c b/upload-pack.c
> > index 2d2b70cbf2..ecd3e7f5ef 100644
> > --- a/upload-pack.c
> > +++ b/upload-pack.c
> > @@ -704,54 +705,82 @@ error:
> >  	return -1;
> >  }
> >  
> > -static int get_reachable_list(struct upload_pack_data *data,
> > -			      struct object_array *reachable)
> > +define_commit_slab(commit_depth, int *);
> > +static void free_depth_in_slab(int **ptr)
> >  {
> > -	struct child_process cmd = CHILD_PROCESS_INIT;
> > -	int i;
> > -	struct object *o;
> > -	char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
> > -	const unsigned hexsz = the_hash_algo->hexsz;
> > -	int ret;
> > -
> > -	if (do_reachable_revlist(&cmd, &data->shallows, reachable,
> > -				 data->allow_uor) < 0) {
> > -		ret = -1;
> > -		goto out;
> > -	}
> > -
> > -	while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz +
> > 1) {
> > -		struct object_id oid;
> > -		const char *p;
> > -
> > -		if (parse_oid_hex(namebuf, &oid, &p) || *p != '\n')
> > -			break;
> > -
> > -		o = lookup_object(the_repository, &oid);
> > -		if (o && o->type == OBJ_COMMIT) {
> > -			o->flags &= ~TMP_MARK;
> > +	FREE_AND_NULL(*ptr);
> > +}
> > +static void get_shallows_depth(struct upload_pack_data *data)
> 
> This function looks very similar to `get_shallow_commits()`. Is it
> possible to deduplicate the logic?

Thank you for the valuable reply. I'll try to address all the raised points including mentioning in commit 1/2 that added test from 2/2 fail without 1/2 as quickly as time allows me to.

Thanks!
Samo
Samo Pogačnik via GitGitGadget· Jan 9, 2026, 22:23 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v2 0/2] shallow: handling fetch relative-deepen

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may endup with some shallows, that are behind the reachable ones. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can allways summ 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening.

Samo Pogačnik (2):
  shallow: free local object_array allocations
  shallow: handling fetch relative-deepen
 shallow.c             | 45 +++++++++++++++++--------
 shallow.h             |  1 +
 t/t5500-fetch-pack.sh | 23 +++++++++++++
 upload-pack.c         | 76 +++++--------------------------------------
 4 files changed, 64 insertions(+), 81 deletions(-)
base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v2
Pull-Request: https://github.com/git/git/pull/2121
Range-diff vs v1:
 1:  277c8616a9 ! 1:  f8a8d077cd shallow: free local object_array allocations
     @@ Commit message
          does not free its dynamic elements before the function returns.
          As a result elements remain allocated and their reference forgotten.
      
     +    Also note, that test 'fetching deepen beyond merged branch' added by
     +    'shallow: handling fetch relative-deepen' patch fails without this
     +    correction in linux-leaks and linux-reftable-leaks test runs.
     +
          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
      
       ## shallow.c ##
 2:  b352a33c90 ! 2:  ba1f80105f shallow: handling fetch relative-deepen
     @@ Commit message
          shallow: handling fetch relative-deepen
      
          When a shallowed repository gets deepened beyond the beginning of a
     -    merged branch, we may endup with some shallows, that are behind the
     -    reachable ones. Added test 'fetching deepen beyond merged branch'
     -    exposes that behaviour.
     +    merged branch, we may end up with some shallows that are hidden behind
     +    the reachable shallow commits. Added test 'fetching deepen beyond
     +    merged branch' exposes that behaviour.
     +
     +    An example showing the problem based on added test:
     +    0. Whole initial git repo to be cloned from
     +    Graph:
     +    *   033585d (HEAD -> main) Merge branch 'branch'
     +    |\
     +    | * 984f8b1 (branch) five
     +    | * ecb578a four
     +    |/
     +    * 0cb5d20 three
     +    * 2b4e70d two
     +    * 61ba98b one
     +
     +    1. Initial shallow clone --depth=3 (all good)
     +    Shallows:
     +    2b4e70da2a10e1d3231a0ae2df396024735601f1
     +    ecb578a3cf37198d122ae5df7efed9abaca17144
     +    Graph:
     +    *   033585d (HEAD -> main) Merge branch 'branch'
     +    |\
     +    | * 984f8b1 five
     +    | * ecb578a (grafted) four
     +    * 0cb5d20 three
     +    * 2b4e70d (grafted) two
     +
     +    2. Deepen shallow clone with fetch --deepen=1 (NOT OK)
     +    Shallows:
     +    0cb5d204f4ef96ed241feb0f2088c9f4794ba758
     +    61ba98be443fd51c542eb66585a1f6d7e15fcdae
     +    Graph:
     +    *   033585d (HEAD -> main) Merge branch 'branch'
     +    |\
     +    | * 984f8b1 five
     +    | * ecb578a four
     +    |/
     +    * 0cb5d20 (grafted) three
     +    ---
     +    Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae
     +    is not reachable.
      
          On the other hand, it seems that equivalent absolute depth driven
          fetches result in all the correct shallows. That led to this proposal,
     @@ Commit message
          repo. Thus a new function get_shallows_depth() has been added and the
          function get_reachable_list() became redundant / removed.
      
     +    Same example showing the corrected second step:
     +    2. Deepen shallow clone with fetch --deepen=1 (all good)
     +    Shallow:
     +    61ba98be443fd51c542eb66585a1f6d7e15fcdae
     +    Graph:
     +    *   033585d (HEAD -> main) Merge branch 'branch'
     +    |\
     +    | * 984f8b1 five
     +    | * ecb578a four
     +    |/
     +    * 0cb5d20 three
     +    * 2b4e70d two
     +    * 61ba98b (grafted) one
     +
          The get_shallows_depth() function also shares the logic of the
          get_shallow_commits() function, but it focuses on counting depth of
          each existing shallow commit. The minimum result is stored as
     @@ Commit message
      
          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
      
     + ## shallow.c ##
     +@@ shallow.c: static void free_depth_in_slab(int **ptr)
     + {
     + 	FREE_AND_NULL(*ptr);
     + }
     +-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
     +-		int shallow_flag, int not_shallow_flag)
     ++struct commit_list *get_shallow_commits(struct object_array *heads,
     ++					struct object_array *shallows, int *deepen_relative,
     ++					int depth, int shallow_flag, int not_shallow_flag)
     + {
     +-	size_t i = 0;
     +-	int cur_depth = 0;
     ++	size_t i = 0, j;
     ++	int cur_depth = 0, cur_depth_shallow = 0;
     + 	struct commit_list *result = NULL;
     + 	struct object_array stack = OBJECT_ARRAY_INIT;
     + 	struct commit *commit = NULL;
     +@@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
     + 		}
     + 		parse_commit_or_die(commit);
     + 		cur_depth++;
     +-		if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
     +-		    (is_repository_shallow(the_repository) && !commit->parents &&
     +-		     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
     +-		     graft->nr_parent < 0)) {
     +-			commit_list_insert(commit, &result);
     +-			commit->object.flags |= shallow_flag;
     +-			commit = NULL;
     +-			continue;
     ++		if (shallows) {
     ++			for (j = 0; j < shallows->nr; j++)
     ++				if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
     ++					if ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))
     ++						cur_depth_shallow = cur_depth;
     ++
     ++			if ((is_repository_shallow(the_repository) && !commit->parents &&
     ++			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
     ++			     graft->nr_parent < 0)) {
     ++				commit = NULL;
     ++				continue;
     ++			}
     ++		} else {
     ++			if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
     ++			    (is_repository_shallow(the_repository) && !commit->parents &&
     ++			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
     ++			     graft->nr_parent < 0)) {
     ++				commit_list_insert(commit, &result);
     ++				commit->object.flags |= shallow_flag;
     ++				commit = NULL;
     ++				continue;
     ++			}
     ++			commit->object.flags |= not_shallow_flag;
     + 		}
     +-		commit->object.flags |= not_shallow_flag;
     + 		for (p = commit->parents, commit = NULL; p; p = p->next) {
     + 			int **depth_slot = commit_depth_at(&depths, p->item);
     + 			if (!*depth_slot) {
     +@@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
     + 	}
     + 	deep_clear_commit_depth(&depths, free_depth_in_slab);
     + 	object_array_clear(&stack);
     +-
     ++	if (shallows && deepen_relative)
     ++		*deepen_relative = cur_depth_shallow;
     + 	return result;
     + }
     + 
     +
     + ## shallow.h ##
     +@@ shallow.h: int commit_shallow_file(struct repository *r, struct shallow_lock *lk);
     + void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);
     + 
     + struct commit_list *get_shallow_commits(struct object_array *heads,
     ++					struct object_array *shallows, int *deepen_relative,
     + 					int depth, int shallow_flag, int not_shallow_flag);
     + struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,
     + 						    int shallow_flag, int not_shallow_flag);
     +
       ## t/t5500-fetch-pack.sh ##
      @@ t/t5500-fetch-pack.sh: test_expect_success 'fetching deepen' '
       	)
     @@ t/t5500-fetch-pack.sh: test_expect_success 'fetching deepen' '
      +		cd - &&
      +		git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-merged" deepen.git &&
      +		git -C deepen.git fetch origin --deepen=1 &&
     -+		echo "Shallow:" && cat deepen.git/shallow &&
      +		git -C deepen.git rev-list --all >actual &&
     -+		echo "All rev-lis:" && cat actual &&
     -+		for commit in $(sed "/^$/d" deepen.git/shallow); do
     ++		for commit in $(sed "/^$/d" deepen.git/shallow)
     ++		do
      +			test_grep "$commit" actual || exit 1
      +		done
      +	)
     @@ t/t5500-fetch-pack.sh: test_expect_success 'fetching deepen' '
       	rm -rf server client &&
      
       ## upload-pack.c ##
     -@@
     - #include "json-writer.h"
     - #include "strmap.h"
     - #include "promisor-remote.h"
     -+#include "tag.h"
     - 
     - /* Remember to update object flag allocation in object.h */
     - #define THEY_HAVE	(1u << 11)
      @@ upload-pack.c: error:
       	return -1;
       }
       
      -static int get_reachable_list(struct upload_pack_data *data,
      -			      struct object_array *reachable)
     -+define_commit_slab(commit_depth, int *);
     -+static void free_depth_in_slab(int **ptr)
     ++static void get_shallows_depth(struct upload_pack_data *data)
       {
      -	struct child_process cmd = CHILD_PROCESS_INIT;
      -	int i;
     @@ upload-pack.c: error:
      -		o = lookup_object(the_repository, &oid);
      -		if (o && o->type == OBJ_COMMIT) {
      -			o->flags &= ~TMP_MARK;
     -+	FREE_AND_NULL(*ptr);
     -+}
     -+static void get_shallows_depth(struct upload_pack_data *data)
     -+{
     -+	size_t i = 0, j;
     -+	int cur_depth = 0, cur_depth_shallow = 0;
     -+	struct object_array stack = OBJECT_ARRAY_INIT;
     -+	struct commit *commit = NULL;
     -+	struct commit_graft *graft;
     -+	struct commit_depth depths;
     -+	struct object_array *heads = &data->want_obj;
     -+	struct object_array *shallows = &data->shallows;
     -+
     -+	init_commit_depth(&depths);
     -+	while (commit || i < heads->nr || stack.nr) {
     -+		struct commit_list *p;
     -+		if (!commit) {
     -+			if (i < heads->nr) {
     -+				int **depth_slot;
     -+				commit = (struct commit *)
     -+					deref_tag(the_repository,
     -+						  heads->objects[i++].item,
     -+						  NULL, 0);
     -+				if (!commit || commit->object.type != OBJ_COMMIT) {
     -+					commit = NULL;
     -+					continue;
     -+				}
     -+				depth_slot = commit_depth_at(&depths, commit);
     -+				if (!*depth_slot)
     -+					*depth_slot = xmalloc(sizeof(int));
     -+				**depth_slot = 0;
     -+				cur_depth = 0;
     -+			} else {
     -+				commit = (struct commit *)
     -+					object_array_pop(&stack);
     -+				cur_depth = **commit_depth_at(&depths, commit);
     -+			}
     - 		}
     +-		}
      -	}
      -	for (i = get_max_object_index(the_repository); 0 < i; i--) {
      -		o = get_indexed_object(the_repository, i - 1);
     @@ upload-pack.c: error:
      -		    (o->flags & TMP_MARK)) {
      -			add_object_array(o, NULL, reachable);
      -				o->flags &= ~TMP_MARK;
     -+		parse_commit_or_die(commit);
     -+		cur_depth++;
     -+		for (j = 0; j < shallows->nr; j++)
     -+			if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
     -+				if ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))
     -+					cur_depth_shallow = cur_depth;
     -+
     -+		if ((is_repository_shallow(the_repository) && !commit->parents &&
     -+		     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
     -+		     graft->nr_parent < 0)) {
     -+			commit = NULL;
     -+			continue;
     -+		}
     -+		for (p = commit->parents, commit = NULL; p; p = p->next) {
     -+			int **depth_slot = commit_depth_at(&depths, p->item);
     -+			if (!*depth_slot) {
     -+				*depth_slot = xmalloc(sizeof(int));
     -+				**depth_slot = cur_depth;
     -+			} else {
     -+				if (cur_depth >= **depth_slot)
     -+					continue;
     -+				**depth_slot = cur_depth;
     -+			}
     -+			if (p->next)
     -+				add_object_array(&p->item->object,
     -+						NULL, &stack);
     -+			else {
     -+				commit = p->item;
     -+				cur_depth = **commit_depth_at(&depths, commit);
     -+			}
     - 		}
     - 	}
     +-		}
     +-	}
      -	close(cmd.out);
      -
      -	if (finish_command(&cmd)) {
     @@ upload-pack.c: error:
      -out:
      -	child_process_clear(&cmd);
      -	return ret;
     -+	deep_clear_commit_depth(&depths, free_depth_in_slab);
     -+	object_array_clear(&stack);
     -+	data->deepen_relative = cur_depth_shallow;
     ++	get_shallow_commits(&data->want_obj, &data->shallows,
     ++			    &data->deepen_relative, 0,
     ++			    SHALLOW, NOT_SHALLOW);
       }
       
       static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)
     @@ upload-pack.c: static void deepen(struct upload_pack_data *data, int depth)
      +		if (data->deepen_relative)
      +			get_shallows_depth(data);
      +
     -+		result = get_shallow_commits(&data->want_obj,
     ++		result = get_shallow_commits(&data->want_obj, NULL, NULL,
      +					     data->deepen_relative + depth,
       					     SHALLOW, NOT_SHALLOW);
       		send_shallow(data, result);
-- 
gitgitgadget
Samo Pogačnik via GitGitGadget· Jan 9, 2026, 22:23 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v2 1/2] shallow: free local object_array allocations

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

The local object_array 'stack' in get_shallow_commits() function does not free its dynamic elements before the function returns. As a result elements remain allocated and their reference forgotten.

Also note, that test 'fetching deepen beyond merged branch' added by 'shallow: handling fetch relative-deepen' patch fails without this correction in linux-leaks and linux-reftable-leaks test runs.

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
---
 shallow.c | 1 +
 1 file changed, 1 insertion(+)
Show changes to shallow.c +1 −0
diff --git a/shallow.c b/shallow.c
index 55b9cd9d3f..497a25836b 100644
--- a/shallow.c
+++ b/shallow.c
@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 		}
 	}
 	deep_clear_commit_depth(&depths, free_depth_in_slab);
+	object_array_clear(&stack);
 
 	return result;
 }
-- 
gitgitgadget
Samo Pogačnik via GitGitGadget· Jan 9, 2026, 22:23 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v2 2/2] shallow: handling fetch relative-deepen

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may end up with some shallows that are hidden behind the reachable shallow commits. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

An example showing the problem based on added test:
0. Whole initial git repo to be cloned from
Graph:
*   033585d (HEAD -> main) Merge branch 'branch'
|\
| * 984f8b1 (branch) five
| * ecb578a four
|/
* 0cb5d20 three
* 2b4e70d two
* 61ba98b one
1. Initial shallow clone --depth=3 (all good)
Shallows:
2b4e70da2a10e1d3231a0ae2df396024735601f1
ecb578a3cf37198d122ae5df7efed9abaca17144
Graph:
*   033585d (HEAD -> main) Merge branch 'branch'
|\
| * 984f8b1 five
| * ecb578a (grafted) four
* 0cb5d20 three
* 2b4e70d (grafted) two
2. Deepen shallow clone with fetch --deepen=1 (NOT OK)
Shallows:
0cb5d204f4ef96ed241feb0f2088c9f4794ba758
61ba98be443fd51c542eb66585a1f6d7e15fcdae
Graph:
*   033585d (HEAD -> main) Merge branch 'branch'
|\
| * 984f8b1 five
| * ecb578a four
|/
* 0cb5d20 (grafted) three
---
Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae
is not reachable.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

Same example showing the corrected second step:
2. Deepen shallow clone with fetch --deepen=1 (all good)
Shallow:
61ba98be443fd51c542eb66585a1f6d7e15fcdae
Graph:
*   033585d (HEAD -> main) Merge branch 'branch'
|\
| * 984f8b1 five
| * ecb578a four
|/
* 0cb5d20 three
* 2b4e70d two
* 61ba98b (grafted) one

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can allways summ 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening.

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
---
 shallow.c             | 44 +++++++++++++++++--------
 shallow.h             |  1 +
 t/t5500-fetch-pack.sh | 23 +++++++++++++
 upload-pack.c         | 76 +++++--------------------------------------
 4 files changed, 63 insertions(+), 81 deletions(-)
Show changes to 4 files +63 −81

shallow.c, shallow.h, t/t5500-fetch-pack.sh, upload-pack.c

diff --git a/shallow.c b/shallow.c
index 497a25836b..1a32808865 100644
--- a/shallow.c
+++ b/shallow.c
@@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)
 {
 	FREE_AND_NULL(*ptr);
 }
-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
-		int shallow_flag, int not_shallow_flag)
+struct commit_list *get_shallow_commits(struct object_array *heads,
+					struct object_array *shallows, int *deepen_relative,
+					int depth, int shallow_flag, int not_shallow_flag)
 {
-	size_t i = 0;
-	int cur_depth = 0;
+	size_t i = 0, j;
+	int cur_depth = 0, cur_depth_shallow = 0;
 	struct commit_list *result = NULL;
 	struct object_array stack = OBJECT_ARRAY_INIT;
 	struct commit *commit = NULL;
@@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 		}
 		parse_commit_or_die(commit);
 		cur_depth++;
-		if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
-		    (is_repository_shallow(the_repository) && !commit->parents &&
-		     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
-		     graft->nr_parent < 0)) {
-			commit_list_insert(commit, &result);
-			commit->object.flags |= shallow_flag;
-			commit = NULL;
-			continue;
+		if (shallows) {
+			for (j = 0; j < shallows->nr; j++)
+				if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
+					if ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))
+						cur_depth_shallow = cur_depth;
+
+			if ((is_repository_shallow(the_repository) && !commit->parents &&
+			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
+			     graft->nr_parent < 0)) {
+				commit = NULL;
+				continue;
+			}
+		} else {
+			if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
+			    (is_repository_shallow(the_repository) && !commit->parents &&
+			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
+			     graft->nr_parent < 0)) {
+				commit_list_insert(commit, &result);
+				commit->object.flags |= shallow_flag;
+				commit = NULL;
+				continue;
+			}
+			commit->object.flags |= not_shallow_flag;
 		}
-		commit->object.flags |= not_shallow_flag;
 		for (p = commit->parents, commit = NULL; p; p = p->next) {
 			int **depth_slot = commit_depth_at(&depths, p->item);
 			if (!*depth_slot) {
@@ -199,7 +214,8 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 	}
 	deep_clear_commit_depth(&depths, free_depth_in_slab);
 	object_array_clear(&stack);
-
+	if (shallows && deepen_relative)
+		*deepen_relative = cur_depth_shallow;
 	return result;
 }
 
diff --git a/shallow.h b/shallow.h
index ad591bd139..d1b3878635 100644
--- a/shallow.h
+++ b/shallow.h
@@ -36,6 +36,7 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);
 void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);
 
 struct commit_list *get_shallow_commits(struct object_array *heads,
+					struct object_array *shallows, int *deepen_relative,
 					int depth, int shallow_flag, int not_shallow_flag);
 struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,
 						    int shallow_flag, int not_shallow_flag);
diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
index 2677cd5faa..5a8b30e1fd 100755
--- a/t/t5500-fetch-pack.sh
+++ b/t/t5500-fetch-pack.sh
@@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '
 	)
 '
 
+test_expect_success 'fetching deepen beyond merged branch' '
+	test_create_repo shallow-deepen-merged &&
+	(
+		cd shallow-deepen-merged &&
+		git commit --allow-empty -m one &&
+		git commit --allow-empty -m two &&
+		git commit --allow-empty -m three &&
+		git switch -c branch &&
+		git commit --allow-empty -m four &&
+		git commit --allow-empty -m five &&
+		git switch main &&
+		git merge --no-ff branch &&
+		cd - &&
+		git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-merged" deepen.git &&
+		git -C deepen.git fetch origin --deepen=1 &&
+		git -C deepen.git rev-list --all >actual &&
+		for commit in $(sed "/^$/d" deepen.git/shallow)
+		do
+			test_grep "$commit" actual || exit 1
+		done
+	)
+'
+
 test_negotiation_algorithm_default () {
 	test_when_finished rm -rf clientv0 clientv2 &&
 	rm -rf server client &&
diff --git a/upload-pack.c b/upload-pack.c
index 2d2b70cbf2..4232eef34f 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -704,54 +704,11 @@ error:
 	return -1;
 }
 
-static int get_reachable_list(struct upload_pack_data *data,
-			      struct object_array *reachable)
+static void get_shallows_depth(struct upload_pack_data *data)
 {
-	struct child_process cmd = CHILD_PROCESS_INIT;
-	int i;
-	struct object *o;
-	char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
-	const unsigned hexsz = the_hash_algo->hexsz;
-	int ret;
-
-	if (do_reachable_revlist(&cmd, &data->shallows, reachable,
-				 data->allow_uor) < 0) {
-		ret = -1;
-		goto out;
-	}
-
-	while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {
-		struct object_id oid;
-		const char *p;
-
-		if (parse_oid_hex(namebuf, &oid, &p) || *p != '\n')
-			break;
-
-		o = lookup_object(the_repository, &oid);
-		if (o && o->type == OBJ_COMMIT) {
-			o->flags &= ~TMP_MARK;
-		}
-	}
-	for (i = get_max_object_index(the_repository); 0 < i; i--) {
-		o = get_indexed_object(the_repository, i - 1);
-		if (o && o->type == OBJ_COMMIT &&
-		    (o->flags & TMP_MARK)) {
-			add_object_array(o, NULL, reachable);
-				o->flags &= ~TMP_MARK;
-		}
-	}
-	close(cmd.out);
-
-	if (finish_command(&cmd)) {
-		ret = -1;
-		goto out;
-	}
-
-	ret = 0;
-
-out:
-	child_process_clear(&cmd);
-	return ret;
+	get_shallow_commits(&data->want_obj, &data->shallows,
+			    &data->deepen_relative, 0,
+			    SHALLOW, NOT_SHALLOW);
 }
 
 static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)
@@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)
 			struct object *object = data->shallows.objects[i].item;
 			object->flags |= NOT_SHALLOW;
 		}
-	} else if (data->deepen_relative) {
-		struct object_array reachable_shallows = OBJECT_ARRAY_INIT;
-		struct commit_list *result;
-
-		/*
-		 * Checking for reachable shallows requires that our refs be
-		 * marked with OUR_REF.
-		 */
-		refs_head_ref_namespaced(get_main_ref_store(the_repository),
-					 check_ref, data);
-		for_each_namespaced_ref_1(check_ref, data);
-
-		get_reachable_list(data, &reachable_shallows);
-		result = get_shallow_commits(&reachable_shallows,
-					     depth + 1,
-					     SHALLOW, NOT_SHALLOW);
-		send_shallow(data, result);
-		free_commit_list(result);
-		object_array_clear(&reachable_shallows);
 	} else {
 		struct commit_list *result;
 
-		result = get_shallow_commits(&data->want_obj, depth,
+		if (data->deepen_relative)
+			get_shallows_depth(data);
+
+		result = get_shallow_commits(&data->want_obj, NULL, NULL,
+					     data->deepen_relative + depth,
 					     SHALLOW, NOT_SHALLOW);
 		send_shallow(data, result);
 		free_commit_list(result);
-- 
gitgitgadget
Junio C Hamano· Jan 10, 2026, 04:17 UTC · re: Samo Pogačnik via GitGitGadget · lore

Re: [PATCH v2 2/2] shallow: handling fetch relative-deepen

"Samo Pogačnik via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 12 quoted lines
> 2. Deepen shallow clone with fetch --deepen=1 (NOT OK)
> Shallows:
> 0cb5d204f4ef96ed241feb0f2088c9f4794ba758
> 61ba98be443fd51c542eb66585a1f6d7e15fcdae
> Graph:
> *   033585d (HEAD -> main) Merge branch 'branch'
> |\
> | * 984f8b1 five
> | * ecb578a four
> |/
> * 0cb5d20 (grafted) three
> ---

This three-dash line will act as a marker to tell "git am" that your log message ends here.

To avoid such an accident, make it a habit to indent any and all displayed material used as examples, e.g.,

2. Deepen shallow clone with fetch --deepen=1 (NOT OK)
   Shallows:
   0cb5d204f4ef96ed241feb0f2088c9f4794ba758
   61ba98be443fd51c542eb66585a1f6d7e15fcdae
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a four
   |/
   * 0cb5d20 (grafted) three
   ---
Show 257 quoted lines
> Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae
> is not reachable.
>
> On the other hand, it seems that equivalent absolute depth driven
> fetches result in all the correct shallows. That led to this proposal,
> which unifies absolute and relative deepening in a way that the same
> get_shallow_commits() call is used in both cases. The difference is
> only that depth is adapted for relative deepening by measuring
> equivalent depth of current local shallow commits in the current remote
> repo. Thus a new function get_shallows_depth() has been added and the
> function get_reachable_list() became redundant / removed.
>
> Same example showing the corrected second step:
> 2. Deepen shallow clone with fetch --deepen=1 (all good)
> Shallow:
> 61ba98be443fd51c542eb66585a1f6d7e15fcdae
> Graph:
> *   033585d (HEAD -> main) Merge branch 'branch'
> |\
> | * 984f8b1 five
> | * ecb578a four
> |/
> * 0cb5d20 three
> * 2b4e70d two
> * 61ba98b (grafted) one
>
> The get_shallows_depth() function also shares the logic of the
> get_shallow_commits() function, but it focuses on counting depth of
> each existing shallow commit. The minimum result is stored as
> 'data->deepen_relative', which is set not to be zero for relative
> deepening anyway. That way we can allways summ 'data->deepen_relative'
> and 'depth' values, because 'data->deepen_relative' is always 0 in
> absolute deepening.
>
> Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
> ---
>  shallow.c             | 44 +++++++++++++++++--------
>  shallow.h             |  1 +
>  t/t5500-fetch-pack.sh | 23 +++++++++++++
>  upload-pack.c         | 76 +++++--------------------------------------
>  4 files changed, 63 insertions(+), 81 deletions(-)
>
> diff --git a/shallow.c b/shallow.c
> index 497a25836b..1a32808865 100644
> --- a/shallow.c
> +++ b/shallow.c
> @@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)
>  {
>  	FREE_AND_NULL(*ptr);
>  }
> -struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
> -		int shallow_flag, int not_shallow_flag)
> +struct commit_list *get_shallow_commits(struct object_array *heads,
> +					struct object_array *shallows, int *deepen_relative,
> +					int depth, int shallow_flag, int not_shallow_flag)
>  {
> -	size_t i = 0;
> -	int cur_depth = 0;
> +	size_t i = 0, j;
> +	int cur_depth = 0, cur_depth_shallow = 0;
>  	struct commit_list *result = NULL;
>  	struct object_array stack = OBJECT_ARRAY_INIT;
>  	struct commit *commit = NULL;
> @@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
>  		}
>  		parse_commit_or_die(commit);
>  		cur_depth++;
> -		if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
> -		    (is_repository_shallow(the_repository) && !commit->parents &&
> -		     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
> -		     graft->nr_parent < 0)) {
> -			commit_list_insert(commit, &result);
> -			commit->object.flags |= shallow_flag;
> -			commit = NULL;
> -			continue;
> +		if (shallows) {
> +			for (j = 0; j < shallows->nr; j++)
> +				if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
> +					if ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))
> +						cur_depth_shallow = cur_depth;
> +
> +			if ((is_repository_shallow(the_repository) && !commit->parents &&
> +			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
> +			     graft->nr_parent < 0)) {
> +				commit = NULL;
> +				continue;
> +			}
> +		} else {
> +			if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
> +			    (is_repository_shallow(the_repository) && !commit->parents &&
> +			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
> +			     graft->nr_parent < 0)) {
> +				commit_list_insert(commit, &result);
> +				commit->object.flags |= shallow_flag;
> +				commit = NULL;
> +				continue;
> +			}
> +			commit->object.flags |= not_shallow_flag;
>  		}
> -		commit->object.flags |= not_shallow_flag;
>  		for (p = commit->parents, commit = NULL; p; p = p->next) {
>  			int **depth_slot = commit_depth_at(&depths, p->item);
>  			if (!*depth_slot) {
> @@ -199,7 +214,8 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
>  	}
>  	deep_clear_commit_depth(&depths, free_depth_in_slab);
>  	object_array_clear(&stack);
> -
> +	if (shallows && deepen_relative)
> +		*deepen_relative = cur_depth_shallow;
>  	return result;
>  }
>  
> diff --git a/shallow.h b/shallow.h
> index ad591bd139..d1b3878635 100644
> --- a/shallow.h
> +++ b/shallow.h
> @@ -36,6 +36,7 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);
>  void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);
>  
>  struct commit_list *get_shallow_commits(struct object_array *heads,
> +					struct object_array *shallows, int *deepen_relative,
>  					int depth, int shallow_flag, int not_shallow_flag);
>  struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,
>  						    int shallow_flag, int not_shallow_flag);
> diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
> index 2677cd5faa..5a8b30e1fd 100755
> --- a/t/t5500-fetch-pack.sh
> +++ b/t/t5500-fetch-pack.sh
> @@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '
>  	)
>  '
>  
> +test_expect_success 'fetching deepen beyond merged branch' '
> +	test_create_repo shallow-deepen-merged &&
> +	(
> +		cd shallow-deepen-merged &&
> +		git commit --allow-empty -m one &&
> +		git commit --allow-empty -m two &&
> +		git commit --allow-empty -m three &&
> +		git switch -c branch &&
> +		git commit --allow-empty -m four &&
> +		git commit --allow-empty -m five &&
> +		git switch main &&
> +		git merge --no-ff branch &&
> +		cd - &&
> +		git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-merged" deepen.git &&
> +		git -C deepen.git fetch origin --deepen=1 &&
> +		git -C deepen.git rev-list --all >actual &&
> +		for commit in $(sed "/^$/d" deepen.git/shallow)
> +		do
> +			test_grep "$commit" actual || exit 1
> +		done
> +	)
> +'
> +
>  test_negotiation_algorithm_default () {
>  	test_when_finished rm -rf clientv0 clientv2 &&
>  	rm -rf server client &&
> diff --git a/upload-pack.c b/upload-pack.c
> index 2d2b70cbf2..4232eef34f 100644
> --- a/upload-pack.c
> +++ b/upload-pack.c
> @@ -704,54 +704,11 @@ error:
>  	return -1;
>  }
>  
> -static int get_reachable_list(struct upload_pack_data *data,
> -			      struct object_array *reachable)
> +static void get_shallows_depth(struct upload_pack_data *data)
>  {
> -	struct child_process cmd = CHILD_PROCESS_INIT;
> -	int i;
> -	struct object *o;
> -	char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
> -	const unsigned hexsz = the_hash_algo->hexsz;
> -	int ret;
> -
> -	if (do_reachable_revlist(&cmd, &data->shallows, reachable,
> -				 data->allow_uor) < 0) {
> -		ret = -1;
> -		goto out;
> -	}
> -
> -	while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {
> -		struct object_id oid;
> -		const char *p;
> -
> -		if (parse_oid_hex(namebuf, &oid, &p) || *p != '\n')
> -			break;
> -
> -		o = lookup_object(the_repository, &oid);
> -		if (o && o->type == OBJ_COMMIT) {
> -			o->flags &= ~TMP_MARK;
> -		}
> -	}
> -	for (i = get_max_object_index(the_repository); 0 < i; i--) {
> -		o = get_indexed_object(the_repository, i - 1);
> -		if (o && o->type == OBJ_COMMIT &&
> -		    (o->flags & TMP_MARK)) {
> -			add_object_array(o, NULL, reachable);
> -				o->flags &= ~TMP_MARK;
> -		}
> -	}
> -	close(cmd.out);
> -
> -	if (finish_command(&cmd)) {
> -		ret = -1;
> -		goto out;
> -	}
> -
> -	ret = 0;
> -
> -out:
> -	child_process_clear(&cmd);
> -	return ret;
> +	get_shallow_commits(&data->want_obj, &data->shallows,
> +			    &data->deepen_relative, 0,
> +			    SHALLOW, NOT_SHALLOW);
>  }
>  
>  static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)
> @@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)
>  			struct object *object = data->shallows.objects[i].item;
>  			object->flags |= NOT_SHALLOW;
>  		}
> -	} else if (data->deepen_relative) {
> -		struct object_array reachable_shallows = OBJECT_ARRAY_INIT;
> -		struct commit_list *result;
> -
> -		/*
> -		 * Checking for reachable shallows requires that our refs be
> -		 * marked with OUR_REF.
> -		 */
> -		refs_head_ref_namespaced(get_main_ref_store(the_repository),
> -					 check_ref, data);
> -		for_each_namespaced_ref_1(check_ref, data);
> -
> -		get_reachable_list(data, &reachable_shallows);
> -		result = get_shallow_commits(&reachable_shallows,
> -					     depth + 1,
> -					     SHALLOW, NOT_SHALLOW);
> -		send_shallow(data, result);
> -		free_commit_list(result);
> -		object_array_clear(&reachable_shallows);
>  	} else {
>  		struct commit_list *result;
>  
> -		result = get_shallow_commits(&data->want_obj, depth,
> +		if (data->deepen_relative)
> +			get_shallows_depth(data);
> +
> +		result = get_shallow_commits(&data->want_obj, NULL, NULL,
> +					     data->deepen_relative + depth,
>  					     SHALLOW, NOT_SHALLOW);
>  		send_shallow(data, result);
>  		free_commit_list(result);
Samo Pogačnik via GitGitGadget· Jan 10, 2026, 05:13 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v3 0/2] shallow: handling fetch relative-deepen

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may endup with some shallows, that are behind the reachable ones. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can allways summ 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening.

Samo Pogačnik (2):
  shallow: free local object_array allocations
  shallow: handling fetch relative-deepen
 shallow.c             | 45 +++++++++++++++++--------
 shallow.h             |  1 +
 t/t5500-fetch-pack.sh | 23 +++++++++++++
 upload-pack.c         | 76 +++++--------------------------------------
 4 files changed, 64 insertions(+), 81 deletions(-)
base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v3
Pull-Request: https://github.com/git/git/pull/2121
Range-diff vs v2:
 1:  f8a8d077cd = 1:  f8a8d077cd shallow: free local object_array allocations
 2:  ba1f80105f ! 2:  e79ab6b740 shallow: handling fetch relative-deepen
     @@ Commit message
      
          An example showing the problem based on added test:
          0. Whole initial git repo to be cloned from
     -    Graph:
     -    *   033585d (HEAD -> main) Merge branch 'branch'
     -    |\
     -    | * 984f8b1 (branch) five
     -    | * ecb578a four
     -    |/
     -    * 0cb5d20 three
     -    * 2b4e70d two
     -    * 61ba98b one
     +       Graph:
     +       *   033585d (HEAD -> main) Merge branch 'branch'
     +       |\
     +       | * 984f8b1 (branch) five
     +       | * ecb578a four
     +       |/
     +       * 0cb5d20 three
     +       * 2b4e70d two
     +       * 61ba98b one
      
          1. Initial shallow clone --depth=3 (all good)
     -    Shallows:
     -    2b4e70da2a10e1d3231a0ae2df396024735601f1
     -    ecb578a3cf37198d122ae5df7efed9abaca17144
     -    Graph:
     -    *   033585d (HEAD -> main) Merge branch 'branch'
     -    |\
     -    | * 984f8b1 five
     -    | * ecb578a (grafted) four
     -    * 0cb5d20 three
     -    * 2b4e70d (grafted) two
     +       Shallows:
     +       2b4e70da2a10e1d3231a0ae2df396024735601f1
     +       ecb578a3cf37198d122ae5df7efed9abaca17144
     +       Graph:
     +       *   033585d (HEAD -> main) Merge branch 'branch'
     +       |\
     +       | * 984f8b1 five
     +       | * ecb578a (grafted) four
     +       * 0cb5d20 three
     +       * 2b4e70d (grafted) two
      
          2. Deepen shallow clone with fetch --deepen=1 (NOT OK)
     -    Shallows:
     -    0cb5d204f4ef96ed241feb0f2088c9f4794ba758
     -    61ba98be443fd51c542eb66585a1f6d7e15fcdae
     -    Graph:
     -    *   033585d (HEAD -> main) Merge branch 'branch'
     -    |\
     -    | * 984f8b1 five
     -    | * ecb578a four
     -    |/
     -    * 0cb5d20 (grafted) three
     -    ---
     -    Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae
     -    is not reachable.
     +       Shallows:
     +       0cb5d204f4ef96ed241feb0f2088c9f4794ba758
     +       61ba98be443fd51c542eb66585a1f6d7e15fcdae
     +       Graph:
     +       *   033585d (HEAD -> main) Merge branch 'branch'
     +       |\
     +       | * 984f8b1 five
     +       | * ecb578a four
     +       |/
     +       * 0cb5d20 (grafted) three
     +       ---
     +       Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae
     +       is not reachable.
      
          On the other hand, it seems that equivalent absolute depth driven
          fetches result in all the correct shallows. That led to this proposal,
     @@ Commit message
      
          Same example showing the corrected second step:
          2. Deepen shallow clone with fetch --deepen=1 (all good)
     -    Shallow:
     -    61ba98be443fd51c542eb66585a1f6d7e15fcdae
     -    Graph:
     -    *   033585d (HEAD -> main) Merge branch 'branch'
     -    |\
     -    | * 984f8b1 five
     -    | * ecb578a four
     -    |/
     -    * 0cb5d20 three
     -    * 2b4e70d two
     -    * 61ba98b (grafted) one
     +       Shallow:
     +       61ba98be443fd51c542eb66585a1f6d7e15fcdae
     +       Graph:
     +       *   033585d (HEAD -> main) Merge branch 'branch'
     +       |\
     +       | * 984f8b1 five
     +       | * ecb578a four
     +       |/
     +       * 0cb5d20 three
     +       * 2b4e70d two
     +       * 61ba98b (grafted) one
      
          The get_shallows_depth() function also shares the logic of the
          get_shallow_commits() function, but it focuses on counting depth of
-- 
gitgitgadget
Samo Pogačnik via GitGitGadget· Jan 10, 2026, 05:13 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v3 1/2] shallow: free local object_array allocations

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

The local object_array 'stack' in get_shallow_commits() function does not free its dynamic elements before the function returns. As a result elements remain allocated and their reference forgotten.

Also note, that test 'fetching deepen beyond merged branch' added by 'shallow: handling fetch relative-deepen' patch fails without this correction in linux-leaks and linux-reftable-leaks test runs.

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
---
 shallow.c | 1 +
 1 file changed, 1 insertion(+)
Show changes to shallow.c +1 −0
diff --git a/shallow.c b/shallow.c
index 55b9cd9d3f..497a25836b 100644
--- a/shallow.c
+++ b/shallow.c
@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 		}
 	}
 	deep_clear_commit_depth(&depths, free_depth_in_slab);
+	object_array_clear(&stack);
 
 	return result;
 }
-- 
gitgitgadget
Samo Pogačnik via GitGitGadget· Jan 10, 2026, 05:13 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v3 2/2] shallow: handling fetch relative-deepen

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may end up with some shallows that are hidden behind the reachable shallow commits. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

An example showing the problem based on added test:
0. Whole initial git repo to be cloned from
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 (branch) five
   | * ecb578a four
   |/
   * 0cb5d20 three
   * 2b4e70d two
   * 61ba98b one
1. Initial shallow clone --depth=3 (all good)
   Shallows:
   2b4e70da2a10e1d3231a0ae2df396024735601f1
   ecb578a3cf37198d122ae5df7efed9abaca17144
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a (grafted) four
   * 0cb5d20 three
   * 2b4e70d (grafted) two
2. Deepen shallow clone with fetch --deepen=1 (NOT OK)
   Shallows:
   0cb5d204f4ef96ed241feb0f2088c9f4794ba758
   61ba98be443fd51c542eb66585a1f6d7e15fcdae
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a four
   |/
   * 0cb5d20 (grafted) three
   ---
   Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae
   is not reachable.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

Same example showing the corrected second step:
2. Deepen shallow clone with fetch --deepen=1 (all good)
   Shallow:
   61ba98be443fd51c542eb66585a1f6d7e15fcdae
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a four
   |/
   * 0cb5d20 three
   * 2b4e70d two
   * 61ba98b (grafted) one

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can allways summ 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening.

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
---
 shallow.c             | 44 +++++++++++++++++--------
 shallow.h             |  1 +
 t/t5500-fetch-pack.sh | 23 +++++++++++++
 upload-pack.c         | 76 +++++--------------------------------------
 4 files changed, 63 insertions(+), 81 deletions(-)
Show changes to 4 files +63 −81

shallow.c, shallow.h, t/t5500-fetch-pack.sh, upload-pack.c

diff --git a/shallow.c b/shallow.c
index 497a25836b..1a32808865 100644
--- a/shallow.c
+++ b/shallow.c
@@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)
 {
 	FREE_AND_NULL(*ptr);
 }
-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
-		int shallow_flag, int not_shallow_flag)
+struct commit_list *get_shallow_commits(struct object_array *heads,
+					struct object_array *shallows, int *deepen_relative,
+					int depth, int shallow_flag, int not_shallow_flag)
 {
-	size_t i = 0;
-	int cur_depth = 0;
+	size_t i = 0, j;
+	int cur_depth = 0, cur_depth_shallow = 0;
 	struct commit_list *result = NULL;
 	struct object_array stack = OBJECT_ARRAY_INIT;
 	struct commit *commit = NULL;
@@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 		}
 		parse_commit_or_die(commit);
 		cur_depth++;
-		if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
-		    (is_repository_shallow(the_repository) && !commit->parents &&
-		     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
-		     graft->nr_parent < 0)) {
-			commit_list_insert(commit, &result);
-			commit->object.flags |= shallow_flag;
-			commit = NULL;
-			continue;
+		if (shallows) {
+			for (j = 0; j < shallows->nr; j++)
+				if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
+					if ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))
+						cur_depth_shallow = cur_depth;
+
+			if ((is_repository_shallow(the_repository) && !commit->parents &&
+			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
+			     graft->nr_parent < 0)) {
+				commit = NULL;
+				continue;
+			}
+		} else {
+			if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
+			    (is_repository_shallow(the_repository) && !commit->parents &&
+			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
+			     graft->nr_parent < 0)) {
+				commit_list_insert(commit, &result);
+				commit->object.flags |= shallow_flag;
+				commit = NULL;
+				continue;
+			}
+			commit->object.flags |= not_shallow_flag;
 		}
-		commit->object.flags |= not_shallow_flag;
 		for (p = commit->parents, commit = NULL; p; p = p->next) {
 			int **depth_slot = commit_depth_at(&depths, p->item);
 			if (!*depth_slot) {
@@ -199,7 +214,8 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 	}
 	deep_clear_commit_depth(&depths, free_depth_in_slab);
 	object_array_clear(&stack);
-
+	if (shallows && deepen_relative)
+		*deepen_relative = cur_depth_shallow;
 	return result;
 }
 
diff --git a/shallow.h b/shallow.h
index ad591bd139..d1b3878635 100644
--- a/shallow.h
+++ b/shallow.h
@@ -36,6 +36,7 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);
 void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);
 
 struct commit_list *get_shallow_commits(struct object_array *heads,
+					struct object_array *shallows, int *deepen_relative,
 					int depth, int shallow_flag, int not_shallow_flag);
 struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,
 						    int shallow_flag, int not_shallow_flag);
diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
index 2677cd5faa..5a8b30e1fd 100755
--- a/t/t5500-fetch-pack.sh
+++ b/t/t5500-fetch-pack.sh
@@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '
 	)
 '
 
+test_expect_success 'fetching deepen beyond merged branch' '
+	test_create_repo shallow-deepen-merged &&
+	(
+		cd shallow-deepen-merged &&
+		git commit --allow-empty -m one &&
+		git commit --allow-empty -m two &&
+		git commit --allow-empty -m three &&
+		git switch -c branch &&
+		git commit --allow-empty -m four &&
+		git commit --allow-empty -m five &&
+		git switch main &&
+		git merge --no-ff branch &&
+		cd - &&
+		git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-merged" deepen.git &&
+		git -C deepen.git fetch origin --deepen=1 &&
+		git -C deepen.git rev-list --all >actual &&
+		for commit in $(sed "/^$/d" deepen.git/shallow)
+		do
+			test_grep "$commit" actual || exit 1
+		done
+	)
+'
+
 test_negotiation_algorithm_default () {
 	test_when_finished rm -rf clientv0 clientv2 &&
 	rm -rf server client &&
diff --git a/upload-pack.c b/upload-pack.c
index 2d2b70cbf2..4232eef34f 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -704,54 +704,11 @@ error:
 	return -1;
 }
 
-static int get_reachable_list(struct upload_pack_data *data,
-			      struct object_array *reachable)
+static void get_shallows_depth(struct upload_pack_data *data)
 {
-	struct child_process cmd = CHILD_PROCESS_INIT;
-	int i;
-	struct object *o;
-	char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
-	const unsigned hexsz = the_hash_algo->hexsz;
-	int ret;
-
-	if (do_reachable_revlist(&cmd, &data->shallows, reachable,
-				 data->allow_uor) < 0) {
-		ret = -1;
-		goto out;
-	}
-
-	while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {
-		struct object_id oid;
-		const char *p;
-
-		if (parse_oid_hex(namebuf, &oid, &p) || *p != '\n')
-			break;
-
-		o = lookup_object(the_repository, &oid);
-		if (o && o->type == OBJ_COMMIT) {
-			o->flags &= ~TMP_MARK;
-		}
-	}
-	for (i = get_max_object_index(the_repository); 0 < i; i--) {
-		o = get_indexed_object(the_repository, i - 1);
-		if (o && o->type == OBJ_COMMIT &&
-		    (o->flags & TMP_MARK)) {
-			add_object_array(o, NULL, reachable);
-				o->flags &= ~TMP_MARK;
-		}
-	}
-	close(cmd.out);
-
-	if (finish_command(&cmd)) {
-		ret = -1;
-		goto out;
-	}
-
-	ret = 0;
-
-out:
-	child_process_clear(&cmd);
-	return ret;
+	get_shallow_commits(&data->want_obj, &data->shallows,
+			    &data->deepen_relative, 0,
+			    SHALLOW, NOT_SHALLOW);
 }
 
 static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)
@@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)
 			struct object *object = data->shallows.objects[i].item;
 			object->flags |= NOT_SHALLOW;
 		}
-	} else if (data->deepen_relative) {
-		struct object_array reachable_shallows = OBJECT_ARRAY_INIT;
-		struct commit_list *result;
-
-		/*
-		 * Checking for reachable shallows requires that our refs be
-		 * marked with OUR_REF.
-		 */
-		refs_head_ref_namespaced(get_main_ref_store(the_repository),
-					 check_ref, data);
-		for_each_namespaced_ref_1(check_ref, data);
-
-		get_reachable_list(data, &reachable_shallows);
-		result = get_shallow_commits(&reachable_shallows,
-					     depth + 1,
-					     SHALLOW, NOT_SHALLOW);
-		send_shallow(data, result);
-		free_commit_list(result);
-		object_array_clear(&reachable_shallows);
 	} else {
 		struct commit_list *result;
 
-		result = get_shallow_commits(&data->want_obj, depth,
+		if (data->deepen_relative)
+			get_shallows_depth(data);
+
+		result = get_shallow_commits(&data->want_obj, NULL, NULL,
+					     data->deepen_relative + depth,
 					     SHALLOW, NOT_SHALLOW);
 		send_shallow(data, result);
 		free_commit_list(result);
-- 
gitgitgadget
Kristoffer Haugsbakk· Jan 15, 2026, 15:50 UTC · re: Samo Pogačnik via GitGitGadget · lore

Re: [PATCH v3 2/2] shallow: handling fetch relative-deepen

On Sat, Jan 10, 2026, at 06:13, Samo Pogačnik via GitGitGadget wrote:
Show 28 quoted lines
> When a shallowed repository gets deepened beyond the beginning of a
> merged branch, we may end up with some shallows that are hidden behind
> the reachable shallow commits. Added test 'fetching deepen beyond
> merged branch' exposes that behaviour.
>
> An example showing the problem based on added test:
>[snip]
>    ---
>    Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae
>    is not reachable.
>
> On the other hand, it seems that equivalent absolute depth driven
> fetches result in all the correct shallows. That led to this proposal,
> which unifies absolute and relative deepening in a way that the same
> get_shallow_commits() call is used in both cases. The difference is
> only that depth is adapted for relative deepening by measuring
> equivalent depth of current local shallow commits in the current remote
> repo. Thus a new function get_shallows_depth() has been added and the
> function get_reachable_list() became redundant / removed.
>
> Same example showing the corrected second step:
>[snip]
>
> The get_shallows_depth() function also shares the logic of the
> get_shallow_commits() function, but it focuses on counting depth of
> each existing shallow commit. The minimum result is stored as
> 'data->deepen_relative', which is set not to be zero for relative
> deepening anyway. That way we can allways summ 'data->deepen_relative'
s/allways summ/always sum/ ?
Show 6 quoted lines
> and 'depth' values, because 'data->deepen_relative' is always 0 in
> absolute deepening.
>
> Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
> ---
>[snip]
Samo Pogačnik via GitGitGadget· Jan 16, 2026, 22:30 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v4 0/2] shallow: handling fetch relative-deepen

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may endup with some shallows, that are behind the reachable ones. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can allways summ 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening.

Samo Pogačnik (2):
  shallow: free local object_array allocations
  shallow: handling fetch relative-deepen
 shallow.c             | 45 +++++++++++++++++--------
 shallow.h             |  1 +
 t/t5500-fetch-pack.sh | 23 +++++++++++++
 upload-pack.c         | 76 +++++--------------------------------------
 4 files changed, 64 insertions(+), 81 deletions(-)
base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v4
Pull-Request: https://github.com/git/git/pull/2121
Range-diff vs v3:
 1:  f8a8d077cd = 1:  f8a8d077cd shallow: free local object_array allocations
 2:  e79ab6b740 ! 2:  e9b20ae06f shallow: handling fetch relative-deepen
     @@ Commit message
          get_shallow_commits() function, but it focuses on counting depth of
          each existing shallow commit. The minimum result is stored as
          'data->deepen_relative', which is set not to be zero for relative
     -    deepening anyway. That way we can allways summ 'data->deepen_relative'
     +    deepening anyway. That way we can always sum 'data->deepen_relative'
          and 'depth' values, because 'data->deepen_relative' is always 0 in
          absolute deepening.
     +    To avoid duplicating logic between get_shallows_depth() and
     +    get_shallow_commits(), get_shallow_commits() was modified so that
     +    it is used by get_shallows_depth().
      
          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
      
-- 
gitgitgadget
Samo Pogačnik via GitGitGadget· Jan 16, 2026, 22:31 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v4 1/2] shallow: free local object_array allocations

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

The local object_array 'stack' in get_shallow_commits() function does not free its dynamic elements before the function returns. As a result elements remain allocated and their reference forgotten.

Also note, that test 'fetching deepen beyond merged branch' added by 'shallow: handling fetch relative-deepen' patch fails without this correction in linux-leaks and linux-reftable-leaks test runs.

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
---
 shallow.c | 1 +
 1 file changed, 1 insertion(+)
Show changes to shallow.c +1 −0
diff --git a/shallow.c b/shallow.c
index 55b9cd9d3f..497a25836b 100644
--- a/shallow.c
+++ b/shallow.c
@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 		}
 	}
 	deep_clear_commit_depth(&depths, free_depth_in_slab);
+	object_array_clear(&stack);
 
 	return result;
 }
-- 
gitgitgadget
Samo Pogačnik via GitGitGadget· Jan 16, 2026, 22:31 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v4 2/2] shallow: handling fetch relative-deepen

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may end up with some shallows that are hidden behind the reachable shallow commits. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

An example showing the problem based on added test:
0. Whole initial git repo to be cloned from
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 (branch) five
   | * ecb578a four
   |/
   * 0cb5d20 three
   * 2b4e70d two
   * 61ba98b one
1. Initial shallow clone --depth=3 (all good)
   Shallows:
   2b4e70da2a10e1d3231a0ae2df396024735601f1
   ecb578a3cf37198d122ae5df7efed9abaca17144
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a (grafted) four
   * 0cb5d20 three
   * 2b4e70d (grafted) two
2. Deepen shallow clone with fetch --deepen=1 (NOT OK)
   Shallows:
   0cb5d204f4ef96ed241feb0f2088c9f4794ba758
   61ba98be443fd51c542eb66585a1f6d7e15fcdae
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a four
   |/
   * 0cb5d20 (grafted) three
   ---
   Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae
   is not reachable.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

Same example showing the corrected second step:
2. Deepen shallow clone with fetch --deepen=1 (all good)
   Shallow:
   61ba98be443fd51c542eb66585a1f6d7e15fcdae
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a four
   |/
   * 0cb5d20 three
   * 2b4e70d two
   * 61ba98b (grafted) one

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can always sum 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening. To avoid duplicating logic between get_shallows_depth() and get_shallow_commits(), get_shallow_commits() was modified so that it is used by get_shallows_depth().

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
---
 shallow.c             | 44 +++++++++++++++++--------
 shallow.h             |  1 +
 t/t5500-fetch-pack.sh | 23 +++++++++++++
 upload-pack.c         | 76 +++++--------------------------------------
 4 files changed, 63 insertions(+), 81 deletions(-)
Show changes to 4 files +63 −81

shallow.c, shallow.h, t/t5500-fetch-pack.sh, upload-pack.c

diff --git a/shallow.c b/shallow.c
index 497a25836b..1a32808865 100644
--- a/shallow.c
+++ b/shallow.c
@@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)
 {
 	FREE_AND_NULL(*ptr);
 }
-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
-		int shallow_flag, int not_shallow_flag)
+struct commit_list *get_shallow_commits(struct object_array *heads,
+					struct object_array *shallows, int *deepen_relative,
+					int depth, int shallow_flag, int not_shallow_flag)
 {
-	size_t i = 0;
-	int cur_depth = 0;
+	size_t i = 0, j;
+	int cur_depth = 0, cur_depth_shallow = 0;
 	struct commit_list *result = NULL;
 	struct object_array stack = OBJECT_ARRAY_INIT;
 	struct commit *commit = NULL;
@@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 		}
 		parse_commit_or_die(commit);
 		cur_depth++;
-		if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
-		    (is_repository_shallow(the_repository) && !commit->parents &&
-		     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
-		     graft->nr_parent < 0)) {
-			commit_list_insert(commit, &result);
-			commit->object.flags |= shallow_flag;
-			commit = NULL;
-			continue;
+		if (shallows) {
+			for (j = 0; j < shallows->nr; j++)
+				if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
+					if ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))
+						cur_depth_shallow = cur_depth;
+
+			if ((is_repository_shallow(the_repository) && !commit->parents &&
+			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
+			     graft->nr_parent < 0)) {
+				commit = NULL;
+				continue;
+			}
+		} else {
+			if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
+			    (is_repository_shallow(the_repository) && !commit->parents &&
+			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
+			     graft->nr_parent < 0)) {
+				commit_list_insert(commit, &result);
+				commit->object.flags |= shallow_flag;
+				commit = NULL;
+				continue;
+			}
+			commit->object.flags |= not_shallow_flag;
 		}
-		commit->object.flags |= not_shallow_flag;
 		for (p = commit->parents, commit = NULL; p; p = p->next) {
 			int **depth_slot = commit_depth_at(&depths, p->item);
 			if (!*depth_slot) {
@@ -199,7 +214,8 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 	}
 	deep_clear_commit_depth(&depths, free_depth_in_slab);
 	object_array_clear(&stack);
-
+	if (shallows && deepen_relative)
+		*deepen_relative = cur_depth_shallow;
 	return result;
 }
 
diff --git a/shallow.h b/shallow.h
index ad591bd139..d1b3878635 100644
--- a/shallow.h
+++ b/shallow.h
@@ -36,6 +36,7 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);
 void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);
 
 struct commit_list *get_shallow_commits(struct object_array *heads,
+					struct object_array *shallows, int *deepen_relative,
 					int depth, int shallow_flag, int not_shallow_flag);
 struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,
 						    int shallow_flag, int not_shallow_flag);
diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
index 2677cd5faa..5a8b30e1fd 100755
--- a/t/t5500-fetch-pack.sh
+++ b/t/t5500-fetch-pack.sh
@@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '
 	)
 '
 
+test_expect_success 'fetching deepen beyond merged branch' '
+	test_create_repo shallow-deepen-merged &&
+	(
+		cd shallow-deepen-merged &&
+		git commit --allow-empty -m one &&
+		git commit --allow-empty -m two &&
+		git commit --allow-empty -m three &&
+		git switch -c branch &&
+		git commit --allow-empty -m four &&
+		git commit --allow-empty -m five &&
+		git switch main &&
+		git merge --no-ff branch &&
+		cd - &&
+		git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-merged" deepen.git &&
+		git -C deepen.git fetch origin --deepen=1 &&
+		git -C deepen.git rev-list --all >actual &&
+		for commit in $(sed "/^$/d" deepen.git/shallow)
+		do
+			test_grep "$commit" actual || exit 1
+		done
+	)
+'
+
 test_negotiation_algorithm_default () {
 	test_when_finished rm -rf clientv0 clientv2 &&
 	rm -rf server client &&
diff --git a/upload-pack.c b/upload-pack.c
index 2d2b70cbf2..4232eef34f 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -704,54 +704,11 @@ error:
 	return -1;
 }
 
-static int get_reachable_list(struct upload_pack_data *data,
-			      struct object_array *reachable)
+static void get_shallows_depth(struct upload_pack_data *data)
 {
-	struct child_process cmd = CHILD_PROCESS_INIT;
-	int i;
-	struct object *o;
-	char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
-	const unsigned hexsz = the_hash_algo->hexsz;
-	int ret;
-
-	if (do_reachable_revlist(&cmd, &data->shallows, reachable,
-				 data->allow_uor) < 0) {
-		ret = -1;
-		goto out;
-	}
-
-	while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {
-		struct object_id oid;
-		const char *p;
-
-		if (parse_oid_hex(namebuf, &oid, &p) || *p != '\n')
-			break;
-
-		o = lookup_object(the_repository, &oid);
-		if (o && o->type == OBJ_COMMIT) {
-			o->flags &= ~TMP_MARK;
-		}
-	}
-	for (i = get_max_object_index(the_repository); 0 < i; i--) {
-		o = get_indexed_object(the_repository, i - 1);
-		if (o && o->type == OBJ_COMMIT &&
-		    (o->flags & TMP_MARK)) {
-			add_object_array(o, NULL, reachable);
-				o->flags &= ~TMP_MARK;
-		}
-	}
-	close(cmd.out);
-
-	if (finish_command(&cmd)) {
-		ret = -1;
-		goto out;
-	}
-
-	ret = 0;
-
-out:
-	child_process_clear(&cmd);
-	return ret;
+	get_shallow_commits(&data->want_obj, &data->shallows,
+			    &data->deepen_relative, 0,
+			    SHALLOW, NOT_SHALLOW);
 }
 
 static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)
@@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)
 			struct object *object = data->shallows.objects[i].item;
 			object->flags |= NOT_SHALLOW;
 		}
-	} else if (data->deepen_relative) {
-		struct object_array reachable_shallows = OBJECT_ARRAY_INIT;
-		struct commit_list *result;
-
-		/*
-		 * Checking for reachable shallows requires that our refs be
-		 * marked with OUR_REF.
-		 */
-		refs_head_ref_namespaced(get_main_ref_store(the_repository),
-					 check_ref, data);
-		for_each_namespaced_ref_1(check_ref, data);
-
-		get_reachable_list(data, &reachable_shallows);
-		result = get_shallow_commits(&reachable_shallows,
-					     depth + 1,
-					     SHALLOW, NOT_SHALLOW);
-		send_shallow(data, result);
-		free_commit_list(result);
-		object_array_clear(&reachable_shallows);
 	} else {
 		struct commit_list *result;
 
-		result = get_shallow_commits(&data->want_obj, depth,
+		if (data->deepen_relative)
+			get_shallows_depth(data);
+
+		result = get_shallow_commits(&data->want_obj, NULL, NULL,
+					     data->deepen_relative + depth,
 					     SHALLOW, NOT_SHALLOW);
 		send_shallow(data, result);
 		free_commit_list(result);
-- 
gitgitgadget
Patrick Steinhardt· Feb 11, 2026, 13:38 UTC · re: Samo Pogačnik via GitGitGadget · lore

Re: [PATCH v4 2/2] shallow: handling fetch relative-deepen

On Fri, Jan 16, 2026 at 10:31:01PM +0000, Samo Pogačnik via GitGitGadget wrote:
Show 17 quoted lines
> diff --git a/shallow.c b/shallow.c
> index 497a25836b..1a32808865 100644
> --- a/shallow.c
> +++ b/shallow.c
> @@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)
>  {
>  	FREE_AND_NULL(*ptr);
>  }
> -struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
> -		int shallow_flag, int not_shallow_flag)
> +struct commit_list *get_shallow_commits(struct object_array *heads,
> +					struct object_array *shallows, int *deepen_relative,
> +					int depth, int shallow_flag, int not_shallow_flag)
>  {
> -	size_t i = 0;
> -	int cur_depth = 0;
> +	size_t i = 0, j;
We can declare `j` in the loop itself, as it's not used anywhere else.
Show 16 quoted lines
> @@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
>  		}
>  		parse_commit_or_die(commit);
>  		cur_depth++;
> -		if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
> -		    (is_repository_shallow(the_repository) && !commit->parents &&
> -		     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
> -		     graft->nr_parent < 0)) {
> -			commit_list_insert(commit, &result);
> -			commit->object.flags |= shallow_flag;
> -			commit = NULL;
> -			continue;
> +		if (shallows) {
> +			for (j = 0; j < shallows->nr; j++)
> +				if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
> +					if ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))
The additional braces around the respective conditions are not needed.
Show 8 quoted lines
> +						cur_depth_shallow = cur_depth;
> +
> +			if ((is_repository_shallow(the_repository) && !commit->parents &&
> +			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
> +			     graft->nr_parent < 0)) {
> +				commit = NULL;
> +				continue;
> +			}

This block here is almost the same as the one below. But there's some confusing parts:

  - Why don't we update `result` at all?
  - Why don't we set the `shallow_flag`?
  - Why don't we have to check for the passed-in depth?

All of these parts feel somewhat surprising to me, as the function now behaves so wildly different depending on whether or not `shallows` was passed.

I guess this is because we really only care about `cur_depth_shallow`?
Show 11 quoted lines
> diff --git a/upload-pack.c b/upload-pack.c
> index 2d2b70cbf2..4232eef34f 100644
> --- a/upload-pack.c
> +++ b/upload-pack.c
> @@ -704,54 +704,11 @@ error:
>  	return -1;
>  }
>  
> -static int get_reachable_list(struct upload_pack_data *data,
> -			      struct object_array *reachable)
> +static void get_shallows_depth(struct upload_pack_data *data)

I think this function is rather pointless, as there is only a single caller and we only end up forwarding to `get_shallow_commits()`. Let's inline it.

Show 29 quoted lines
> @@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)
>  			struct object *object = data->shallows.objects[i].item;
>  			object->flags |= NOT_SHALLOW;
>  		}
> -	} else if (data->deepen_relative) {
> -		struct object_array reachable_shallows = OBJECT_ARRAY_INIT;
> -		struct commit_list *result;
> -
> -		/*
> -		 * Checking for reachable shallows requires that our refs be
> -		 * marked with OUR_REF.
> -		 */
> -		refs_head_ref_namespaced(get_main_ref_store(the_repository),
> -					 check_ref, data);
> -		for_each_namespaced_ref_1(check_ref, data);
> -
> -		get_reachable_list(data, &reachable_shallows);
> -		result = get_shallow_commits(&reachable_shallows,
> -					     depth + 1,
> -					     SHALLOW, NOT_SHALLOW);
> -		send_shallow(data, result);
> -		free_commit_list(result);
> -		object_array_clear(&reachable_shallows);
>  	} else {
>  		struct commit_list *result;
>  
> -		result = get_shallow_commits(&data->want_obj, depth,
> +		if (data->deepen_relative)
> +			get_shallows_depth(data);

Okay, so here we now essentially call `get_shallow_commits()` twice. The first time we compute `data->deepen_relative`, only to then pass it back to `get_shallow_commits()` a second time. That feels quite strange to me. Can't we have `get_shallow_commits()` handle this for us directly in a single call?

Show 5 quoted lines
> +		result = get_shallow_commits(&data->want_obj, NULL, NULL,
> +					     data->deepen_relative + depth,
>  					     SHALLOW, NOT_SHALLOW);
>  		send_shallow(data, result);
>  		free_commit_list(result);
Thanks!
Patrick
Samo Pogačnik· Feb 13, 2026, 20:48 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v4 2/2] shallow: handling fetch relative-deepen

On Wed, 2026-02-11 at 14:38 +0100, Patrick Steinhardt wrote:
Show 24 quoted lines
> On Fri, Jan 16, 2026 at 10:31:01PM +0000, Samo Pogačnik via GitGitGadget
> wrote:
> > diff --git a/shallow.c b/shallow.c
> > index 497a25836b..1a32808865 100644
> > --- a/shallow.c
> > +++ b/shallow.c
> > @@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)
> >  {
> >  	FREE_AND_NULL(*ptr);
> >  }
> > -struct commit_list *get_shallow_commits(struct object_array *heads, int
> > depth,
> > -		int shallow_flag, int not_shallow_flag)
> > +struct commit_list *get_shallow_commits(struct object_array *heads,
> > +					struct object_array *shallows, int
> > *deepen_relative,
> > +					int depth, int shallow_flag, int
> > not_shallow_flag)
> >  {
> > -	size_t i = 0;
> > -	int cur_depth = 0;
> > +	size_t i = 0, j;
> 
> We can declare `j` in the loop itself, as it's not used anywhere else.
Yes, sure.
Show 24 quoted lines
> 
> > @@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct
> > object_array *heads, int depth,
> >  		}
> >  		parse_commit_or_die(commit);
> >  		cur_depth++;
> > -		if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
> > -		    (is_repository_shallow(the_repository) && !commit-
> > >parents &&
> > -		     (graft = lookup_commit_graft(the_repository, &commit-
> > >object.oid)) != NULL &&
> > -		     graft->nr_parent < 0)) {
> > -			commit_list_insert(commit, &result);
> > -			commit->object.flags |= shallow_flag;
> > -			commit = NULL;
> > -			continue;
> > +		if (shallows) {
> > +			for (j = 0; j < shallows->nr; j++)
> > +				if (oideq(&commit->object.oid, &shallows-
> > >objects[j].item->oid))
> > +					if ((!cur_depth_shallow) ||
> > (cur_depth < cur_depth_shallow))
> 
> The additional braces around the respective conditions are not needed.
Of course.
Show 27 quoted lines
> 
> > +						cur_depth_shallow =
> > cur_depth;
> > +
> > +			if ((is_repository_shallow(the_repository) &&
> > !commit->parents &&
> > +			     (graft = lookup_commit_graft(the_repository,
> > &commit->object.oid)) != NULL &&
> > +			     graft->nr_parent < 0)) {
> > +				commit = NULL;
> > +				continue;
> > +			}
> 
> This block here is almost the same as the one below. But there's some
> confusing parts:
> 
>   - Why don't we update `result` at all?
> 
>   - Why don't we set the `shallow_flag`?
> 
>   - Why don't we have to check for the passed-in depth?
> 
> All of these parts feel somewhat surprising to me, as the function now
> behaves so wildly different depending on whether or not `shallows` was
> passed.
> 
> I guess this is because we really only care about `cur_depth_shallow`?

Exactly, i merged the two functions with almost the same algorithm producing different results depending on additional input parameter shallows. When shallows passed only current maximum absolute depth is returned in the extra output parameter deepen_relative (if provided) and nothing else is done. The passed-in depth is not checked as it is irrelevant for this depth-measuring scenario.

Show 16 quoted lines
> 
> > diff --git a/upload-pack.c b/upload-pack.c
> > index 2d2b70cbf2..4232eef34f 100644
> > --- a/upload-pack.c
> > +++ b/upload-pack.c
> > @@ -704,54 +704,11 @@ error:
> >  	return -1;
> >  }
> >  
> > -static int get_reachable_list(struct upload_pack_data *data,
> > -			      struct object_array *reachable)
> > +static void get_shallows_depth(struct upload_pack_data *data)
> 
> I think this function is rather pointless, as there is only a single
> caller and we only end up forwarding to `get_shallow_commits()`. Let's
> inline it.

True, i suppose you ment inline the function code instead of function call and not making the function inline?

Show 40 quoted lines
> 
> > @@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int
> > depth)
> >  			struct object *object = data-
> > >shallows.objects[i].item;
> >  			object->flags |= NOT_SHALLOW;
> >  		}
> > -	} else if (data->deepen_relative) {
> > -		struct object_array reachable_shallows = OBJECT_ARRAY_INIT;
> > -		struct commit_list *result;
> > -
> > -		/*
> > -		 * Checking for reachable shallows requires that our refs
> > be
> > -		 * marked with OUR_REF.
> > -		 */
> > -
> > 		refs_head_ref_namespaced(get_main_ref_store(the_repository),
> > -					 check_ref, data);
> > -		for_each_namespaced_ref_1(check_ref, data);
> > -
> > -		get_reachable_list(data, &reachable_shallows);
> > -		result = get_shallow_commits(&reachable_shallows,
> > -					     depth + 1,
> > -					     SHALLOW, NOT_SHALLOW);
> > -		send_shallow(data, result);
> > -		free_commit_list(result);
> > -		object_array_clear(&reachable_shallows);
> >  	} else {
> >  		struct commit_list *result;
> >  
> > -		result = get_shallow_commits(&data->want_obj, depth,
> > +		if (data->deepen_relative)
> > +			get_shallows_depth(data);
> 
> Okay, so here we now essentially call `get_shallow_commits()` twice. The
> first time we compute `data->deepen_relative`, only to then pass it back
> to `get_shallow_commits()` a second time. That feels quite strange to
> me. Can't we have `get_shallow_commits()` handle this for us directly in
> a single call?

Nicely put, i just wasn't (and still am not) confident enough to change code in a way that would potentially affect any other scenarios than fetching relative- deepen.

Thank you very much for the review and i'll try to address all your review points in the next patch version.

Best regards, Samo
Samo Pogačnik· Feb 14, 2026, 09:40 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v4 2/2] shallow: handling fetch relative-deepen

On Wed, 2026-02-11 at 14:38 +0100, Patrick Steinhardt wrote:
Show 49 quoted lines
> On Fri, Jan 16, 2026 at 10:31:01PM +0000, Samo Pogačnik via GitGitGadget
> wrote:
> 
> > @@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int
> > depth)
> >  			struct object *object = data-
> > >shallows.objects[i].item;
> >  			object->flags |= NOT_SHALLOW;
> >  		}
> > -	} else if (data->deepen_relative) {
> > -		struct object_array reachable_shallows = OBJECT_ARRAY_INIT;
> > -		struct commit_list *result;
> > -
> > -		/*
> > -		 * Checking for reachable shallows requires that our refs
> > be
> > -		 * marked with OUR_REF.
> > -		 */
> > -
> > 		refs_head_ref_namespaced(get_main_ref_store(the_repository),
> > -					 check_ref, data);
> > -		for_each_namespaced_ref_1(check_ref, data);
> > -
> > -		get_reachable_list(data, &reachable_shallows);
> > -		result = get_shallow_commits(&reachable_shallows,
> > -					     depth + 1,
> > -					     SHALLOW, NOT_SHALLOW);
> > -		send_shallow(data, result);
> > -		free_commit_list(result);
> > -		object_array_clear(&reachable_shallows);
> >  	} else {
> >  		struct commit_list *result;
> >  
> > -		result = get_shallow_commits(&data->want_obj, depth,
> > +		if (data->deepen_relative)
> > +			get_shallows_depth(data);
> 
> Okay, so here we now essentially call `get_shallow_commits()` twice. The
> first time we compute `data->deepen_relative`, only to then pass it back
> to `get_shallow_commits()` a second time. That feels quite strange to
> me. Can't we have `get_shallow_commits()` handle this for us directly in
> a single call?
> 
> > +		result = get_shallow_commits(&data->want_obj, NULL, NULL,
> > +					     data->deepen_relative + depth,
> >  					     SHALLOW, NOT_SHALLOW);
> >  		send_shallow(data, result);
> >  		free_commit_list(result);
> 

I have additional dilemma regarding handling this in a single call. Wouldn't it be generally good/useful to have a separate function in shallow.c just for measuring current absolute depth instead of blending the measurement into get_shallow_commits()?

Thanks, Samo
Samo Pogačnik· Feb 15, 2026, 11:19 UTC · re: Samo Pogačnik · lore

Re: [PATCH v4 2/2] shallow: handling fetch relative-deepen

On Sat, 2026-02-14 at 10:40 +0100, Samo Pogačnik wrote:
Show 30 quoted lines
> On Wed, 2026-02-11 at 14:38 +0100, Patrick Steinhardt wrote:
> > On Fri, Jan 16, 2026 at 10:31:01PM +0000, Samo Pogačnik via GitGitGadget
> > wrote:
> > > 	object_array_clear(&reachable_shallows);
> > >  	} else {
> > >  		struct commit_list *result;
> > >  
> > > -		result = get_shallow_commits(&data->want_obj, depth,
> > > +		if (data->deepen_relative)
> > > +			get_shallows_depth(data);
> > 
> > Okay, so here we now essentially call `get_shallow_commits()` twice. The
> > first time we compute `data->deepen_relative`, only to then pass it back
> > to `get_shallow_commits()` a second time. That feels quite strange to
> > me. Can't we have `get_shallow_commits()` handle this for us directly in
> > a single call?
> > 
> > > +		result = get_shallow_commits(&data->want_obj, NULL, NULL,
> > > +					     data->deepen_relative +
> > > depth,
> > >  					     SHALLOW, NOT_SHALLOW);
> > >  		send_shallow(data, result);
> > >  		free_commit_list(result);
> > 
> 
> I have additional dilemma regarding handling this in a single call.
> Wouldn't it be generally good/useful to have a separate function in shallow.c
> just for measuring current absolute depth instead of blending the measurement
> into get_shallow_commits()?
> 

I prepared another version which i am going to post shortly. It does make a single call to 'get_shallow_commits()' in upload_pack.c, however in shallow.c there is again a common internal function, which is called once for measuring current depth and then again to get a list shallow commits. I do not know how to perform current depth measurement and shallows list retrieval with new depth extended by the same measurement simultaneously. However i find it important to keep common algorithm in a single function for easier maintenance. I hope this is ok.

Best regards, Samo
Samo Pogačnik via GitGitGadget· Feb 15, 2026, 20:11 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v5 0/2] shallow: handling fetch relative-deepen

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may endup with some shallows, that are behind the reachable ones. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can allways summ 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening.

Samo Pogačnik (2):
  shallow: free local object_array allocations
  shallow: handling fetch relative-deepen
 shallow.c             | 73 ++++++++++++++++++++++++++++++++++++-------
 shallow.h             |  2 ++
 t/t5500-fetch-pack.sh | 23 ++++++++++++++
 upload-pack.c         | 72 ++----------------------------------------
 4 files changed, 88 insertions(+), 82 deletions(-)
base-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v5
Pull-Request: https://github.com/git/git/pull/2121
Range-diff vs v4:
 1:  f8a8d077cd = 1:  f8a8d077cd shallow: free local object_array allocations
 2:  e9b20ae06f ! 2:  8d48ba9cd1 shallow: handling fetch relative-deepen
     @@ Commit message
      
          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
      
     +    Fixing v4
     +
     +    Fixing v4 again
     +
       ## shallow.c ##
      @@ shallow.c: static void free_depth_in_slab(int **ptr)
       {
     @@ shallow.c: static void free_depth_in_slab(int **ptr)
       }
      -struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
      -		int shallow_flag, int not_shallow_flag)
     -+struct commit_list *get_shallow_commits(struct object_array *heads,
     -+					struct object_array *shallows, int *deepen_relative,
     -+					int depth, int shallow_flag, int not_shallow_flag)
     ++/*
     ++ * This is a common internal function that can either return a list of
     ++ * shallow commits or calculate the current maximum depth of a shallow
     ++ * repository, depending on the input parameters.
     ++ *
     ++ * Depth calculation is triggered by passing the `shallows` parameter.
     ++ * In this case, the computed depth is stored in `max_cur_depth` (if it is
     ++ * provided), and the function returns NULL.
     ++ *
     ++ * Otherwise, `max_cur_depth` remains unchanged and the function returns
     ++ * a list of shallow commits.
     ++ */
     ++static struct commit_list *get_shallows_or_depth(struct object_array *heads,
     ++				struct object_array *shallows, int *max_cur_depth,
     ++				int depth, int shallow_flag, int not_shallow_flag)
       {
     --	size_t i = 0;
     + 	size_t i = 0;
      -	int cur_depth = 0;
     -+	size_t i = 0, j;
      +	int cur_depth = 0, cur_depth_shallow = 0;
       	struct commit_list *result = NULL;
       	struct object_array stack = OBJECT_ARRAY_INIT;
     @@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, i
      -			commit = NULL;
      -			continue;
      +		if (shallows) {
     -+			for (j = 0; j < shallows->nr; j++)
     ++			for (size_t j = 0; j < shallows->nr; j++)
      +				if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
     -+					if ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))
     ++					if (!cur_depth_shallow || cur_depth < cur_depth_shallow)
      +						cur_depth_shallow = cur_depth;
      +
      +			if ((is_repository_shallow(the_repository) && !commit->parents &&
     @@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, i
       			int **depth_slot = commit_depth_at(&depths, p->item);
       			if (!*depth_slot) {
      @@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
     - 	}
       	deep_clear_commit_depth(&depths, free_depth_in_slab);
       	object_array_clear(&stack);
     --
     -+	if (shallows && deepen_relative)
     -+		*deepen_relative = cur_depth_shallow;
     + 
     ++	if (shallows && max_cur_depth)
     ++		*max_cur_depth = cur_depth_shallow;
       	return result;
       }
       
     ++int get_shallows_depth(struct object_array *heads, struct object_array *shallows)
     ++{
     ++	int max_cur_depth = 0;
     ++	get_shallows_or_depth(heads, shallows, &max_cur_depth, 0, 0, 0);
     ++	return max_cur_depth;
     ++
     ++}
     ++
     ++struct commit_list *get_shallow_commits(struct object_array *heads,
     ++					struct object_array *shallows, int deepen_relative,
     ++					int depth, int shallow_flag, int not_shallow_flag)
     ++{
     ++	if (shallows && deepen_relative) {
     ++		depth += get_shallows_depth(heads, shallows);
     ++	}
     ++	return get_shallows_or_depth(heads, NULL, NULL,
     ++				     depth, shallow_flag, not_shallow_flag);
     ++}
     ++
     + static void show_commit(struct commit *commit, void *data)
     + {
     + 	commit_list_insert(commit, data);
      
       ## shallow.h ##
      @@ shallow.h: int commit_shallow_file(struct repository *r, struct shallow_lock *lk);
     + /* rollback $GIT_DIR/shallow and reset stat-validity checks */
       void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);
       
     ++int get_shallows_depth(struct object_array *heads, struct object_array *shallows);
       struct commit_list *get_shallow_commits(struct object_array *heads,
     -+					struct object_array *shallows, int *deepen_relative,
     ++					struct object_array *shallows, int deepen_relative,
       					int depth, int shallow_flag, int not_shallow_flag);
       struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,
       						    int shallow_flag, int not_shallow_flag);
     @@ upload-pack.c: error:
       
      -static int get_reachable_list(struct upload_pack_data *data,
      -			      struct object_array *reachable)
     -+static void get_shallows_depth(struct upload_pack_data *data)
     - {
     +-{
      -	struct child_process cmd = CHILD_PROCESS_INIT;
      -	int i;
      -	struct object *o;
     @@ upload-pack.c: error:
      -out:
      -	child_process_clear(&cmd);
      -	return ret;
     -+	get_shallow_commits(&data->want_obj, &data->shallows,
     -+			    &data->deepen_relative, 0,
     -+			    SHALLOW, NOT_SHALLOW);
     - }
     - 
     +-}
     +-
       static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)
     + {
     + 	struct child_process cmd = CHILD_PROCESS_INIT;
      @@ upload-pack.c: static void deepen(struct upload_pack_data *data, int depth)
       			struct object *object = data->shallows.objects[i].item;
       			object->flags |= NOT_SHALLOW;
     @@ upload-pack.c: static void deepen(struct upload_pack_data *data, int depth)
       		struct commit_list *result;
       
      -		result = get_shallow_commits(&data->want_obj, depth,
     -+		if (data->deepen_relative)
     -+			get_shallows_depth(data);
     -+
     -+		result = get_shallow_commits(&data->want_obj, NULL, NULL,
     -+					     data->deepen_relative + depth,
     ++		result = get_shallow_commits(&data->want_obj, &data->shallows,
     ++					     data->deepen_relative, depth,
       					     SHALLOW, NOT_SHALLOW);
       		send_shallow(data, result);
       		free_commit_list(result);
-- 
gitgitgadget
Samo Pogačnik via GitGitGadget· Feb 15, 2026, 20:11 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v5 1/2] shallow: free local object_array allocations

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

The local object_array 'stack' in get_shallow_commits() function does not free its dynamic elements before the function returns. As a result elements remain allocated and their reference forgotten.

Also note, that test 'fetching deepen beyond merged branch' added by 'shallow: handling fetch relative-deepen' patch fails without this correction in linux-leaks and linux-reftable-leaks test runs.

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
---
 shallow.c | 1 +
 1 file changed, 1 insertion(+)
Show changes to shallow.c +1 −0
diff --git a/shallow.c b/shallow.c
index 55b9cd9d3f..497a25836b 100644
--- a/shallow.c
+++ b/shallow.c
@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 		}
 	}
 	deep_clear_commit_depth(&depths, free_depth_in_slab);
+	object_array_clear(&stack);
 
 	return result;
 }
-- 
gitgitgadget
Samo Pogačnik via GitGitGadget· Feb 15, 2026, 20:11 UTC · re: Samo Pogačnik via GitGitGadget · lore

[PATCH v5 2/2] shallow: handling fetch relative-deepen

From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>

When a shallowed repository gets deepened beyond the beginning of a merged branch, we may end up with some shallows that are hidden behind the reachable shallow commits. Added test 'fetching deepen beyond merged branch' exposes that behaviour.

An example showing the problem based on added test:
0. Whole initial git repo to be cloned from
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 (branch) five
   | * ecb578a four
   |/
   * 0cb5d20 three
   * 2b4e70d two
   * 61ba98b one
1. Initial shallow clone --depth=3 (all good)
   Shallows:
   2b4e70da2a10e1d3231a0ae2df396024735601f1
   ecb578a3cf37198d122ae5df7efed9abaca17144
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a (grafted) four
   * 0cb5d20 three
   * 2b4e70d (grafted) two
2. Deepen shallow clone with fetch --deepen=1 (NOT OK)
   Shallows:
   0cb5d204f4ef96ed241feb0f2088c9f4794ba758
   61ba98be443fd51c542eb66585a1f6d7e15fcdae
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a four
   |/
   * 0cb5d20 (grafted) three
   ---
   Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae
   is not reachable.

On the other hand, it seems that equivalent absolute depth driven fetches result in all the correct shallows. That led to this proposal, which unifies absolute and relative deepening in a way that the same get_shallow_commits() call is used in both cases. The difference is only that depth is adapted for relative deepening by measuring equivalent depth of current local shallow commits in the current remote repo. Thus a new function get_shallows_depth() has been added and the function get_reachable_list() became redundant / removed.

Same example showing the corrected second step:
2. Deepen shallow clone with fetch --deepen=1 (all good)
   Shallow:
   61ba98be443fd51c542eb66585a1f6d7e15fcdae
   Graph:
   *   033585d (HEAD -> main) Merge branch 'branch'
   |\
   | * 984f8b1 five
   | * ecb578a four
   |/
   * 0cb5d20 three
   * 2b4e70d two
   * 61ba98b (grafted) one

The get_shallows_depth() function also shares the logic of the get_shallow_commits() function, but it focuses on counting depth of each existing shallow commit. The minimum result is stored as 'data->deepen_relative', which is set not to be zero for relative deepening anyway. That way we can always sum 'data->deepen_relative' and 'depth' values, because 'data->deepen_relative' is always 0 in absolute deepening. To avoid duplicating logic between get_shallows_depth() and get_shallow_commits(), get_shallow_commits() was modified so that it is used by get_shallows_depth().

Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>
Fixing v4
Fixing v4 again
---
 shallow.c             | 72 +++++++++++++++++++++++++++++++++++--------
 shallow.h             |  2 ++
 t/t5500-fetch-pack.sh | 23 ++++++++++++++
 upload-pack.c         | 72 ++-----------------------------------------
 4 files changed, 87 insertions(+), 82 deletions(-)
Show changes to 4 files +87 −82

shallow.c, shallow.h, t/t5500-fetch-pack.sh, upload-pack.c

diff --git a/shallow.c b/shallow.c
index 497a25836b..a156006d88 100644
--- a/shallow.c
+++ b/shallow.c
@@ -130,11 +130,24 @@ static void free_depth_in_slab(int **ptr)
 {
 	FREE_AND_NULL(*ptr);
 }
-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
-		int shallow_flag, int not_shallow_flag)
+/*
+ * This is a common internal function that can either return a list of
+ * shallow commits or calculate the current maximum depth of a shallow
+ * repository, depending on the input parameters.
+ *
+ * Depth calculation is triggered by passing the `shallows` parameter.
+ * In this case, the computed depth is stored in `max_cur_depth` (if it is
+ * provided), and the function returns NULL.
+ *
+ * Otherwise, `max_cur_depth` remains unchanged and the function returns
+ * a list of shallow commits.
+ */
+static struct commit_list *get_shallows_or_depth(struct object_array *heads,
+				struct object_array *shallows, int *max_cur_depth,
+				int depth, int shallow_flag, int not_shallow_flag)
 {
 	size_t i = 0;
-	int cur_depth = 0;
+	int cur_depth = 0, cur_depth_shallow = 0;
 	struct commit_list *result = NULL;
 	struct object_array stack = OBJECT_ARRAY_INIT;
 	struct commit *commit = NULL;
@@ -168,16 +181,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 		}
 		parse_commit_or_die(commit);
 		cur_depth++;
-		if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
-		    (is_repository_shallow(the_repository) && !commit->parents &&
-		     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
-		     graft->nr_parent < 0)) {
-			commit_list_insert(commit, &result);
-			commit->object.flags |= shallow_flag;
-			commit = NULL;
-			continue;
+		if (shallows) {
+			for (size_t j = 0; j < shallows->nr; j++)
+				if (oideq(&commit->object.oid, &shallows->objects[j].item->oid))
+					if (!cur_depth_shallow || cur_depth < cur_depth_shallow)
+						cur_depth_shallow = cur_depth;
+
+			if ((is_repository_shallow(the_repository) && !commit->parents &&
+			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
+			     graft->nr_parent < 0)) {
+				commit = NULL;
+				continue;
+			}
+		} else {
+			if ((depth != INFINITE_DEPTH && cur_depth >= depth) ||
+			    (is_repository_shallow(the_repository) && !commit->parents &&
+			     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&
+			     graft->nr_parent < 0)) {
+				commit_list_insert(commit, &result);
+				commit->object.flags |= shallow_flag;
+				commit = NULL;
+				continue;
+			}
+			commit->object.flags |= not_shallow_flag;
 		}
-		commit->object.flags |= not_shallow_flag;
 		for (p = commit->parents, commit = NULL; p; p = p->next) {
 			int **depth_slot = commit_depth_at(&depths, p->item);
 			if (!*depth_slot) {
@@ -200,9 +227,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,
 	deep_clear_commit_depth(&depths, free_depth_in_slab);
 	object_array_clear(&stack);
 
+	if (shallows && max_cur_depth)
+		*max_cur_depth = cur_depth_shallow;
 	return result;
 }
 
+int get_shallows_depth(struct object_array *heads, struct object_array *shallows)
+{
+	int max_cur_depth = 0;
+	get_shallows_or_depth(heads, shallows, &max_cur_depth, 0, 0, 0);
+	return max_cur_depth;
+
+}
+
+struct commit_list *get_shallow_commits(struct object_array *heads,
+					struct object_array *shallows, int deepen_relative,
+					int depth, int shallow_flag, int not_shallow_flag)
+{
+	if (shallows && deepen_relative) {
+		depth += get_shallows_depth(heads, shallows);
+	}
+	return get_shallows_or_depth(heads, NULL, NULL,
+				     depth, shallow_flag, not_shallow_flag);
+}
+
 static void show_commit(struct commit *commit, void *data)
 {
 	commit_list_insert(commit, data);
diff --git a/shallow.h b/shallow.h
index ad591bd139..e3f0df57ad 100644
--- a/shallow.h
+++ b/shallow.h
@@ -35,7 +35,9 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);
 /* rollback $GIT_DIR/shallow and reset stat-validity checks */
 void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);
 
+int get_shallows_depth(struct object_array *heads, struct object_array *shallows);
 struct commit_list *get_shallow_commits(struct object_array *heads,
+					struct object_array *shallows, int deepen_relative,
 					int depth, int shallow_flag, int not_shallow_flag);
 struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,
 						    int shallow_flag, int not_shallow_flag);
diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
index 2677cd5faa..5a8b30e1fd 100755
--- a/t/t5500-fetch-pack.sh
+++ b/t/t5500-fetch-pack.sh
@@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '
 	)
 '
 
+test_expect_success 'fetching deepen beyond merged branch' '
+	test_create_repo shallow-deepen-merged &&
+	(
+		cd shallow-deepen-merged &&
+		git commit --allow-empty -m one &&
+		git commit --allow-empty -m two &&
+		git commit --allow-empty -m three &&
+		git switch -c branch &&
+		git commit --allow-empty -m four &&
+		git commit --allow-empty -m five &&
+		git switch main &&
+		git merge --no-ff branch &&
+		cd - &&
+		git clone --bare --depth 3 "file://$(pwd)/shallow-deepen-merged" deepen.git &&
+		git -C deepen.git fetch origin --deepen=1 &&
+		git -C deepen.git rev-list --all >actual &&
+		for commit in $(sed "/^$/d" deepen.git/shallow)
+		do
+			test_grep "$commit" actual || exit 1
+		done
+	)
+'
+
 test_negotiation_algorithm_default () {
 	test_when_finished rm -rf clientv0 clientv2 &&
 	rm -rf server client &&
diff --git a/upload-pack.c b/upload-pack.c
index 2d2b70cbf2..88dac1b65c 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -704,56 +704,6 @@ error:
 	return -1;
 }
 
-static int get_reachable_list(struct upload_pack_data *data,
-			      struct object_array *reachable)
-{
-	struct child_process cmd = CHILD_PROCESS_INIT;
-	int i;
-	struct object *o;
-	char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
-	const unsigned hexsz = the_hash_algo->hexsz;
-	int ret;
-
-	if (do_reachable_revlist(&cmd, &data->shallows, reachable,
-				 data->allow_uor) < 0) {
-		ret = -1;
-		goto out;
-	}
-
-	while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {
-		struct object_id oid;
-		const char *p;
-
-		if (parse_oid_hex(namebuf, &oid, &p) || *p != '\n')
-			break;
-
-		o = lookup_object(the_repository, &oid);
-		if (o && o->type == OBJ_COMMIT) {
-			o->flags &= ~TMP_MARK;
-		}
-	}
-	for (i = get_max_object_index(the_repository); 0 < i; i--) {
-		o = get_indexed_object(the_repository, i - 1);
-		if (o && o->type == OBJ_COMMIT &&
-		    (o->flags & TMP_MARK)) {
-			add_object_array(o, NULL, reachable);
-				o->flags &= ~TMP_MARK;
-		}
-	}
-	close(cmd.out);
-
-	if (finish_command(&cmd)) {
-		ret = -1;
-		goto out;
-	}
-
-	ret = 0;
-
-out:
-	child_process_clear(&cmd);
-	return ret;
-}
-
 static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)
 {
 	struct child_process cmd = CHILD_PROCESS_INIT;
@@ -881,29 +831,11 @@ static void deepen(struct upload_pack_data *data, int depth)
 			struct object *object = data->shallows.objects[i].item;
 			object->flags |= NOT_SHALLOW;
 		}
-	} else if (data->deepen_relative) {
-		struct object_array reachable_shallows = OBJECT_ARRAY_INIT;
-		struct commit_list *result;
-
-		/*
-		 * Checking for reachable shallows requires that our refs be
-		 * marked with OUR_REF.
-		 */
-		refs_head_ref_namespaced(get_main_ref_store(the_repository),
-					 check_ref, data);
-		for_each_namespaced_ref_1(check_ref, data);
-
-		get_reachable_list(data, &reachable_shallows);
-		result = get_shallow_commits(&reachable_shallows,
-					     depth + 1,
-					     SHALLOW, NOT_SHALLOW);
-		send_shallow(data, result);
-		free_commit_list(result);
-		object_array_clear(&reachable_shallows);
 	} else {
 		struct commit_list *result;
 
-		result = get_shallow_commits(&data->want_obj, depth,
+		result = get_shallow_commits(&data->want_obj, &data->shallows,
+					     data->deepen_relative, depth,
 					     SHALLOW, NOT_SHALLOW);
 		send_shallow(data, result);
 		free_commit_list(result);
-- 
gitgitgadget
Junio C Hamano· Feb 20, 2026, 22:34 UTC · re: Samo Pogačnik via GitGitGadget · lore

Re: [PATCH v5 0/2] shallow: handling fetch relative-deepen

"Samo Pogačnik via GitGitGadget" <gitgitgadget@gmail.com> writes:
> When a shallowed repository gets deepened beyond the beginning of a merged
> branch, we may endup with some shallows, that are behind the reachable ones.
> Added test 'fetching deepen beyond merged branch' exposes that behaviour.

We didn't see any response to the latest round, and the comments on previous rounds seem to have been addressed. Is this ready to be merged down to 'next' now?

Thanks.

← back to recent threads