{"thread":{"id":"64603","subject":"[PATCH 0/2] shallow: handling fetch relative-deepen","startedAt":"2025-12-09T18:11:23Z","lastAt":"2026-02-20T22:34:48Z","messageCount":27,"participants":["Samo Pogačnik via GitGitGadget","Patrick Steinhardt","Samo Pogačnik","Junio C Hamano","Kristoffer Haugsbakk"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"531907","messageId":"pull.2121.git.git.1765303880.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":null,"subject":"[PATCH 0/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-09T18:11:18Z","receivedAt":"2025-12-09T18:11:23Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"When a shallowed repository gets deepened beyond the beginning of a merged\nbranch, we may endup with some shallows, that are behind the reachable ones.\nAdded test 'fetching deepen beyond merged branch' exposes that behaviour.\n\nOn the other hand, it seems that equivalent absolute depth driven fetches\nresult in all the correct shallows. That led to this proposal, which unifies\nabsolute and relative deepening in a way that the same get_shallow_commits()\ncall is used in both cases. The difference is only that depth is adapted for\nrelative deepening by measuring equivalent depth of current local shallow\ncommits in the current remote repo. Thus a new function get_shallows_depth()\nhas been added and the function get_reachable_list() became redundant /\nremoved.\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of each\nexisting shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative deepening\nanyway. That way we can allways summ 'data->deepen_relative' and 'depth'\nvalues, because 'data->deepen_relative' is always 0 in absolute deepening.\n\nSamo Pogačnik (2):\n  shallow: free local object_array allocations\n  shallow: handling fetch relative-deepen\n\n shallow.c             |   1 +\n t/t5500-fetch-pack.sh |  24 +++++++\n upload-pack.c         | 142 +++++++++++++++++++++++-------------------\n 3 files changed, 103 insertions(+), 64 deletions(-)\n\n\nbase-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v1\nPull-Request: https://github.com/git/git/pull/2121\n-- \ngitgitgadget\n"},{"id":"531908","messageId":"277c8616a9fc365b76b2f4ab458cd927834f9e0e.1765303880.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.git.git.1765303880.gitgitgadget@gmail.com","subject":"[PATCH 1/2] shallow: free local object_array allocations","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-09T18:11:19Z","receivedAt":"2025-12-09T18:11:24Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nThe local object_array 'stack' in get_shallow_commits() function\ndoes not free its dynamic elements before the function returns.\nAs a result elements remain allocated and their reference forgotten.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n shallow.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/shallow.c b/shallow.c\nindex 55b9cd9d3f..497a25836b 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t}\n \t}\n \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n+\tobject_array_clear(&stack);\n \n \treturn result;\n }\n-- \ngitgitgadget\n\n"},{"id":"531909","messageId":"b352a33c90ca67f4ad68df08c0fd155ceeeb167c.1765303880.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.git.git.1765303880.gitgitgadget@gmail.com","subject":"[PATCH 2/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-09T18:11:20Z","receivedAt":"2025-12-09T18:11:25Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nWhen a shallowed repository gets deepened beyond the beginning of a\nmerged branch, we may endup with some shallows, that are behind the\nreachable ones. Added test 'fetching deepen beyond merged branch'\nexposes that behaviour.\n\nOn the other hand, it seems that equivalent absolute depth driven\nfetches result in all the correct shallows. That led to this proposal,\nwhich unifies absolute and relative deepening in a way that the same\nget_shallow_commits() call is used in both cases. The difference is\nonly that depth is adapted for relative deepening by measuring\nequivalent depth of current local shallow commits in the current remote\nrepo. Thus a new function get_shallows_depth() has been added and the\nfunction get_reachable_list() became redundant / removed.\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of\neach existing shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative\ndeepening anyway. That way we can allways summ 'data->deepen_relative'\nand 'depth' values, because 'data->deepen_relative' is always 0 in\nabsolute deepening.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n t/t5500-fetch-pack.sh |  24 +++++++\n upload-pack.c         | 142 +++++++++++++++++++++++-------------------\n 2 files changed, 102 insertions(+), 64 deletions(-)\n\ndiff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\nindex 2677cd5faa..d05c45e32b 100755\n--- a/t/t5500-fetch-pack.sh\n+++ b/t/t5500-fetch-pack.sh\n@@ -955,6 +955,30 @@ test_expect_success 'fetching deepen' '\n \t)\n '\n \n+test_expect_success 'fetching deepen beyond merged branch' '\n+\ttest_create_repo shallow-deepen-merged &&\n+\t(\n+\t\tcd shallow-deepen-merged &&\n+\t\tgit commit --allow-empty -m one &&\n+\t\tgit commit --allow-empty -m two &&\n+\t\tgit commit --allow-empty -m three &&\n+\t\tgit switch -c branch &&\n+\t\tgit commit --allow-empty -m four &&\n+\t\tgit commit --allow-empty -m five &&\n+\t\tgit switch main &&\n+\t\tgit merge --no-ff branch &&\n+\t\tcd - &&\n+\t\tgit clone --bare --depth 3 \"file://$(pwd)/shallow-deepen-merged\" deepen.git &&\n+\t\tgit -C deepen.git fetch origin --deepen=1 &&\n+\t\techo \"Shallow:\" && cat deepen.git/shallow &&\n+\t\tgit -C deepen.git rev-list --all >actual &&\n+\t\techo \"All rev-lis:\" && cat actual &&\n+\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow); do\n+\t\t\ttest_grep \"$commit\" actual || exit 1\n+\t\tdone\n+\t)\n+'\n+\n test_negotiation_algorithm_default () {\n \ttest_when_finished rm -rf clientv0 clientv2 &&\n \trm -rf server client &&\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 2d2b70cbf2..ecd3e7f5ef 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -33,6 +33,7 @@\n #include \"json-writer.h\"\n #include \"strmap.h\"\n #include \"promisor-remote.h\"\n+#include \"tag.h\"\n \n /* Remember to update object flag allocation in object.h */\n #define THEY_HAVE\t(1u << 11)\n@@ -704,54 +705,82 @@ error:\n \treturn -1;\n }\n \n-static int get_reachable_list(struct upload_pack_data *data,\n-\t\t\t      struct object_array *reachable)\n+define_commit_slab(commit_depth, int *);\n+static void free_depth_in_slab(int **ptr)\n {\n-\tstruct child_process cmd = CHILD_PROCESS_INIT;\n-\tint i;\n-\tstruct object *o;\n-\tchar namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */\n-\tconst unsigned hexsz = the_hash_algo->hexsz;\n-\tint ret;\n-\n-\tif (do_reachable_revlist(&cmd, &data->shallows, reachable,\n-\t\t\t\t data->allow_uor) < 0) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\twhile ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {\n-\t\tstruct object_id oid;\n-\t\tconst char *p;\n-\n-\t\tif (parse_oid_hex(namebuf, &oid, &p) || *p != '\\n')\n-\t\t\tbreak;\n-\n-\t\to = lookup_object(the_repository, &oid);\n-\t\tif (o && o->type == OBJ_COMMIT) {\n-\t\t\to->flags &= ~TMP_MARK;\n+\tFREE_AND_NULL(*ptr);\n+}\n+static void get_shallows_depth(struct upload_pack_data *data)\n+{\n+\tsize_t i = 0, j;\n+\tint cur_depth = 0, cur_depth_shallow = 0;\n+\tstruct object_array stack = OBJECT_ARRAY_INIT;\n+\tstruct commit *commit = NULL;\n+\tstruct commit_graft *graft;\n+\tstruct commit_depth depths;\n+\tstruct object_array *heads = &data->want_obj;\n+\tstruct object_array *shallows = &data->shallows;\n+\n+\tinit_commit_depth(&depths);\n+\twhile (commit || i < heads->nr || stack.nr) {\n+\t\tstruct commit_list *p;\n+\t\tif (!commit) {\n+\t\t\tif (i < heads->nr) {\n+\t\t\t\tint **depth_slot;\n+\t\t\t\tcommit = (struct commit *)\n+\t\t\t\t\tderef_tag(the_repository,\n+\t\t\t\t\t\t  heads->objects[i++].item,\n+\t\t\t\t\t\t  NULL, 0);\n+\t\t\t\tif (!commit || commit->object.type != OBJ_COMMIT) {\n+\t\t\t\t\tcommit = NULL;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t\tdepth_slot = commit_depth_at(&depths, commit);\n+\t\t\t\tif (!*depth_slot)\n+\t\t\t\t\t*depth_slot = xmalloc(sizeof(int));\n+\t\t\t\t**depth_slot = 0;\n+\t\t\t\tcur_depth = 0;\n+\t\t\t} else {\n+\t\t\t\tcommit = (struct commit *)\n+\t\t\t\t\tobject_array_pop(&stack);\n+\t\t\t\tcur_depth = **commit_depth_at(&depths, commit);\n+\t\t\t}\n \t\t}\n-\t}\n-\tfor (i = get_max_object_index(the_repository); 0 < i; i--) {\n-\t\to = get_indexed_object(the_repository, i - 1);\n-\t\tif (o && o->type == OBJ_COMMIT &&\n-\t\t    (o->flags & TMP_MARK)) {\n-\t\t\tadd_object_array(o, NULL, reachable);\n-\t\t\t\to->flags &= ~TMP_MARK;\n+\t\tparse_commit_or_die(commit);\n+\t\tcur_depth++;\n+\t\tfor (j = 0; j < shallows->nr; j++)\n+\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n+\t\t\t\tif ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))\n+\t\t\t\t\tcur_depth_shallow = cur_depth;\n+\n+\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n+\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n+\t\t     graft->nr_parent < 0)) {\n+\t\t\tcommit = NULL;\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfor (p = commit->parents, commit = NULL; p; p = p->next) {\n+\t\t\tint **depth_slot = commit_depth_at(&depths, p->item);\n+\t\t\tif (!*depth_slot) {\n+\t\t\t\t*depth_slot = xmalloc(sizeof(int));\n+\t\t\t\t**depth_slot = cur_depth;\n+\t\t\t} else {\n+\t\t\t\tif (cur_depth >= **depth_slot)\n+\t\t\t\t\tcontinue;\n+\t\t\t\t**depth_slot = cur_depth;\n+\t\t\t}\n+\t\t\tif (p->next)\n+\t\t\t\tadd_object_array(&p->item->object,\n+\t\t\t\t\t\tNULL, &stack);\n+\t\t\telse {\n+\t\t\t\tcommit = p->item;\n+\t\t\t\tcur_depth = **commit_depth_at(&depths, commit);\n+\t\t\t}\n \t\t}\n \t}\n-\tclose(cmd.out);\n-\n-\tif (finish_command(&cmd)) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tret = 0;\n-\n-out:\n-\tchild_process_clear(&cmd);\n-\treturn ret;\n+\tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n+\tobject_array_clear(&stack);\n+\tdata->deepen_relative = cur_depth_shallow;\n }\n \n static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)\n@@ -881,29 +910,14 @@ static void deepen(struct upload_pack_data *data, int depth)\n \t\t\tstruct object *object = data->shallows.objects[i].item;\n \t\t\tobject->flags |= NOT_SHALLOW;\n \t\t}\n-\t} else if (data->deepen_relative) {\n-\t\tstruct object_array reachable_shallows = OBJECT_ARRAY_INIT;\n-\t\tstruct commit_list *result;\n-\n-\t\t/*\n-\t\t * Checking for reachable shallows requires that our refs be\n-\t\t * marked with OUR_REF.\n-\t\t */\n-\t\trefs_head_ref_namespaced(get_main_ref_store(the_repository),\n-\t\t\t\t\t check_ref, data);\n-\t\tfor_each_namespaced_ref_1(check_ref, data);\n-\n-\t\tget_reachable_list(data, &reachable_shallows);\n-\t\tresult = get_shallow_commits(&reachable_shallows,\n-\t\t\t\t\t     depth + 1,\n-\t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n-\t\tsend_shallow(data, result);\n-\t\tfree_commit_list(result);\n-\t\tobject_array_clear(&reachable_shallows);\n \t} else {\n \t\tstruct commit_list *result;\n \n-\t\tresult = get_shallow_commits(&data->want_obj, depth,\n+\t\tif (data->deepen_relative)\n+\t\t\tget_shallows_depth(data);\n+\n+\t\tresult = get_shallow_commits(&data->want_obj,\n+\t\t\t\t\t     data->deepen_relative + depth,\n \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n \t\tsend_shallow(data, result);\n \t\tfree_commit_list(result);\n-- \ngitgitgadget\n"},{"id":"533110","messageId":"aVy9ZveUOg3yum2X@pks.im","threadId":"64603","inReplyTo":"277c8616a9fc365b76b2f4ab458cd927834f9e0e.1765303880.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/2] shallow: free local object_array allocations","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T07:44:38Z","receivedAt":"2026-01-06T07:44:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Dec 09, 2025 at 06:11:19PM +0000, Samo Pogačnik via GitGitGadget wrote:\n> From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n> \n> The local object_array 'stack' in get_shallow_commits() function\n> does not free its dynamic elements before the function returns.\n> As a result elements remain allocated and their reference forgotten.\n\nI think the elements themselves are actually fine. We have the following\nloop:\n\n\twhile (commit || i < heads->nr || stack.nr) {\n\nSo while the stack still has entries, we'll keep on iteration.\nFurthermore, there is no `break` or early return in the loop, so we can\nsure that we actually pop every single element from the array.\n\nThat being said, what we _don't_ do is to free the array itself. So I'm\nmostly splitting hairs with how the commit message is phrased, the\nchange looks correct to me.\n\nWhat I'm wondering though is why we never hit this memory leak in our\ntest suite. I guess the reason is simply that we ain't got enough test\ncoverage around shallow clones. Have you seen this leak in the wild? And\nif so, can we add a test case that surfaces it?\n\nThanks!\n\nPatrick\n"},{"id":"533111","messageId":"aVy9a9f1AZzTbBQa@pks.im","threadId":"64603","inReplyTo":"b352a33c90ca67f4ad68df08c0fd155ceeeb167c.1765303880.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/2] shallow: handling fetch relative-deepen","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T07:44:43Z","receivedAt":"2026-01-06T07:44:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Dec 09, 2025 at 06:11:20PM +0000, Samo Pogačnik via GitGitGadget wrote:\n> From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n> \n> When a shallowed repository gets deepened beyond the beginning of a\n> merged branch, we may endup with some shallows, that are behind the\n\ns/endup/end up/\ns/shallows, that/shallows that/\n\n> reachable ones.\n\nHm, which reachable ones? Sorry, I can't quite follow, it would help the\nreviewer to add a bit more context.\n\n> Added test 'fetching deepen beyond merged branch' exposes that\n> behaviour.\n> \n> On the other hand, it seems that equivalent absolute depth driven\n> fetches result in all the correct shallows. That led to this proposal,\n> which unifies absolute and relative deepening in a way that the same\n> get_shallow_commits() call is used in both cases. The difference is\n> only that depth is adapted for relative deepening by measuring\n> equivalent depth of current local shallow commits in the current remote\n> repo. Thus a new function get_shallows_depth() has been added and the\n> function get_reachable_list() became redundant / removed.\n> \n> The get_shallows_depth() function also shares the logic of the\n> get_shallow_commits() function, but it focuses on counting depth of\n> each existing shallow commit. The minimum result is stored as\n> 'data->deepen_relative', which is set not to be zero for relative\n> deepening anyway. That way we can allways summ 'data->deepen_relative'\n> and 'depth' values, because 'data->deepen_relative' is always 0 in\n> absolute deepening.\n\nI think the commit message needs some polishing. I myself am not that\nfamiliar with our shallow logic, so I'm a bit lost here to be honest.\n\nTypically, a commit message should be self-explanatory and guide the\nreader through the problem space as well as the solution. It should, in\nthe following order:\n\n  - Explain what the actual issue is as observed by the user. I'm not\n    really sure about this part, only that it's something related to\n    shallow clones, deepening and merge commits.\n\n  - Explain what the root cause of the issue is.\n\n  - Explain how the root cause is being fixed. Ideally, it should also\n    explain why that is the correct fix, potentially referencing other\n    code like you do.\n\nYour commit message on the other hand explains more of the \"what\" and\nless of the \"why\", which makes it hard to follow. Also, an ASCII commit\ngraph would probably go a long way in explaining the issue :)\n\n> diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\n> index 2677cd5faa..d05c45e32b 100755\n> --- a/t/t5500-fetch-pack.sh\n> +++ b/t/t5500-fetch-pack.sh\n> @@ -955,6 +955,30 @@ test_expect_success 'fetching deepen' '\n>  \t)\n>  '\n>  \n> +test_expect_success 'fetching deepen beyond merged branch' '\n> +\ttest_create_repo shallow-deepen-merged &&\n> +\t(\n> +\t\tcd shallow-deepen-merged &&\n> +\t\tgit commit --allow-empty -m one &&\n> +\t\tgit commit --allow-empty -m two &&\n> +\t\tgit commit --allow-empty -m three &&\n> +\t\tgit switch -c branch &&\n> +\t\tgit commit --allow-empty -m four &&\n> +\t\tgit commit --allow-empty -m five &&\n> +\t\tgit switch main &&\n> +\t\tgit merge --no-ff branch &&\n> +\t\tcd - &&\n> +\t\tgit clone --bare --depth 3 \"file://$(pwd)/shallow-deepen-merged\" deepen.git &&\n> +\t\tgit -C deepen.git fetch origin --deepen=1 &&\n> +\t\techo \"Shallow:\" && cat deepen.git/shallow &&\n> +\t\tgit -C deepen.git rev-list --all >actual &&\n> +\t\techo \"All rev-lis:\" && cat actual &&\n\nThis statement and the one two lines further up look like debug code to\nme.\n\n> +\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow); do\n\nNit: loops should be formatted like this:\n\n    for commit in ...\n    do\n        ...\n    done\n\n> diff --git a/upload-pack.c b/upload-pack.c\n> index 2d2b70cbf2..ecd3e7f5ef 100644\n> --- a/upload-pack.c\n> +++ b/upload-pack.c\n> @@ -704,54 +705,82 @@ error:\n>  \treturn -1;\n>  }\n>  \n> -static int get_reachable_list(struct upload_pack_data *data,\n> -\t\t\t      struct object_array *reachable)\n> +define_commit_slab(commit_depth, int *);\n> +static void free_depth_in_slab(int **ptr)\n>  {\n> -\tstruct child_process cmd = CHILD_PROCESS_INIT;\n> -\tint i;\n> -\tstruct object *o;\n> -\tchar namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */\n> -\tconst unsigned hexsz = the_hash_algo->hexsz;\n> -\tint ret;\n> -\n> -\tif (do_reachable_revlist(&cmd, &data->shallows, reachable,\n> -\t\t\t\t data->allow_uor) < 0) {\n> -\t\tret = -1;\n> -\t\tgoto out;\n> -\t}\n> -\n> -\twhile ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {\n> -\t\tstruct object_id oid;\n> -\t\tconst char *p;\n> -\n> -\t\tif (parse_oid_hex(namebuf, &oid, &p) || *p != '\\n')\n> -\t\t\tbreak;\n> -\n> -\t\to = lookup_object(the_repository, &oid);\n> -\t\tif (o && o->type == OBJ_COMMIT) {\n> -\t\t\to->flags &= ~TMP_MARK;\n> +\tFREE_AND_NULL(*ptr);\n> +}\n> +static void get_shallows_depth(struct upload_pack_data *data)\n\nThis function looks very similar to `get_shallow_commits()`. Is it\npossible to deduplicate the logic?\n\nThanks!\n\nPatrick\n"},{"id":"533388","messageId":"eefa06bc46f8029b68efe993da67d14e268e1bf2.camel@t-2.net","threadId":"64603","inReplyTo":"aVy9ZveUOg3yum2X@pks.im","subject":"Re: [PATCH 1/2] shallow: free local object_array allocations","fromName":"Samo Pogačnik","fromEmail":"samo_pogacnik@t-2.net","sentAt":"2026-01-09T16:21:45Z","receivedAt":"2026-01-09T16:30:45Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"Hi Patrick,\nthanks a lot for the reply.\n\nOn Tue, 2026-01-06 at 08:44 +0100, Patrick Steinhardt wrote:\n> On Tue, Dec 09, 2025 at 06:11:19PM +0000, Samo Pogačnik via GitGitGadget\n> wrote:\n> > From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n> > \n> > The local object_array 'stack' in get_shallow_commits() function\n> > does not free its dynamic elements before the function returns.\n> > As a result elements remain allocated and their reference forgotten.\n> \n> I think the elements themselves are actually fine. We have the following\n> loop:\n> \n> \twhile (commit || i < heads->nr || stack.nr) {\n> \n> So while the stack still has entries, we'll keep on iteration.\n> Furthermore, there is no `break` or early return in the loop, so we can\n> sure that we actually pop every single element from the array.\n> \n> That being said, what we _don't_ do is to free the array itself. So I'm\n> mostly splitting hairs with how the commit message is phrased, the\n> change looks correct to me.\n> \n> What I'm wondering though is why we never hit this memory leak in our\n> test suite. I guess the reason is simply that we ain't got enough test\n> coverage around shallow clones. Have you seen this leak in the wild? And\n> if so, can we add a test case that surfaces it?\n> \n\nActually, the test I've added with the patch 2/2 does not pass without this\nmemory fix in linux-leaks and linux-reftable-leaks test runs.\n\nbest regards, Samo\n"},{"id":"533390","messageId":"aWEt8UXucXMY0QCg@pks.im","threadId":"64603","inReplyTo":"eefa06bc46f8029b68efe993da67d14e268e1bf2.camel@t-2.net","subject":"Re: [PATCH 1/2] shallow: free local object_array allocations","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T16:33:53Z","receivedAt":"2026-01-09T16:33:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 09, 2026 at 05:21:45PM +0100, Samo Pogačnik wrote:\n> Hi Patrick,\n> thanks a lot for the reply.\n> \n> On Tue, 2026-01-06 at 08:44 +0100, Patrick Steinhardt wrote:\n> > On Tue, Dec 09, 2025 at 06:11:19PM +0000, Samo Pogačnik via GitGitGadget\n> > wrote:\n> > > From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n> > > \n> > > The local object_array 'stack' in get_shallow_commits() function\n> > > does not free its dynamic elements before the function returns.\n> > > As a result elements remain allocated and their reference forgotten.\n> > \n> > I think the elements themselves are actually fine. We have the following\n> > loop:\n> > \n> > \twhile (commit || i < heads->nr || stack.nr) {\n> > \n> > So while the stack still has entries, we'll keep on iteration.\n> > Furthermore, there is no `break` or early return in the loop, so we can\n> > sure that we actually pop every single element from the array.\n> > \n> > That being said, what we _don't_ do is to free the array itself. So I'm\n> > mostly splitting hairs with how the commit message is phrased, the\n> > change looks correct to me.\n> > \n> > What I'm wondering though is why we never hit this memory leak in our\n> > test suite. I guess the reason is simply that we ain't got enough test\n> > coverage around shallow clones. Have you seen this leak in the wild? And\n> > if so, can we add a test case that surfaces it?\n> > \n> \n> Actually, the test I've added with the patch 2/2 does not pass without this\n> memory fix in linux-leaks and linux-reftable-leaks test runs.\n\nIn that case it would make sense to point out this detail in the commit\nmessage to make it a bit easier for the reviewer. Thanks!\n\nPatrick\n"},{"id":"533394","messageId":"d2cb7351419002ec3be6423f39091e9d0d0e3877.camel@t-2.net","threadId":"64603","inReplyTo":"aVy9a9f1AZzTbBQa@pks.im","subject":"Re: [PATCH 2/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik","fromEmail":"samo_pogacnik@t-2.net","sentAt":"2026-01-09T16:48:19Z","receivedAt":"2026-01-09T16:48:53Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"\nOn Tue, 2026-01-06 at 08:44 +0100, Patrick Steinhardt wrote:\n> On Tue, Dec 09, 2025 at 06:11:20PM +0000, Samo Pogačnik via GitGitGadget\n> wrote:\n> > From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n> > \n> > When a shallowed repository gets deepened beyond the beginning of a\n> > merged branch, we may endup with some shallows, that are behind the\n> \n> s/endup/end up/\n> s/shallows, that/shallows that/\n> \n> > reachable ones.\n> \n> Hm, which reachable ones? Sorry, I can't quite follow, it would help the\n> reviewer to add a bit more context.\n> \n> > Added test 'fetching deepen beyond merged branch' exposes that\n> > behaviour.\n> > \n> > On the other hand, it seems that equivalent absolute depth driven\n> > fetches result in all the correct shallows. That led to this proposal,\n> > which unifies absolute and relative deepening in a way that the same\n> > get_shallow_commits() call is used in both cases. The difference is\n> > only that depth is adapted for relative deepening by measuring\n> > equivalent depth of current local shallow commits in the current remote\n> > repo. Thus a new function get_shallows_depth() has been added and the\n> > function get_reachable_list() became redundant / removed.\n> > \n> > The get_shallows_depth() function also shares the logic of the\n> > get_shallow_commits() function, but it focuses on counting depth of\n> > each existing shallow commit. The minimum result is stored as\n> > 'data->deepen_relative', which is set not to be zero for relative\n> > deepening anyway. That way we can allways summ 'data->deepen_relative'\n> > and 'depth' values, because 'data->deepen_relative' is always 0 in\n> > absolute deepening.\n> \n> I think the commit message needs some polishing. I myself am not that\n> familiar with our shallow logic, so I'm a bit lost here to be honest.\n> \n> Typically, a commit message should be self-explanatory and guide the\n> reader through the problem space as well as the solution. It should, in\n> the following order:\n> \n>   - Explain what the actual issue is as observed by the user. I'm not\n>     really sure about this part, only that it's something related to\n>     shallow clones, deepening and merge commits.\n> \n>   - Explain what the root cause of the issue is.\n> \n>   - Explain how the root cause is being fixed. Ideally, it should also\n>     explain why that is the correct fix, potentially referencing other\n>     code like you do.\n> \n> Your commit message on the other hand explains more of the \"what\" and\n> less of the \"why\", which makes it hard to follow. Also, an ASCII commit\n> graph would probably go a long way in explaining the issue :)\n> \n> > diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\n> > index 2677cd5faa..d05c45e32b 100755\n> > --- a/t/t5500-fetch-pack.sh\n> > +++ b/t/t5500-fetch-pack.sh\n> > @@ -955,6 +955,30 @@ test_expect_success 'fetching deepen' '\n> >  \t)\n> >  '\n> >  \n> > +test_expect_success 'fetching deepen beyond merged branch' '\n> > +\ttest_create_repo shallow-deepen-merged &&\n> > +\t(\n> > +\t\tcd shallow-deepen-merged &&\n> > +\t\tgit commit --allow-empty -m one &&\n> > +\t\tgit commit --allow-empty -m two &&\n> > +\t\tgit commit --allow-empty -m three &&\n> > +\t\tgit switch -c branch &&\n> > +\t\tgit commit --allow-empty -m four &&\n> > +\t\tgit commit --allow-empty -m five &&\n> > +\t\tgit switch main &&\n> > +\t\tgit merge --no-ff branch &&\n> > +\t\tcd - &&\n> > +\t\tgit clone --bare --depth 3 \"file://$(pwd)/shallow-deepen-\n> > merged\" deepen.git &&\n> > +\t\tgit -C deepen.git fetch origin --deepen=1 &&\n> > +\t\techo \"Shallow:\" && cat deepen.git/shallow &&\n> > +\t\tgit -C deepen.git rev-list --all >actual &&\n> > +\t\techo \"All rev-lis:\" && cat actual &&\n> \n> This statement and the one two lines further up look like debug code to\n> me.\n> \n> > +\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow); do\n> \n> Nit: loops should be formatted like this:\n> \n>     for commit in ...\n>     do\n>         ...\n>     done\n> \n> > diff --git a/upload-pack.c b/upload-pack.c\n> > index 2d2b70cbf2..ecd3e7f5ef 100644\n> > --- a/upload-pack.c\n> > +++ b/upload-pack.c\n> > @@ -704,54 +705,82 @@ error:\n> >  \treturn -1;\n> >  }\n> >  \n> > -static int get_reachable_list(struct upload_pack_data *data,\n> > -\t\t\t      struct object_array *reachable)\n> > +define_commit_slab(commit_depth, int *);\n> > +static void free_depth_in_slab(int **ptr)\n> >  {\n> > -\tstruct child_process cmd = CHILD_PROCESS_INIT;\n> > -\tint i;\n> > -\tstruct object *o;\n> > -\tchar namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */\n> > -\tconst unsigned hexsz = the_hash_algo->hexsz;\n> > -\tint ret;\n> > -\n> > -\tif (do_reachable_revlist(&cmd, &data->shallows, reachable,\n> > -\t\t\t\t data->allow_uor) < 0) {\n> > -\t\tret = -1;\n> > -\t\tgoto out;\n> > -\t}\n> > -\n> > -\twhile ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz +\n> > 1) {\n> > -\t\tstruct object_id oid;\n> > -\t\tconst char *p;\n> > -\n> > -\t\tif (parse_oid_hex(namebuf, &oid, &p) || *p != '\\n')\n> > -\t\t\tbreak;\n> > -\n> > -\t\to = lookup_object(the_repository, &oid);\n> > -\t\tif (o && o->type == OBJ_COMMIT) {\n> > -\t\t\to->flags &= ~TMP_MARK;\n> > +\tFREE_AND_NULL(*ptr);\n> > +}\n> > +static void get_shallows_depth(struct upload_pack_data *data)\n> \n> This function looks very similar to `get_shallow_commits()`. Is it\n> possible to deduplicate the logic?\n\nThank you for the valuable reply. I'll try to address all the raised points\nincluding mentioning in commit 1/2 that added test from 2/2 fail without 1/2 as\nquickly as time allows me to.\n\nThanks!\n\nSamo\n"},{"id":"533450","messageId":"pull.2121.v2.git.git.1767997426.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.git.git.1765303880.gitgitgadget@gmail.com","subject":"[PATCH v2 0/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T22:23:44Z","receivedAt":"2026-01-09T22:23:48Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"When a shallowed repository gets deepened beyond the beginning of a merged\nbranch, we may endup with some shallows, that are behind the reachable ones.\nAdded test 'fetching deepen beyond merged branch' exposes that behaviour.\n\nOn the other hand, it seems that equivalent absolute depth driven fetches\nresult in all the correct shallows. That led to this proposal, which unifies\nabsolute and relative deepening in a way that the same get_shallow_commits()\ncall is used in both cases. The difference is only that depth is adapted for\nrelative deepening by measuring equivalent depth of current local shallow\ncommits in the current remote repo. Thus a new function get_shallows_depth()\nhas been added and the function get_reachable_list() became redundant /\nremoved.\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of each\nexisting shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative deepening\nanyway. That way we can allways summ 'data->deepen_relative' and 'depth'\nvalues, because 'data->deepen_relative' is always 0 in absolute deepening.\n\nSamo Pogačnik (2):\n  shallow: free local object_array allocations\n  shallow: handling fetch relative-deepen\n\n shallow.c             | 45 +++++++++++++++++--------\n shallow.h             |  1 +\n t/t5500-fetch-pack.sh | 23 +++++++++++++\n upload-pack.c         | 76 +++++--------------------------------------\n 4 files changed, 64 insertions(+), 81 deletions(-)\n\n\nbase-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v2\nPull-Request: https://github.com/git/git/pull/2121\n\nRange-diff vs v1:\n\n 1:  277c8616a9 ! 1:  f8a8d077cd shallow: free local object_array allocations\n     @@ Commit message\n          does not free its dynamic elements before the function returns.\n          As a result elements remain allocated and their reference forgotten.\n      \n     +    Also note, that test 'fetching deepen beyond merged branch' added by\n     +    'shallow: handling fetch relative-deepen' patch fails without this\n     +    correction in linux-leaks and linux-reftable-leaks test runs.\n     +\n          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n      \n       ## shallow.c ##\n 2:  b352a33c90 ! 2:  ba1f80105f shallow: handling fetch relative-deepen\n     @@ Commit message\n          shallow: handling fetch relative-deepen\n      \n          When a shallowed repository gets deepened beyond the beginning of a\n     -    merged branch, we may endup with some shallows, that are behind the\n     -    reachable ones. Added test 'fetching deepen beyond merged branch'\n     -    exposes that behaviour.\n     +    merged branch, we may end up with some shallows that are hidden behind\n     +    the reachable shallow commits. Added test 'fetching deepen beyond\n     +    merged branch' exposes that behaviour.\n     +\n     +    An example showing the problem based on added test:\n     +    0. Whole initial git repo to be cloned from\n     +    Graph:\n     +    *   033585d (HEAD -> main) Merge branch 'branch'\n     +    |\\\n     +    | * 984f8b1 (branch) five\n     +    | * ecb578a four\n     +    |/\n     +    * 0cb5d20 three\n     +    * 2b4e70d two\n     +    * 61ba98b one\n     +\n     +    1. Initial shallow clone --depth=3 (all good)\n     +    Shallows:\n     +    2b4e70da2a10e1d3231a0ae2df396024735601f1\n     +    ecb578a3cf37198d122ae5df7efed9abaca17144\n     +    Graph:\n     +    *   033585d (HEAD -> main) Merge branch 'branch'\n     +    |\\\n     +    | * 984f8b1 five\n     +    | * ecb578a (grafted) four\n     +    * 0cb5d20 three\n     +    * 2b4e70d (grafted) two\n     +\n     +    2. Deepen shallow clone with fetch --deepen=1 (NOT OK)\n     +    Shallows:\n     +    0cb5d204f4ef96ed241feb0f2088c9f4794ba758\n     +    61ba98be443fd51c542eb66585a1f6d7e15fcdae\n     +    Graph:\n     +    *   033585d (HEAD -> main) Merge branch 'branch'\n     +    |\\\n     +    | * 984f8b1 five\n     +    | * ecb578a four\n     +    |/\n     +    * 0cb5d20 (grafted) three\n     +    ---\n     +    Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n     +    is not reachable.\n      \n          On the other hand, it seems that equivalent absolute depth driven\n          fetches result in all the correct shallows. That led to this proposal,\n     @@ Commit message\n          repo. Thus a new function get_shallows_depth() has been added and the\n          function get_reachable_list() became redundant / removed.\n      \n     +    Same example showing the corrected second step:\n     +    2. Deepen shallow clone with fetch --deepen=1 (all good)\n     +    Shallow:\n     +    61ba98be443fd51c542eb66585a1f6d7e15fcdae\n     +    Graph:\n     +    *   033585d (HEAD -> main) Merge branch 'branch'\n     +    |\\\n     +    | * 984f8b1 five\n     +    | * ecb578a four\n     +    |/\n     +    * 0cb5d20 three\n     +    * 2b4e70d two\n     +    * 61ba98b (grafted) one\n     +\n          The get_shallows_depth() function also shares the logic of the\n          get_shallow_commits() function, but it focuses on counting depth of\n          each existing shallow commit. The minimum result is stored as\n     @@ Commit message\n      \n          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n      \n     + ## shallow.c ##\n     +@@ shallow.c: static void free_depth_in_slab(int **ptr)\n     + {\n     + \tFREE_AND_NULL(*ptr);\n     + }\n     +-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n     +-\t\tint shallow_flag, int not_shallow_flag)\n     ++struct commit_list *get_shallow_commits(struct object_array *heads,\n     ++\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n     ++\t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n     + {\n     +-\tsize_t i = 0;\n     +-\tint cur_depth = 0;\n     ++\tsize_t i = 0, j;\n     ++\tint cur_depth = 0, cur_depth_shallow = 0;\n     + \tstruct commit_list *result = NULL;\n     + \tstruct object_array stack = OBJECT_ARRAY_INIT;\n     + \tstruct commit *commit = NULL;\n     +@@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n     + \t\t}\n     + \t\tparse_commit_or_die(commit);\n     + \t\tcur_depth++;\n     +-\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n     +-\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n     +-\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n     +-\t\t     graft->nr_parent < 0)) {\n     +-\t\t\tcommit_list_insert(commit, &result);\n     +-\t\t\tcommit->object.flags |= shallow_flag;\n     +-\t\t\tcommit = NULL;\n     +-\t\t\tcontinue;\n     ++\t\tif (shallows) {\n     ++\t\t\tfor (j = 0; j < shallows->nr; j++)\n     ++\t\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n     ++\t\t\t\t\tif ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))\n     ++\t\t\t\t\t\tcur_depth_shallow = cur_depth;\n     ++\n     ++\t\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n     ++\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n     ++\t\t\t     graft->nr_parent < 0)) {\n     ++\t\t\t\tcommit = NULL;\n     ++\t\t\t\tcontinue;\n     ++\t\t\t}\n     ++\t\t} else {\n     ++\t\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n     ++\t\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n     ++\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n     ++\t\t\t     graft->nr_parent < 0)) {\n     ++\t\t\t\tcommit_list_insert(commit, &result);\n     ++\t\t\t\tcommit->object.flags |= shallow_flag;\n     ++\t\t\t\tcommit = NULL;\n     ++\t\t\t\tcontinue;\n     ++\t\t\t}\n     ++\t\t\tcommit->object.flags |= not_shallow_flag;\n     + \t\t}\n     +-\t\tcommit->object.flags |= not_shallow_flag;\n     + \t\tfor (p = commit->parents, commit = NULL; p; p = p->next) {\n     + \t\t\tint **depth_slot = commit_depth_at(&depths, p->item);\n     + \t\t\tif (!*depth_slot) {\n     +@@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n     + \t}\n     + \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n     + \tobject_array_clear(&stack);\n     +-\n     ++\tif (shallows && deepen_relative)\n     ++\t\t*deepen_relative = cur_depth_shallow;\n     + \treturn result;\n     + }\n     + \n     +\n     + ## shallow.h ##\n     +@@ shallow.h: int commit_shallow_file(struct repository *r, struct shallow_lock *lk);\n     + void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);\n     + \n     + struct commit_list *get_shallow_commits(struct object_array *heads,\n     ++\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n     + \t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag);\n     + struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n     + \t\t\t\t\t\t    int shallow_flag, int not_shallow_flag);\n     +\n       ## t/t5500-fetch-pack.sh ##\n      @@ t/t5500-fetch-pack.sh: test_expect_success 'fetching deepen' '\n       \t)\n     @@ t/t5500-fetch-pack.sh: test_expect_success 'fetching deepen' '\n      +\t\tcd - &&\n      +\t\tgit clone --bare --depth 3 \"file://$(pwd)/shallow-deepen-merged\" deepen.git &&\n      +\t\tgit -C deepen.git fetch origin --deepen=1 &&\n     -+\t\techo \"Shallow:\" && cat deepen.git/shallow &&\n      +\t\tgit -C deepen.git rev-list --all >actual &&\n     -+\t\techo \"All rev-lis:\" && cat actual &&\n     -+\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow); do\n     ++\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow)\n     ++\t\tdo\n      +\t\t\ttest_grep \"$commit\" actual || exit 1\n      +\t\tdone\n      +\t)\n     @@ t/t5500-fetch-pack.sh: test_expect_success 'fetching deepen' '\n       \trm -rf server client &&\n      \n       ## upload-pack.c ##\n     -@@\n     - #include \"json-writer.h\"\n     - #include \"strmap.h\"\n     - #include \"promisor-remote.h\"\n     -+#include \"tag.h\"\n     - \n     - /* Remember to update object flag allocation in object.h */\n     - #define THEY_HAVE\t(1u << 11)\n      @@ upload-pack.c: error:\n       \treturn -1;\n       }\n       \n      -static int get_reachable_list(struct upload_pack_data *data,\n      -\t\t\t      struct object_array *reachable)\n     -+define_commit_slab(commit_depth, int *);\n     -+static void free_depth_in_slab(int **ptr)\n     ++static void get_shallows_depth(struct upload_pack_data *data)\n       {\n      -\tstruct child_process cmd = CHILD_PROCESS_INIT;\n      -\tint i;\n     @@ upload-pack.c: error:\n      -\t\to = lookup_object(the_repository, &oid);\n      -\t\tif (o && o->type == OBJ_COMMIT) {\n      -\t\t\to->flags &= ~TMP_MARK;\n     -+\tFREE_AND_NULL(*ptr);\n     -+}\n     -+static void get_shallows_depth(struct upload_pack_data *data)\n     -+{\n     -+\tsize_t i = 0, j;\n     -+\tint cur_depth = 0, cur_depth_shallow = 0;\n     -+\tstruct object_array stack = OBJECT_ARRAY_INIT;\n     -+\tstruct commit *commit = NULL;\n     -+\tstruct commit_graft *graft;\n     -+\tstruct commit_depth depths;\n     -+\tstruct object_array *heads = &data->want_obj;\n     -+\tstruct object_array *shallows = &data->shallows;\n     -+\n     -+\tinit_commit_depth(&depths);\n     -+\twhile (commit || i < heads->nr || stack.nr) {\n     -+\t\tstruct commit_list *p;\n     -+\t\tif (!commit) {\n     -+\t\t\tif (i < heads->nr) {\n     -+\t\t\t\tint **depth_slot;\n     -+\t\t\t\tcommit = (struct commit *)\n     -+\t\t\t\t\tderef_tag(the_repository,\n     -+\t\t\t\t\t\t  heads->objects[i++].item,\n     -+\t\t\t\t\t\t  NULL, 0);\n     -+\t\t\t\tif (!commit || commit->object.type != OBJ_COMMIT) {\n     -+\t\t\t\t\tcommit = NULL;\n     -+\t\t\t\t\tcontinue;\n     -+\t\t\t\t}\n     -+\t\t\t\tdepth_slot = commit_depth_at(&depths, commit);\n     -+\t\t\t\tif (!*depth_slot)\n     -+\t\t\t\t\t*depth_slot = xmalloc(sizeof(int));\n     -+\t\t\t\t**depth_slot = 0;\n     -+\t\t\t\tcur_depth = 0;\n     -+\t\t\t} else {\n     -+\t\t\t\tcommit = (struct commit *)\n     -+\t\t\t\t\tobject_array_pop(&stack);\n     -+\t\t\t\tcur_depth = **commit_depth_at(&depths, commit);\n     -+\t\t\t}\n     - \t\t}\n     +-\t\t}\n      -\t}\n      -\tfor (i = get_max_object_index(the_repository); 0 < i; i--) {\n      -\t\to = get_indexed_object(the_repository, i - 1);\n     @@ upload-pack.c: error:\n      -\t\t    (o->flags & TMP_MARK)) {\n      -\t\t\tadd_object_array(o, NULL, reachable);\n      -\t\t\t\to->flags &= ~TMP_MARK;\n     -+\t\tparse_commit_or_die(commit);\n     -+\t\tcur_depth++;\n     -+\t\tfor (j = 0; j < shallows->nr; j++)\n     -+\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n     -+\t\t\t\tif ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))\n     -+\t\t\t\t\tcur_depth_shallow = cur_depth;\n     -+\n     -+\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n     -+\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n     -+\t\t     graft->nr_parent < 0)) {\n     -+\t\t\tcommit = NULL;\n     -+\t\t\tcontinue;\n     -+\t\t}\n     -+\t\tfor (p = commit->parents, commit = NULL; p; p = p->next) {\n     -+\t\t\tint **depth_slot = commit_depth_at(&depths, p->item);\n     -+\t\t\tif (!*depth_slot) {\n     -+\t\t\t\t*depth_slot = xmalloc(sizeof(int));\n     -+\t\t\t\t**depth_slot = cur_depth;\n     -+\t\t\t} else {\n     -+\t\t\t\tif (cur_depth >= **depth_slot)\n     -+\t\t\t\t\tcontinue;\n     -+\t\t\t\t**depth_slot = cur_depth;\n     -+\t\t\t}\n     -+\t\t\tif (p->next)\n     -+\t\t\t\tadd_object_array(&p->item->object,\n     -+\t\t\t\t\t\tNULL, &stack);\n     -+\t\t\telse {\n     -+\t\t\t\tcommit = p->item;\n     -+\t\t\t\tcur_depth = **commit_depth_at(&depths, commit);\n     -+\t\t\t}\n     - \t\t}\n     - \t}\n     +-\t\t}\n     +-\t}\n      -\tclose(cmd.out);\n      -\n      -\tif (finish_command(&cmd)) {\n     @@ upload-pack.c: error:\n      -out:\n      -\tchild_process_clear(&cmd);\n      -\treturn ret;\n     -+\tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n     -+\tobject_array_clear(&stack);\n     -+\tdata->deepen_relative = cur_depth_shallow;\n     ++\tget_shallow_commits(&data->want_obj, &data->shallows,\n     ++\t\t\t    &data->deepen_relative, 0,\n     ++\t\t\t    SHALLOW, NOT_SHALLOW);\n       }\n       \n       static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)\n     @@ upload-pack.c: static void deepen(struct upload_pack_data *data, int depth)\n      +\t\tif (data->deepen_relative)\n      +\t\t\tget_shallows_depth(data);\n      +\n     -+\t\tresult = get_shallow_commits(&data->want_obj,\n     ++\t\tresult = get_shallow_commits(&data->want_obj, NULL, NULL,\n      +\t\t\t\t\t     data->deepen_relative + depth,\n       \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n       \t\tsend_shallow(data, result);\n\n-- \ngitgitgadget\n"},{"id":"533449","messageId":"f8a8d077cd7203663406bf656287ed7e0d6e6d90.1767997426.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v2.git.git.1767997426.gitgitgadget@gmail.com","subject":"[PATCH v2 1/2] shallow: free local object_array allocations","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T22:23:45Z","receivedAt":"2026-01-09T22:23:49Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nThe local object_array 'stack' in get_shallow_commits() function\ndoes not free its dynamic elements before the function returns.\nAs a result elements remain allocated and their reference forgotten.\n\nAlso note, that test 'fetching deepen beyond merged branch' added by\n'shallow: handling fetch relative-deepen' patch fails without this\ncorrection in linux-leaks and linux-reftable-leaks test runs.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n shallow.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/shallow.c b/shallow.c\nindex 55b9cd9d3f..497a25836b 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t}\n \t}\n \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n+\tobject_array_clear(&stack);\n \n \treturn result;\n }\n-- \ngitgitgadget\n\n"},{"id":"533451","messageId":"ba1f80105f3e1c3dc6d133fb319e1df69a19bf8f.1767997426.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v2.git.git.1767997426.gitgitgadget@gmail.com","subject":"[PATCH v2 2/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T22:23:46Z","receivedAt":"2026-01-09T22:23:51Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nWhen a shallowed repository gets deepened beyond the beginning of a\nmerged branch, we may end up with some shallows that are hidden behind\nthe reachable shallow commits. Added test 'fetching deepen beyond\nmerged branch' exposes that behaviour.\n\nAn example showing the problem based on added test:\n0. Whole initial git repo to be cloned from\nGraph:\n*   033585d (HEAD -> main) Merge branch 'branch'\n|\\\n| * 984f8b1 (branch) five\n| * ecb578a four\n|/\n* 0cb5d20 three\n* 2b4e70d two\n* 61ba98b one\n\n1. Initial shallow clone --depth=3 (all good)\nShallows:\n2b4e70da2a10e1d3231a0ae2df396024735601f1\necb578a3cf37198d122ae5df7efed9abaca17144\nGraph:\n*   033585d (HEAD -> main) Merge branch 'branch'\n|\\\n| * 984f8b1 five\n| * ecb578a (grafted) four\n* 0cb5d20 three\n* 2b4e70d (grafted) two\n\n2. Deepen shallow clone with fetch --deepen=1 (NOT OK)\nShallows:\n0cb5d204f4ef96ed241feb0f2088c9f4794ba758\n61ba98be443fd51c542eb66585a1f6d7e15fcdae\nGraph:\n*   033585d (HEAD -> main) Merge branch 'branch'\n|\\\n| * 984f8b1 five\n| * ecb578a four\n|/\n* 0cb5d20 (grafted) three\n---\nNote that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae\nis not reachable.\n\nOn the other hand, it seems that equivalent absolute depth driven\nfetches result in all the correct shallows. That led to this proposal,\nwhich unifies absolute and relative deepening in a way that the same\nget_shallow_commits() call is used in both cases. The difference is\nonly that depth is adapted for relative deepening by measuring\nequivalent depth of current local shallow commits in the current remote\nrepo. Thus a new function get_shallows_depth() has been added and the\nfunction get_reachable_list() became redundant / removed.\n\nSame example showing the corrected second step:\n2. Deepen shallow clone with fetch --deepen=1 (all good)\nShallow:\n61ba98be443fd51c542eb66585a1f6d7e15fcdae\nGraph:\n*   033585d (HEAD -> main) Merge branch 'branch'\n|\\\n| * 984f8b1 five\n| * ecb578a four\n|/\n* 0cb5d20 three\n* 2b4e70d two\n* 61ba98b (grafted) one\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of\neach existing shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative\ndeepening anyway. That way we can allways summ 'data->deepen_relative'\nand 'depth' values, because 'data->deepen_relative' is always 0 in\nabsolute deepening.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n shallow.c             | 44 +++++++++++++++++--------\n shallow.h             |  1 +\n t/t5500-fetch-pack.sh | 23 +++++++++++++\n upload-pack.c         | 76 +++++--------------------------------------\n 4 files changed, 63 insertions(+), 81 deletions(-)\n\ndiff --git a/shallow.c b/shallow.c\nindex 497a25836b..1a32808865 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)\n {\n \tFREE_AND_NULL(*ptr);\n }\n-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n-\t\tint shallow_flag, int not_shallow_flag)\n+struct commit_list *get_shallow_commits(struct object_array *heads,\n+\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n+\t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n {\n-\tsize_t i = 0;\n-\tint cur_depth = 0;\n+\tsize_t i = 0, j;\n+\tint cur_depth = 0, cur_depth_shallow = 0;\n \tstruct commit_list *result = NULL;\n \tstruct object_array stack = OBJECT_ARRAY_INIT;\n \tstruct commit *commit = NULL;\n@@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t}\n \t\tparse_commit_or_die(commit);\n \t\tcur_depth++;\n-\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n-\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n-\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n-\t\t     graft->nr_parent < 0)) {\n-\t\t\tcommit_list_insert(commit, &result);\n-\t\t\tcommit->object.flags |= shallow_flag;\n-\t\t\tcommit = NULL;\n-\t\t\tcontinue;\n+\t\tif (shallows) {\n+\t\t\tfor (j = 0; j < shallows->nr; j++)\n+\t\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n+\t\t\t\t\tif ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))\n+\t\t\t\t\t\tcur_depth_shallow = cur_depth;\n+\n+\t\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n+\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n+\t\t\t     graft->nr_parent < 0)) {\n+\t\t\t\tcommit = NULL;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n+\t\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n+\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n+\t\t\t     graft->nr_parent < 0)) {\n+\t\t\t\tcommit_list_insert(commit, &result);\n+\t\t\t\tcommit->object.flags |= shallow_flag;\n+\t\t\t\tcommit = NULL;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tcommit->object.flags |= not_shallow_flag;\n \t\t}\n-\t\tcommit->object.flags |= not_shallow_flag;\n \t\tfor (p = commit->parents, commit = NULL; p; p = p->next) {\n \t\t\tint **depth_slot = commit_depth_at(&depths, p->item);\n \t\t\tif (!*depth_slot) {\n@@ -199,7 +214,8 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t}\n \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n \tobject_array_clear(&stack);\n-\n+\tif (shallows && deepen_relative)\n+\t\t*deepen_relative = cur_depth_shallow;\n \treturn result;\n }\n \ndiff --git a/shallow.h b/shallow.h\nindex ad591bd139..d1b3878635 100644\n--- a/shallow.h\n+++ b/shallow.h\n@@ -36,6 +36,7 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);\n void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);\n \n struct commit_list *get_shallow_commits(struct object_array *heads,\n+\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n \t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag);\n struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n \t\t\t\t\t\t    int shallow_flag, int not_shallow_flag);\ndiff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\nindex 2677cd5faa..5a8b30e1fd 100755\n--- a/t/t5500-fetch-pack.sh\n+++ b/t/t5500-fetch-pack.sh\n@@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '\n \t)\n '\n \n+test_expect_success 'fetching deepen beyond merged branch' '\n+\ttest_create_repo shallow-deepen-merged &&\n+\t(\n+\t\tcd shallow-deepen-merged &&\n+\t\tgit commit --allow-empty -m one &&\n+\t\tgit commit --allow-empty -m two &&\n+\t\tgit commit --allow-empty -m three &&\n+\t\tgit switch -c branch &&\n+\t\tgit commit --allow-empty -m four &&\n+\t\tgit commit --allow-empty -m five &&\n+\t\tgit switch main &&\n+\t\tgit merge --no-ff branch &&\n+\t\tcd - &&\n+\t\tgit clone --bare --depth 3 \"file://$(pwd)/shallow-deepen-merged\" deepen.git &&\n+\t\tgit -C deepen.git fetch origin --deepen=1 &&\n+\t\tgit -C deepen.git rev-list --all >actual &&\n+\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow)\n+\t\tdo\n+\t\t\ttest_grep \"$commit\" actual || exit 1\n+\t\tdone\n+\t)\n+'\n+\n test_negotiation_algorithm_default () {\n \ttest_when_finished rm -rf clientv0 clientv2 &&\n \trm -rf server client &&\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 2d2b70cbf2..4232eef34f 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -704,54 +704,11 @@ error:\n \treturn -1;\n }\n \n-static int get_reachable_list(struct upload_pack_data *data,\n-\t\t\t      struct object_array *reachable)\n+static void get_shallows_depth(struct upload_pack_data *data)\n {\n-\tstruct child_process cmd = CHILD_PROCESS_INIT;\n-\tint i;\n-\tstruct object *o;\n-\tchar namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */\n-\tconst unsigned hexsz = the_hash_algo->hexsz;\n-\tint ret;\n-\n-\tif (do_reachable_revlist(&cmd, &data->shallows, reachable,\n-\t\t\t\t data->allow_uor) < 0) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\twhile ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {\n-\t\tstruct object_id oid;\n-\t\tconst char *p;\n-\n-\t\tif (parse_oid_hex(namebuf, &oid, &p) || *p != '\\n')\n-\t\t\tbreak;\n-\n-\t\to = lookup_object(the_repository, &oid);\n-\t\tif (o && o->type == OBJ_COMMIT) {\n-\t\t\to->flags &= ~TMP_MARK;\n-\t\t}\n-\t}\n-\tfor (i = get_max_object_index(the_repository); 0 < i; i--) {\n-\t\to = get_indexed_object(the_repository, i - 1);\n-\t\tif (o && o->type == OBJ_COMMIT &&\n-\t\t    (o->flags & TMP_MARK)) {\n-\t\t\tadd_object_array(o, NULL, reachable);\n-\t\t\t\to->flags &= ~TMP_MARK;\n-\t\t}\n-\t}\n-\tclose(cmd.out);\n-\n-\tif (finish_command(&cmd)) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tret = 0;\n-\n-out:\n-\tchild_process_clear(&cmd);\n-\treturn ret;\n+\tget_shallow_commits(&data->want_obj, &data->shallows,\n+\t\t\t    &data->deepen_relative, 0,\n+\t\t\t    SHALLOW, NOT_SHALLOW);\n }\n \n static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)\n@@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)\n \t\t\tstruct object *object = data->shallows.objects[i].item;\n \t\t\tobject->flags |= NOT_SHALLOW;\n \t\t}\n-\t} else if (data->deepen_relative) {\n-\t\tstruct object_array reachable_shallows = OBJECT_ARRAY_INIT;\n-\t\tstruct commit_list *result;\n-\n-\t\t/*\n-\t\t * Checking for reachable shallows requires that our refs be\n-\t\t * marked with OUR_REF.\n-\t\t */\n-\t\trefs_head_ref_namespaced(get_main_ref_store(the_repository),\n-\t\t\t\t\t check_ref, data);\n-\t\tfor_each_namespaced_ref_1(check_ref, data);\n-\n-\t\tget_reachable_list(data, &reachable_shallows);\n-\t\tresult = get_shallow_commits(&reachable_shallows,\n-\t\t\t\t\t     depth + 1,\n-\t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n-\t\tsend_shallow(data, result);\n-\t\tfree_commit_list(result);\n-\t\tobject_array_clear(&reachable_shallows);\n \t} else {\n \t\tstruct commit_list *result;\n \n-\t\tresult = get_shallow_commits(&data->want_obj, depth,\n+\t\tif (data->deepen_relative)\n+\t\t\tget_shallows_depth(data);\n+\n+\t\tresult = get_shallow_commits(&data->want_obj, NULL, NULL,\n+\t\t\t\t\t     data->deepen_relative + depth,\n \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n \t\tsend_shallow(data, result);\n \t\tfree_commit_list(result);\n-- \ngitgitgadget\n"},{"id":"533470","messageId":"xmqqsecertib.fsf@gitster.g","threadId":"64603","inReplyTo":"ba1f80105f3e1c3dc6d133fb319e1df69a19bf8f.1767997426.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 2/2] shallow: handling fetch relative-deepen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-10T04:17:32Z","receivedAt":"2026-01-10T04:17:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Samo Pogačnik via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> 2. Deepen shallow clone with fetch --deepen=1 (NOT OK)\n> Shallows:\n> 0cb5d204f4ef96ed241feb0f2088c9f4794ba758\n> 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n> Graph:\n> *   033585d (HEAD -> main) Merge branch 'branch'\n> |\\\n> | * 984f8b1 five\n> | * ecb578a four\n> |/\n> * 0cb5d20 (grafted) three\n> ---\n\nThis three-dash line will act as a marker to tell \"git am\" that your\nlog message ends here.\n\nTo avoid such an accident, make it a habit to indent any and all\ndisplayed material used as examples, e.g.,\n\n2. Deepen shallow clone with fetch --deepen=1 (NOT OK)\n\n   Shallows:\n   0cb5d204f4ef96ed241feb0f2088c9f4794ba758\n   61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a four\n   |/\n   * 0cb5d20 (grafted) three\n   ---\n\n\n\n> Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n> is not reachable.\n>\n> On the other hand, it seems that equivalent absolute depth driven\n> fetches result in all the correct shallows. That led to this proposal,\n> which unifies absolute and relative deepening in a way that the same\n> get_shallow_commits() call is used in both cases. The difference is\n> only that depth is adapted for relative deepening by measuring\n> equivalent depth of current local shallow commits in the current remote\n> repo. Thus a new function get_shallows_depth() has been added and the\n> function get_reachable_list() became redundant / removed.\n>\n> Same example showing the corrected second step:\n> 2. Deepen shallow clone with fetch --deepen=1 (all good)\n> Shallow:\n> 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n> Graph:\n> *   033585d (HEAD -> main) Merge branch 'branch'\n> |\\\n> | * 984f8b1 five\n> | * ecb578a four\n> |/\n> * 0cb5d20 three\n> * 2b4e70d two\n> * 61ba98b (grafted) one\n>\n> The get_shallows_depth() function also shares the logic of the\n> get_shallow_commits() function, but it focuses on counting depth of\n> each existing shallow commit. The minimum result is stored as\n> 'data->deepen_relative', which is set not to be zero for relative\n> deepening anyway. That way we can allways summ 'data->deepen_relative'\n> and 'depth' values, because 'data->deepen_relative' is always 0 in\n> absolute deepening.\n>\n> Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n> ---\n>  shallow.c             | 44 +++++++++++++++++--------\n>  shallow.h             |  1 +\n>  t/t5500-fetch-pack.sh | 23 +++++++++++++\n>  upload-pack.c         | 76 +++++--------------------------------------\n>  4 files changed, 63 insertions(+), 81 deletions(-)\n>\n> diff --git a/shallow.c b/shallow.c\n> index 497a25836b..1a32808865 100644\n> --- a/shallow.c\n> +++ b/shallow.c\n> @@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)\n>  {\n>  \tFREE_AND_NULL(*ptr);\n>  }\n> -struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n> -\t\tint shallow_flag, int not_shallow_flag)\n> +struct commit_list *get_shallow_commits(struct object_array *heads,\n> +\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n> +\t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n>  {\n> -\tsize_t i = 0;\n> -\tint cur_depth = 0;\n> +\tsize_t i = 0, j;\n> +\tint cur_depth = 0, cur_depth_shallow = 0;\n>  \tstruct commit_list *result = NULL;\n>  \tstruct object_array stack = OBJECT_ARRAY_INIT;\n>  \tstruct commit *commit = NULL;\n> @@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n>  \t\t}\n>  \t\tparse_commit_or_die(commit);\n>  \t\tcur_depth++;\n> -\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n> -\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n> -\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n> -\t\t     graft->nr_parent < 0)) {\n> -\t\t\tcommit_list_insert(commit, &result);\n> -\t\t\tcommit->object.flags |= shallow_flag;\n> -\t\t\tcommit = NULL;\n> -\t\t\tcontinue;\n> +\t\tif (shallows) {\n> +\t\t\tfor (j = 0; j < shallows->nr; j++)\n> +\t\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n> +\t\t\t\t\tif ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))\n> +\t\t\t\t\t\tcur_depth_shallow = cur_depth;\n> +\n> +\t\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n> +\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n> +\t\t\t     graft->nr_parent < 0)) {\n> +\t\t\t\tcommit = NULL;\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n> +\t\t} else {\n> +\t\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n> +\t\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n> +\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n> +\t\t\t     graft->nr_parent < 0)) {\n> +\t\t\t\tcommit_list_insert(commit, &result);\n> +\t\t\t\tcommit->object.flags |= shallow_flag;\n> +\t\t\t\tcommit = NULL;\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n> +\t\t\tcommit->object.flags |= not_shallow_flag;\n>  \t\t}\n> -\t\tcommit->object.flags |= not_shallow_flag;\n>  \t\tfor (p = commit->parents, commit = NULL; p; p = p->next) {\n>  \t\t\tint **depth_slot = commit_depth_at(&depths, p->item);\n>  \t\t\tif (!*depth_slot) {\n> @@ -199,7 +214,8 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n>  \t}\n>  \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n>  \tobject_array_clear(&stack);\n> -\n> +\tif (shallows && deepen_relative)\n> +\t\t*deepen_relative = cur_depth_shallow;\n>  \treturn result;\n>  }\n>  \n> diff --git a/shallow.h b/shallow.h\n> index ad591bd139..d1b3878635 100644\n> --- a/shallow.h\n> +++ b/shallow.h\n> @@ -36,6 +36,7 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);\n>  void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);\n>  \n>  struct commit_list *get_shallow_commits(struct object_array *heads,\n> +\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n>  \t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag);\n>  struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n>  \t\t\t\t\t\t    int shallow_flag, int not_shallow_flag);\n> diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\n> index 2677cd5faa..5a8b30e1fd 100755\n> --- a/t/t5500-fetch-pack.sh\n> +++ b/t/t5500-fetch-pack.sh\n> @@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '\n>  \t)\n>  '\n>  \n> +test_expect_success 'fetching deepen beyond merged branch' '\n> +\ttest_create_repo shallow-deepen-merged &&\n> +\t(\n> +\t\tcd shallow-deepen-merged &&\n> +\t\tgit commit --allow-empty -m one &&\n> +\t\tgit commit --allow-empty -m two &&\n> +\t\tgit commit --allow-empty -m three &&\n> +\t\tgit switch -c branch &&\n> +\t\tgit commit --allow-empty -m four &&\n> +\t\tgit commit --allow-empty -m five &&\n> +\t\tgit switch main &&\n> +\t\tgit merge --no-ff branch &&\n> +\t\tcd - &&\n> +\t\tgit clone --bare --depth 3 \"file://$(pwd)/shallow-deepen-merged\" deepen.git &&\n> +\t\tgit -C deepen.git fetch origin --deepen=1 &&\n> +\t\tgit -C deepen.git rev-list --all >actual &&\n> +\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow)\n> +\t\tdo\n> +\t\t\ttest_grep \"$commit\" actual || exit 1\n> +\t\tdone\n> +\t)\n> +'\n> +\n>  test_negotiation_algorithm_default () {\n>  \ttest_when_finished rm -rf clientv0 clientv2 &&\n>  \trm -rf server client &&\n> diff --git a/upload-pack.c b/upload-pack.c\n> index 2d2b70cbf2..4232eef34f 100644\n> --- a/upload-pack.c\n> +++ b/upload-pack.c\n> @@ -704,54 +704,11 @@ error:\n>  \treturn -1;\n>  }\n>  \n> -static int get_reachable_list(struct upload_pack_data *data,\n> -\t\t\t      struct object_array *reachable)\n> +static void get_shallows_depth(struct upload_pack_data *data)\n>  {\n> -\tstruct child_process cmd = CHILD_PROCESS_INIT;\n> -\tint i;\n> -\tstruct object *o;\n> -\tchar namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */\n> -\tconst unsigned hexsz = the_hash_algo->hexsz;\n> -\tint ret;\n> -\n> -\tif (do_reachable_revlist(&cmd, &data->shallows, reachable,\n> -\t\t\t\t data->allow_uor) < 0) {\n> -\t\tret = -1;\n> -\t\tgoto out;\n> -\t}\n> -\n> -\twhile ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {\n> -\t\tstruct object_id oid;\n> -\t\tconst char *p;\n> -\n> -\t\tif (parse_oid_hex(namebuf, &oid, &p) || *p != '\\n')\n> -\t\t\tbreak;\n> -\n> -\t\to = lookup_object(the_repository, &oid);\n> -\t\tif (o && o->type == OBJ_COMMIT) {\n> -\t\t\to->flags &= ~TMP_MARK;\n> -\t\t}\n> -\t}\n> -\tfor (i = get_max_object_index(the_repository); 0 < i; i--) {\n> -\t\to = get_indexed_object(the_repository, i - 1);\n> -\t\tif (o && o->type == OBJ_COMMIT &&\n> -\t\t    (o->flags & TMP_MARK)) {\n> -\t\t\tadd_object_array(o, NULL, reachable);\n> -\t\t\t\to->flags &= ~TMP_MARK;\n> -\t\t}\n> -\t}\n> -\tclose(cmd.out);\n> -\n> -\tif (finish_command(&cmd)) {\n> -\t\tret = -1;\n> -\t\tgoto out;\n> -\t}\n> -\n> -\tret = 0;\n> -\n> -out:\n> -\tchild_process_clear(&cmd);\n> -\treturn ret;\n> +\tget_shallow_commits(&data->want_obj, &data->shallows,\n> +\t\t\t    &data->deepen_relative, 0,\n> +\t\t\t    SHALLOW, NOT_SHALLOW);\n>  }\n>  \n>  static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)\n> @@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)\n>  \t\t\tstruct object *object = data->shallows.objects[i].item;\n>  \t\t\tobject->flags |= NOT_SHALLOW;\n>  \t\t}\n> -\t} else if (data->deepen_relative) {\n> -\t\tstruct object_array reachable_shallows = OBJECT_ARRAY_INIT;\n> -\t\tstruct commit_list *result;\n> -\n> -\t\t/*\n> -\t\t * Checking for reachable shallows requires that our refs be\n> -\t\t * marked with OUR_REF.\n> -\t\t */\n> -\t\trefs_head_ref_namespaced(get_main_ref_store(the_repository),\n> -\t\t\t\t\t check_ref, data);\n> -\t\tfor_each_namespaced_ref_1(check_ref, data);\n> -\n> -\t\tget_reachable_list(data, &reachable_shallows);\n> -\t\tresult = get_shallow_commits(&reachable_shallows,\n> -\t\t\t\t\t     depth + 1,\n> -\t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n> -\t\tsend_shallow(data, result);\n> -\t\tfree_commit_list(result);\n> -\t\tobject_array_clear(&reachable_shallows);\n>  \t} else {\n>  \t\tstruct commit_list *result;\n>  \n> -\t\tresult = get_shallow_commits(&data->want_obj, depth,\n> +\t\tif (data->deepen_relative)\n> +\t\t\tget_shallows_depth(data);\n> +\n> +\t\tresult = get_shallow_commits(&data->want_obj, NULL, NULL,\n> +\t\t\t\t\t     data->deepen_relative + depth,\n>  \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n>  \t\tsend_shallow(data, result);\n>  \t\tfree_commit_list(result);\n"},{"id":"533471","messageId":"pull.2121.v3.git.git.1768022018.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v2.git.git.1767997426.gitgitgadget@gmail.com","subject":"[PATCH v3 0/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T05:13:36Z","receivedAt":"2026-01-10T05:13:40Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"When a shallowed repository gets deepened beyond the beginning of a merged\nbranch, we may endup with some shallows, that are behind the reachable ones.\nAdded test 'fetching deepen beyond merged branch' exposes that behaviour.\n\nOn the other hand, it seems that equivalent absolute depth driven fetches\nresult in all the correct shallows. That led to this proposal, which unifies\nabsolute and relative deepening in a way that the same get_shallow_commits()\ncall is used in both cases. The difference is only that depth is adapted for\nrelative deepening by measuring equivalent depth of current local shallow\ncommits in the current remote repo. Thus a new function get_shallows_depth()\nhas been added and the function get_reachable_list() became redundant /\nremoved.\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of each\nexisting shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative deepening\nanyway. That way we can allways summ 'data->deepen_relative' and 'depth'\nvalues, because 'data->deepen_relative' is always 0 in absolute deepening.\n\nSamo Pogačnik (2):\n  shallow: free local object_array allocations\n  shallow: handling fetch relative-deepen\n\n shallow.c             | 45 +++++++++++++++++--------\n shallow.h             |  1 +\n t/t5500-fetch-pack.sh | 23 +++++++++++++\n upload-pack.c         | 76 +++++--------------------------------------\n 4 files changed, 64 insertions(+), 81 deletions(-)\n\n\nbase-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v3\nPull-Request: https://github.com/git/git/pull/2121\n\nRange-diff vs v2:\n\n 1:  f8a8d077cd = 1:  f8a8d077cd shallow: free local object_array allocations\n 2:  ba1f80105f ! 2:  e79ab6b740 shallow: handling fetch relative-deepen\n     @@ Commit message\n      \n          An example showing the problem based on added test:\n          0. Whole initial git repo to be cloned from\n     -    Graph:\n     -    *   033585d (HEAD -> main) Merge branch 'branch'\n     -    |\\\n     -    | * 984f8b1 (branch) five\n     -    | * ecb578a four\n     -    |/\n     -    * 0cb5d20 three\n     -    * 2b4e70d two\n     -    * 61ba98b one\n     +       Graph:\n     +       *   033585d (HEAD -> main) Merge branch 'branch'\n     +       |\\\n     +       | * 984f8b1 (branch) five\n     +       | * ecb578a four\n     +       |/\n     +       * 0cb5d20 three\n     +       * 2b4e70d two\n     +       * 61ba98b one\n      \n          1. Initial shallow clone --depth=3 (all good)\n     -    Shallows:\n     -    2b4e70da2a10e1d3231a0ae2df396024735601f1\n     -    ecb578a3cf37198d122ae5df7efed9abaca17144\n     -    Graph:\n     -    *   033585d (HEAD -> main) Merge branch 'branch'\n     -    |\\\n     -    | * 984f8b1 five\n     -    | * ecb578a (grafted) four\n     -    * 0cb5d20 three\n     -    * 2b4e70d (grafted) two\n     +       Shallows:\n     +       2b4e70da2a10e1d3231a0ae2df396024735601f1\n     +       ecb578a3cf37198d122ae5df7efed9abaca17144\n     +       Graph:\n     +       *   033585d (HEAD -> main) Merge branch 'branch'\n     +       |\\\n     +       | * 984f8b1 five\n     +       | * ecb578a (grafted) four\n     +       * 0cb5d20 three\n     +       * 2b4e70d (grafted) two\n      \n          2. Deepen shallow clone with fetch --deepen=1 (NOT OK)\n     -    Shallows:\n     -    0cb5d204f4ef96ed241feb0f2088c9f4794ba758\n     -    61ba98be443fd51c542eb66585a1f6d7e15fcdae\n     -    Graph:\n     -    *   033585d (HEAD -> main) Merge branch 'branch'\n     -    |\\\n     -    | * 984f8b1 five\n     -    | * ecb578a four\n     -    |/\n     -    * 0cb5d20 (grafted) three\n     -    ---\n     -    Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n     -    is not reachable.\n     +       Shallows:\n     +       0cb5d204f4ef96ed241feb0f2088c9f4794ba758\n     +       61ba98be443fd51c542eb66585a1f6d7e15fcdae\n     +       Graph:\n     +       *   033585d (HEAD -> main) Merge branch 'branch'\n     +       |\\\n     +       | * 984f8b1 five\n     +       | * ecb578a four\n     +       |/\n     +       * 0cb5d20 (grafted) three\n     +       ---\n     +       Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n     +       is not reachable.\n      \n          On the other hand, it seems that equivalent absolute depth driven\n          fetches result in all the correct shallows. That led to this proposal,\n     @@ Commit message\n      \n          Same example showing the corrected second step:\n          2. Deepen shallow clone with fetch --deepen=1 (all good)\n     -    Shallow:\n     -    61ba98be443fd51c542eb66585a1f6d7e15fcdae\n     -    Graph:\n     -    *   033585d (HEAD -> main) Merge branch 'branch'\n     -    |\\\n     -    | * 984f8b1 five\n     -    | * ecb578a four\n     -    |/\n     -    * 0cb5d20 three\n     -    * 2b4e70d two\n     -    * 61ba98b (grafted) one\n     +       Shallow:\n     +       61ba98be443fd51c542eb66585a1f6d7e15fcdae\n     +       Graph:\n     +       *   033585d (HEAD -> main) Merge branch 'branch'\n     +       |\\\n     +       | * 984f8b1 five\n     +       | * ecb578a four\n     +       |/\n     +       * 0cb5d20 three\n     +       * 2b4e70d two\n     +       * 61ba98b (grafted) one\n      \n          The get_shallows_depth() function also shares the logic of the\n          get_shallow_commits() function, but it focuses on counting depth of\n\n-- \ngitgitgadget\n"},{"id":"533472","messageId":"f8a8d077cd7203663406bf656287ed7e0d6e6d90.1768022018.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v3.git.git.1768022018.gitgitgadget@gmail.com","subject":"[PATCH v3 1/2] shallow: free local object_array allocations","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T05:13:37Z","receivedAt":"2026-01-10T05:13:41Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nThe local object_array 'stack' in get_shallow_commits() function\ndoes not free its dynamic elements before the function returns.\nAs a result elements remain allocated and their reference forgotten.\n\nAlso note, that test 'fetching deepen beyond merged branch' added by\n'shallow: handling fetch relative-deepen' patch fails without this\ncorrection in linux-leaks and linux-reftable-leaks test runs.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n shallow.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/shallow.c b/shallow.c\nindex 55b9cd9d3f..497a25836b 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t}\n \t}\n \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n+\tobject_array_clear(&stack);\n \n \treturn result;\n }\n-- \ngitgitgadget\n\n"},{"id":"533473","messageId":"e79ab6b7406e3472ed8816131d4e33f7d8e696f2.1768022018.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v3.git.git.1768022018.gitgitgadget@gmail.com","subject":"[PATCH v3 2/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T05:13:38Z","receivedAt":"2026-01-10T05:13:42Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nWhen a shallowed repository gets deepened beyond the beginning of a\nmerged branch, we may end up with some shallows that are hidden behind\nthe reachable shallow commits. Added test 'fetching deepen beyond\nmerged branch' exposes that behaviour.\n\nAn example showing the problem based on added test:\n0. Whole initial git repo to be cloned from\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 (branch) five\n   | * ecb578a four\n   |/\n   * 0cb5d20 three\n   * 2b4e70d two\n   * 61ba98b one\n\n1. Initial shallow clone --depth=3 (all good)\n   Shallows:\n   2b4e70da2a10e1d3231a0ae2df396024735601f1\n   ecb578a3cf37198d122ae5df7efed9abaca17144\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a (grafted) four\n   * 0cb5d20 three\n   * 2b4e70d (grafted) two\n\n2. Deepen shallow clone with fetch --deepen=1 (NOT OK)\n   Shallows:\n   0cb5d204f4ef96ed241feb0f2088c9f4794ba758\n   61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a four\n   |/\n   * 0cb5d20 (grafted) three\n   ---\n   Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   is not reachable.\n\nOn the other hand, it seems that equivalent absolute depth driven\nfetches result in all the correct shallows. That led to this proposal,\nwhich unifies absolute and relative deepening in a way that the same\nget_shallow_commits() call is used in both cases. The difference is\nonly that depth is adapted for relative deepening by measuring\nequivalent depth of current local shallow commits in the current remote\nrepo. Thus a new function get_shallows_depth() has been added and the\nfunction get_reachable_list() became redundant / removed.\n\nSame example showing the corrected second step:\n2. Deepen shallow clone with fetch --deepen=1 (all good)\n   Shallow:\n   61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a four\n   |/\n   * 0cb5d20 three\n   * 2b4e70d two\n   * 61ba98b (grafted) one\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of\neach existing shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative\ndeepening anyway. That way we can allways summ 'data->deepen_relative'\nand 'depth' values, because 'data->deepen_relative' is always 0 in\nabsolute deepening.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n shallow.c             | 44 +++++++++++++++++--------\n shallow.h             |  1 +\n t/t5500-fetch-pack.sh | 23 +++++++++++++\n upload-pack.c         | 76 +++++--------------------------------------\n 4 files changed, 63 insertions(+), 81 deletions(-)\n\ndiff --git a/shallow.c b/shallow.c\nindex 497a25836b..1a32808865 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)\n {\n \tFREE_AND_NULL(*ptr);\n }\n-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n-\t\tint shallow_flag, int not_shallow_flag)\n+struct commit_list *get_shallow_commits(struct object_array *heads,\n+\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n+\t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n {\n-\tsize_t i = 0;\n-\tint cur_depth = 0;\n+\tsize_t i = 0, j;\n+\tint cur_depth = 0, cur_depth_shallow = 0;\n \tstruct commit_list *result = NULL;\n \tstruct object_array stack = OBJECT_ARRAY_INIT;\n \tstruct commit *commit = NULL;\n@@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t}\n \t\tparse_commit_or_die(commit);\n \t\tcur_depth++;\n-\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n-\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n-\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n-\t\t     graft->nr_parent < 0)) {\n-\t\t\tcommit_list_insert(commit, &result);\n-\t\t\tcommit->object.flags |= shallow_flag;\n-\t\t\tcommit = NULL;\n-\t\t\tcontinue;\n+\t\tif (shallows) {\n+\t\t\tfor (j = 0; j < shallows->nr; j++)\n+\t\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n+\t\t\t\t\tif ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))\n+\t\t\t\t\t\tcur_depth_shallow = cur_depth;\n+\n+\t\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n+\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n+\t\t\t     graft->nr_parent < 0)) {\n+\t\t\t\tcommit = NULL;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n+\t\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n+\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n+\t\t\t     graft->nr_parent < 0)) {\n+\t\t\t\tcommit_list_insert(commit, &result);\n+\t\t\t\tcommit->object.flags |= shallow_flag;\n+\t\t\t\tcommit = NULL;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tcommit->object.flags |= not_shallow_flag;\n \t\t}\n-\t\tcommit->object.flags |= not_shallow_flag;\n \t\tfor (p = commit->parents, commit = NULL; p; p = p->next) {\n \t\t\tint **depth_slot = commit_depth_at(&depths, p->item);\n \t\t\tif (!*depth_slot) {\n@@ -199,7 +214,8 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t}\n \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n \tobject_array_clear(&stack);\n-\n+\tif (shallows && deepen_relative)\n+\t\t*deepen_relative = cur_depth_shallow;\n \treturn result;\n }\n \ndiff --git a/shallow.h b/shallow.h\nindex ad591bd139..d1b3878635 100644\n--- a/shallow.h\n+++ b/shallow.h\n@@ -36,6 +36,7 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);\n void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);\n \n struct commit_list *get_shallow_commits(struct object_array *heads,\n+\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n \t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag);\n struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n \t\t\t\t\t\t    int shallow_flag, int not_shallow_flag);\ndiff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\nindex 2677cd5faa..5a8b30e1fd 100755\n--- a/t/t5500-fetch-pack.sh\n+++ b/t/t5500-fetch-pack.sh\n@@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '\n \t)\n '\n \n+test_expect_success 'fetching deepen beyond merged branch' '\n+\ttest_create_repo shallow-deepen-merged &&\n+\t(\n+\t\tcd shallow-deepen-merged &&\n+\t\tgit commit --allow-empty -m one &&\n+\t\tgit commit --allow-empty -m two &&\n+\t\tgit commit --allow-empty -m three &&\n+\t\tgit switch -c branch &&\n+\t\tgit commit --allow-empty -m four &&\n+\t\tgit commit --allow-empty -m five &&\n+\t\tgit switch main &&\n+\t\tgit merge --no-ff branch &&\n+\t\tcd - &&\n+\t\tgit clone --bare --depth 3 \"file://$(pwd)/shallow-deepen-merged\" deepen.git &&\n+\t\tgit -C deepen.git fetch origin --deepen=1 &&\n+\t\tgit -C deepen.git rev-list --all >actual &&\n+\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow)\n+\t\tdo\n+\t\t\ttest_grep \"$commit\" actual || exit 1\n+\t\tdone\n+\t)\n+'\n+\n test_negotiation_algorithm_default () {\n \ttest_when_finished rm -rf clientv0 clientv2 &&\n \trm -rf server client &&\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 2d2b70cbf2..4232eef34f 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -704,54 +704,11 @@ error:\n \treturn -1;\n }\n \n-static int get_reachable_list(struct upload_pack_data *data,\n-\t\t\t      struct object_array *reachable)\n+static void get_shallows_depth(struct upload_pack_data *data)\n {\n-\tstruct child_process cmd = CHILD_PROCESS_INIT;\n-\tint i;\n-\tstruct object *o;\n-\tchar namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */\n-\tconst unsigned hexsz = the_hash_algo->hexsz;\n-\tint ret;\n-\n-\tif (do_reachable_revlist(&cmd, &data->shallows, reachable,\n-\t\t\t\t data->allow_uor) < 0) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\twhile ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {\n-\t\tstruct object_id oid;\n-\t\tconst char *p;\n-\n-\t\tif (parse_oid_hex(namebuf, &oid, &p) || *p != '\\n')\n-\t\t\tbreak;\n-\n-\t\to = lookup_object(the_repository, &oid);\n-\t\tif (o && o->type == OBJ_COMMIT) {\n-\t\t\to->flags &= ~TMP_MARK;\n-\t\t}\n-\t}\n-\tfor (i = get_max_object_index(the_repository); 0 < i; i--) {\n-\t\to = get_indexed_object(the_repository, i - 1);\n-\t\tif (o && o->type == OBJ_COMMIT &&\n-\t\t    (o->flags & TMP_MARK)) {\n-\t\t\tadd_object_array(o, NULL, reachable);\n-\t\t\t\to->flags &= ~TMP_MARK;\n-\t\t}\n-\t}\n-\tclose(cmd.out);\n-\n-\tif (finish_command(&cmd)) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tret = 0;\n-\n-out:\n-\tchild_process_clear(&cmd);\n-\treturn ret;\n+\tget_shallow_commits(&data->want_obj, &data->shallows,\n+\t\t\t    &data->deepen_relative, 0,\n+\t\t\t    SHALLOW, NOT_SHALLOW);\n }\n \n static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)\n@@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)\n \t\t\tstruct object *object = data->shallows.objects[i].item;\n \t\t\tobject->flags |= NOT_SHALLOW;\n \t\t}\n-\t} else if (data->deepen_relative) {\n-\t\tstruct object_array reachable_shallows = OBJECT_ARRAY_INIT;\n-\t\tstruct commit_list *result;\n-\n-\t\t/*\n-\t\t * Checking for reachable shallows requires that our refs be\n-\t\t * marked with OUR_REF.\n-\t\t */\n-\t\trefs_head_ref_namespaced(get_main_ref_store(the_repository),\n-\t\t\t\t\t check_ref, data);\n-\t\tfor_each_namespaced_ref_1(check_ref, data);\n-\n-\t\tget_reachable_list(data, &reachable_shallows);\n-\t\tresult = get_shallow_commits(&reachable_shallows,\n-\t\t\t\t\t     depth + 1,\n-\t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n-\t\tsend_shallow(data, result);\n-\t\tfree_commit_list(result);\n-\t\tobject_array_clear(&reachable_shallows);\n \t} else {\n \t\tstruct commit_list *result;\n \n-\t\tresult = get_shallow_commits(&data->want_obj, depth,\n+\t\tif (data->deepen_relative)\n+\t\t\tget_shallows_depth(data);\n+\n+\t\tresult = get_shallow_commits(&data->want_obj, NULL, NULL,\n+\t\t\t\t\t     data->deepen_relative + depth,\n \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n \t\tsend_shallow(data, result);\n \t\tfree_commit_list(result);\n-- \ngitgitgadget\n"},{"id":"533965","messageId":"11b951ab-b624-4ab8-b7b1-fe41a40c9d0e@app.fastmail.com","threadId":"64603","inReplyTo":"e79ab6b7406e3472ed8816131d4e33f7d8e696f2.1768022018.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 2/2] shallow: handling fetch relative-deepen","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-01-15T15:50:17Z","receivedAt":"2026-01-15T15:50:41Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Sat, Jan 10, 2026, at 06:13, Samo Pogačnik via GitGitGadget wrote:\n> When a shallowed repository gets deepened beyond the beginning of a\n> merged branch, we may end up with some shallows that are hidden behind\n> the reachable shallow commits. Added test 'fetching deepen beyond\n> merged branch' exposes that behaviour.\n>\n> An example showing the problem based on added test:\n>[snip]\n>    ---\n>    Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n>    is not reachable.\n>\n> On the other hand, it seems that equivalent absolute depth driven\n> fetches result in all the correct shallows. That led to this proposal,\n> which unifies absolute and relative deepening in a way that the same\n> get_shallow_commits() call is used in both cases. The difference is\n> only that depth is adapted for relative deepening by measuring\n> equivalent depth of current local shallow commits in the current remote\n> repo. Thus a new function get_shallows_depth() has been added and the\n> function get_reachable_list() became redundant / removed.\n>\n> Same example showing the corrected second step:\n>[snip]\n>\n> The get_shallows_depth() function also shares the logic of the\n> get_shallow_commits() function, but it focuses on counting depth of\n> each existing shallow commit. The minimum result is stored as\n> 'data->deepen_relative', which is set not to be zero for relative\n> deepening anyway. That way we can allways summ 'data->deepen_relative'\n\ns/allways summ/always sum/ ?\n\n> and 'depth' values, because 'data->deepen_relative' is always 0 in\n> absolute deepening.\n>\n> Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n> ---\n>[snip]\n"},{"id":"534103","messageId":"pull.2121.v4.git.git.1768602661.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v3.git.git.1768022018.gitgitgadget@gmail.com","subject":"[PATCH v4 0/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-16T22:30:59Z","receivedAt":"2026-01-16T22:31:06Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"When a shallowed repository gets deepened beyond the beginning of a merged\nbranch, we may endup with some shallows, that are behind the reachable ones.\nAdded test 'fetching deepen beyond merged branch' exposes that behaviour.\n\nOn the other hand, it seems that equivalent absolute depth driven fetches\nresult in all the correct shallows. That led to this proposal, which unifies\nabsolute and relative deepening in a way that the same get_shallow_commits()\ncall is used in both cases. The difference is only that depth is adapted for\nrelative deepening by measuring equivalent depth of current local shallow\ncommits in the current remote repo. Thus a new function get_shallows_depth()\nhas been added and the function get_reachable_list() became redundant /\nremoved.\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of each\nexisting shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative deepening\nanyway. That way we can allways summ 'data->deepen_relative' and 'depth'\nvalues, because 'data->deepen_relative' is always 0 in absolute deepening.\n\nSamo Pogačnik (2):\n  shallow: free local object_array allocations\n  shallow: handling fetch relative-deepen\n\n shallow.c             | 45 +++++++++++++++++--------\n shallow.h             |  1 +\n t/t5500-fetch-pack.sh | 23 +++++++++++++\n upload-pack.c         | 76 +++++--------------------------------------\n 4 files changed, 64 insertions(+), 81 deletions(-)\n\n\nbase-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v4\nPull-Request: https://github.com/git/git/pull/2121\n\nRange-diff vs v3:\n\n 1:  f8a8d077cd = 1:  f8a8d077cd shallow: free local object_array allocations\n 2:  e79ab6b740 ! 2:  e9b20ae06f shallow: handling fetch relative-deepen\n     @@ Commit message\n          get_shallow_commits() function, but it focuses on counting depth of\n          each existing shallow commit. The minimum result is stored as\n          'data->deepen_relative', which is set not to be zero for relative\n     -    deepening anyway. That way we can allways summ 'data->deepen_relative'\n     +    deepening anyway. That way we can always sum 'data->deepen_relative'\n          and 'depth' values, because 'data->deepen_relative' is always 0 in\n          absolute deepening.\n     +    To avoid duplicating logic between get_shallows_depth() and\n     +    get_shallow_commits(), get_shallow_commits() was modified so that\n     +    it is used by get_shallows_depth().\n      \n          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n      \n\n-- \ngitgitgadget\n"},{"id":"534104","messageId":"f8a8d077cd7203663406bf656287ed7e0d6e6d90.1768602661.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v4.git.git.1768602661.gitgitgadget@gmail.com","subject":"[PATCH v4 1/2] shallow: free local object_array allocations","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-16T22:31:00Z","receivedAt":"2026-01-16T22:31:09Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nThe local object_array 'stack' in get_shallow_commits() function\ndoes not free its dynamic elements before the function returns.\nAs a result elements remain allocated and their reference forgotten.\n\nAlso note, that test 'fetching deepen beyond merged branch' added by\n'shallow: handling fetch relative-deepen' patch fails without this\ncorrection in linux-leaks and linux-reftable-leaks test runs.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n shallow.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/shallow.c b/shallow.c\nindex 55b9cd9d3f..497a25836b 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t}\n \t}\n \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n+\tobject_array_clear(&stack);\n \n \treturn result;\n }\n-- \ngitgitgadget\n\n"},{"id":"534105","messageId":"e9b20ae06fd2c7f2c6b73c9f093a23c812227b7e.1768602661.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v4.git.git.1768602661.gitgitgadget@gmail.com","subject":"[PATCH v4 2/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-16T22:31:01Z","receivedAt":"2026-01-16T22:31:11Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nWhen a shallowed repository gets deepened beyond the beginning of a\nmerged branch, we may end up with some shallows that are hidden behind\nthe reachable shallow commits. Added test 'fetching deepen beyond\nmerged branch' exposes that behaviour.\n\nAn example showing the problem based on added test:\n0. Whole initial git repo to be cloned from\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 (branch) five\n   | * ecb578a four\n   |/\n   * 0cb5d20 three\n   * 2b4e70d two\n   * 61ba98b one\n\n1. Initial shallow clone --depth=3 (all good)\n   Shallows:\n   2b4e70da2a10e1d3231a0ae2df396024735601f1\n   ecb578a3cf37198d122ae5df7efed9abaca17144\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a (grafted) four\n   * 0cb5d20 three\n   * 2b4e70d (grafted) two\n\n2. Deepen shallow clone with fetch --deepen=1 (NOT OK)\n   Shallows:\n   0cb5d204f4ef96ed241feb0f2088c9f4794ba758\n   61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a four\n   |/\n   * 0cb5d20 (grafted) three\n   ---\n   Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   is not reachable.\n\nOn the other hand, it seems that equivalent absolute depth driven\nfetches result in all the correct shallows. That led to this proposal,\nwhich unifies absolute and relative deepening in a way that the same\nget_shallow_commits() call is used in both cases. The difference is\nonly that depth is adapted for relative deepening by measuring\nequivalent depth of current local shallow commits in the current remote\nrepo. Thus a new function get_shallows_depth() has been added and the\nfunction get_reachable_list() became redundant / removed.\n\nSame example showing the corrected second step:\n2. Deepen shallow clone with fetch --deepen=1 (all good)\n   Shallow:\n   61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a four\n   |/\n   * 0cb5d20 three\n   * 2b4e70d two\n   * 61ba98b (grafted) one\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of\neach existing shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative\ndeepening anyway. That way we can always sum 'data->deepen_relative'\nand 'depth' values, because 'data->deepen_relative' is always 0 in\nabsolute deepening.\nTo avoid duplicating logic between get_shallows_depth() and\nget_shallow_commits(), get_shallow_commits() was modified so that\nit is used by get_shallows_depth().\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n shallow.c             | 44 +++++++++++++++++--------\n shallow.h             |  1 +\n t/t5500-fetch-pack.sh | 23 +++++++++++++\n upload-pack.c         | 76 +++++--------------------------------------\n 4 files changed, 63 insertions(+), 81 deletions(-)\n\ndiff --git a/shallow.c b/shallow.c\nindex 497a25836b..1a32808865 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)\n {\n \tFREE_AND_NULL(*ptr);\n }\n-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n-\t\tint shallow_flag, int not_shallow_flag)\n+struct commit_list *get_shallow_commits(struct object_array *heads,\n+\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n+\t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n {\n-\tsize_t i = 0;\n-\tint cur_depth = 0;\n+\tsize_t i = 0, j;\n+\tint cur_depth = 0, cur_depth_shallow = 0;\n \tstruct commit_list *result = NULL;\n \tstruct object_array stack = OBJECT_ARRAY_INIT;\n \tstruct commit *commit = NULL;\n@@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t}\n \t\tparse_commit_or_die(commit);\n \t\tcur_depth++;\n-\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n-\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n-\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n-\t\t     graft->nr_parent < 0)) {\n-\t\t\tcommit_list_insert(commit, &result);\n-\t\t\tcommit->object.flags |= shallow_flag;\n-\t\t\tcommit = NULL;\n-\t\t\tcontinue;\n+\t\tif (shallows) {\n+\t\t\tfor (j = 0; j < shallows->nr; j++)\n+\t\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n+\t\t\t\t\tif ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))\n+\t\t\t\t\t\tcur_depth_shallow = cur_depth;\n+\n+\t\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n+\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n+\t\t\t     graft->nr_parent < 0)) {\n+\t\t\t\tcommit = NULL;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n+\t\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n+\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n+\t\t\t     graft->nr_parent < 0)) {\n+\t\t\t\tcommit_list_insert(commit, &result);\n+\t\t\t\tcommit->object.flags |= shallow_flag;\n+\t\t\t\tcommit = NULL;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tcommit->object.flags |= not_shallow_flag;\n \t\t}\n-\t\tcommit->object.flags |= not_shallow_flag;\n \t\tfor (p = commit->parents, commit = NULL; p; p = p->next) {\n \t\t\tint **depth_slot = commit_depth_at(&depths, p->item);\n \t\t\tif (!*depth_slot) {\n@@ -199,7 +214,8 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t}\n \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n \tobject_array_clear(&stack);\n-\n+\tif (shallows && deepen_relative)\n+\t\t*deepen_relative = cur_depth_shallow;\n \treturn result;\n }\n \ndiff --git a/shallow.h b/shallow.h\nindex ad591bd139..d1b3878635 100644\n--- a/shallow.h\n+++ b/shallow.h\n@@ -36,6 +36,7 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);\n void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);\n \n struct commit_list *get_shallow_commits(struct object_array *heads,\n+\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n \t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag);\n struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n \t\t\t\t\t\t    int shallow_flag, int not_shallow_flag);\ndiff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\nindex 2677cd5faa..5a8b30e1fd 100755\n--- a/t/t5500-fetch-pack.sh\n+++ b/t/t5500-fetch-pack.sh\n@@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '\n \t)\n '\n \n+test_expect_success 'fetching deepen beyond merged branch' '\n+\ttest_create_repo shallow-deepen-merged &&\n+\t(\n+\t\tcd shallow-deepen-merged &&\n+\t\tgit commit --allow-empty -m one &&\n+\t\tgit commit --allow-empty -m two &&\n+\t\tgit commit --allow-empty -m three &&\n+\t\tgit switch -c branch &&\n+\t\tgit commit --allow-empty -m four &&\n+\t\tgit commit --allow-empty -m five &&\n+\t\tgit switch main &&\n+\t\tgit merge --no-ff branch &&\n+\t\tcd - &&\n+\t\tgit clone --bare --depth 3 \"file://$(pwd)/shallow-deepen-merged\" deepen.git &&\n+\t\tgit -C deepen.git fetch origin --deepen=1 &&\n+\t\tgit -C deepen.git rev-list --all >actual &&\n+\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow)\n+\t\tdo\n+\t\t\ttest_grep \"$commit\" actual || exit 1\n+\t\tdone\n+\t)\n+'\n+\n test_negotiation_algorithm_default () {\n \ttest_when_finished rm -rf clientv0 clientv2 &&\n \trm -rf server client &&\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 2d2b70cbf2..4232eef34f 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -704,54 +704,11 @@ error:\n \treturn -1;\n }\n \n-static int get_reachable_list(struct upload_pack_data *data,\n-\t\t\t      struct object_array *reachable)\n+static void get_shallows_depth(struct upload_pack_data *data)\n {\n-\tstruct child_process cmd = CHILD_PROCESS_INIT;\n-\tint i;\n-\tstruct object *o;\n-\tchar namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */\n-\tconst unsigned hexsz = the_hash_algo->hexsz;\n-\tint ret;\n-\n-\tif (do_reachable_revlist(&cmd, &data->shallows, reachable,\n-\t\t\t\t data->allow_uor) < 0) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\twhile ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {\n-\t\tstruct object_id oid;\n-\t\tconst char *p;\n-\n-\t\tif (parse_oid_hex(namebuf, &oid, &p) || *p != '\\n')\n-\t\t\tbreak;\n-\n-\t\to = lookup_object(the_repository, &oid);\n-\t\tif (o && o->type == OBJ_COMMIT) {\n-\t\t\to->flags &= ~TMP_MARK;\n-\t\t}\n-\t}\n-\tfor (i = get_max_object_index(the_repository); 0 < i; i--) {\n-\t\to = get_indexed_object(the_repository, i - 1);\n-\t\tif (o && o->type == OBJ_COMMIT &&\n-\t\t    (o->flags & TMP_MARK)) {\n-\t\t\tadd_object_array(o, NULL, reachable);\n-\t\t\t\to->flags &= ~TMP_MARK;\n-\t\t}\n-\t}\n-\tclose(cmd.out);\n-\n-\tif (finish_command(&cmd)) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tret = 0;\n-\n-out:\n-\tchild_process_clear(&cmd);\n-\treturn ret;\n+\tget_shallow_commits(&data->want_obj, &data->shallows,\n+\t\t\t    &data->deepen_relative, 0,\n+\t\t\t    SHALLOW, NOT_SHALLOW);\n }\n \n static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)\n@@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)\n \t\t\tstruct object *object = data->shallows.objects[i].item;\n \t\t\tobject->flags |= NOT_SHALLOW;\n \t\t}\n-\t} else if (data->deepen_relative) {\n-\t\tstruct object_array reachable_shallows = OBJECT_ARRAY_INIT;\n-\t\tstruct commit_list *result;\n-\n-\t\t/*\n-\t\t * Checking for reachable shallows requires that our refs be\n-\t\t * marked with OUR_REF.\n-\t\t */\n-\t\trefs_head_ref_namespaced(get_main_ref_store(the_repository),\n-\t\t\t\t\t check_ref, data);\n-\t\tfor_each_namespaced_ref_1(check_ref, data);\n-\n-\t\tget_reachable_list(data, &reachable_shallows);\n-\t\tresult = get_shallow_commits(&reachable_shallows,\n-\t\t\t\t\t     depth + 1,\n-\t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n-\t\tsend_shallow(data, result);\n-\t\tfree_commit_list(result);\n-\t\tobject_array_clear(&reachable_shallows);\n \t} else {\n \t\tstruct commit_list *result;\n \n-\t\tresult = get_shallow_commits(&data->want_obj, depth,\n+\t\tif (data->deepen_relative)\n+\t\t\tget_shallows_depth(data);\n+\n+\t\tresult = get_shallow_commits(&data->want_obj, NULL, NULL,\n+\t\t\t\t\t     data->deepen_relative + depth,\n \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n \t\tsend_shallow(data, result);\n \t\tfree_commit_list(result);\n-- \ngitgitgadget\n"},{"id":"535769","messageId":"aYyGTmS6fEb2QfBU@pks.im","threadId":"64603","inReplyTo":"e9b20ae06fd2c7f2c6b73c9f093a23c812227b7e.1768602661.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/2] shallow: handling fetch relative-deepen","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-11T13:38:22Z","receivedAt":"2026-02-11T13:38:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 16, 2026 at 10:31:01PM +0000, Samo Pogačnik via GitGitGadget wrote:\n> diff --git a/shallow.c b/shallow.c\n> index 497a25836b..1a32808865 100644\n> --- a/shallow.c\n> +++ b/shallow.c\n> @@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)\n>  {\n>  \tFREE_AND_NULL(*ptr);\n>  }\n> -struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n> -\t\tint shallow_flag, int not_shallow_flag)\n> +struct commit_list *get_shallow_commits(struct object_array *heads,\n> +\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n> +\t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n>  {\n> -\tsize_t i = 0;\n> -\tint cur_depth = 0;\n> +\tsize_t i = 0, j;\n\nWe can declare `j` in the loop itself, as it's not used anywhere else.\n\n> @@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n>  \t\t}\n>  \t\tparse_commit_or_die(commit);\n>  \t\tcur_depth++;\n> -\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n> -\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n> -\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n> -\t\t     graft->nr_parent < 0)) {\n> -\t\t\tcommit_list_insert(commit, &result);\n> -\t\t\tcommit->object.flags |= shallow_flag;\n> -\t\t\tcommit = NULL;\n> -\t\t\tcontinue;\n> +\t\tif (shallows) {\n> +\t\t\tfor (j = 0; j < shallows->nr; j++)\n> +\t\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n> +\t\t\t\t\tif ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))\n\nThe additional braces around the respective conditions are not needed.\n\n> +\t\t\t\t\t\tcur_depth_shallow = cur_depth;\n> +\n> +\t\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n> +\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n> +\t\t\t     graft->nr_parent < 0)) {\n> +\t\t\t\tcommit = NULL;\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n\nThis block here is almost the same as the one below. But there's some\nconfusing parts:\n\n  - Why don't we update `result` at all?\n\n  - Why don't we set the `shallow_flag`?\n\n  - Why don't we have to check for the passed-in depth?\n\nAll of these parts feel somewhat surprising to me, as the function now\nbehaves so wildly different depending on whether or not `shallows` was\npassed.\n\nI guess this is because we really only care about `cur_depth_shallow`?\n\n> diff --git a/upload-pack.c b/upload-pack.c\n> index 2d2b70cbf2..4232eef34f 100644\n> --- a/upload-pack.c\n> +++ b/upload-pack.c\n> @@ -704,54 +704,11 @@ error:\n>  \treturn -1;\n>  }\n>  \n> -static int get_reachable_list(struct upload_pack_data *data,\n> -\t\t\t      struct object_array *reachable)\n> +static void get_shallows_depth(struct upload_pack_data *data)\n\nI think this function is rather pointless, as there is only a single\ncaller and we only end up forwarding to `get_shallow_commits()`. Let's\ninline it.\n\n> @@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int depth)\n>  \t\t\tstruct object *object = data->shallows.objects[i].item;\n>  \t\t\tobject->flags |= NOT_SHALLOW;\n>  \t\t}\n> -\t} else if (data->deepen_relative) {\n> -\t\tstruct object_array reachable_shallows = OBJECT_ARRAY_INIT;\n> -\t\tstruct commit_list *result;\n> -\n> -\t\t/*\n> -\t\t * Checking for reachable shallows requires that our refs be\n> -\t\t * marked with OUR_REF.\n> -\t\t */\n> -\t\trefs_head_ref_namespaced(get_main_ref_store(the_repository),\n> -\t\t\t\t\t check_ref, data);\n> -\t\tfor_each_namespaced_ref_1(check_ref, data);\n> -\n> -\t\tget_reachable_list(data, &reachable_shallows);\n> -\t\tresult = get_shallow_commits(&reachable_shallows,\n> -\t\t\t\t\t     depth + 1,\n> -\t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n> -\t\tsend_shallow(data, result);\n> -\t\tfree_commit_list(result);\n> -\t\tobject_array_clear(&reachable_shallows);\n>  \t} else {\n>  \t\tstruct commit_list *result;\n>  \n> -\t\tresult = get_shallow_commits(&data->want_obj, depth,\n> +\t\tif (data->deepen_relative)\n> +\t\t\tget_shallows_depth(data);\n\nOkay, so here we now essentially call `get_shallow_commits()` twice. The\nfirst time we compute `data->deepen_relative`, only to then pass it back\nto `get_shallow_commits()` a second time. That feels quite strange to\nme. Can't we have `get_shallow_commits()` handle this for us directly in\na single call?\n\n> +\t\tresult = get_shallow_commits(&data->want_obj, NULL, NULL,\n> +\t\t\t\t\t     data->deepen_relative + depth,\n>  \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n>  \t\tsend_shallow(data, result);\n>  \t\tfree_commit_list(result);\n\nThanks!\n\nPatrick\n"},{"id":"535963","messageId":"0331ea3cef47b56ab920756fd66449e572667fee.camel@t-2.net","threadId":"64603","inReplyTo":"aYyGTmS6fEb2QfBU@pks.im","subject":"Re: [PATCH v4 2/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik","fromEmail":"samo_pogacnik@t-2.net","sentAt":"2026-02-13T20:48:11Z","receivedAt":"2026-02-13T20:48:51Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"On Wed, 2026-02-11 at 14:38 +0100, Patrick Steinhardt wrote:\n> On Fri, Jan 16, 2026 at 10:31:01PM +0000, Samo Pogačnik via GitGitGadget\n> wrote:\n> > diff --git a/shallow.c b/shallow.c\n> > index 497a25836b..1a32808865 100644\n> > --- a/shallow.c\n> > +++ b/shallow.c\n> > @@ -130,11 +130,12 @@ static void free_depth_in_slab(int **ptr)\n> >  {\n> >  \tFREE_AND_NULL(*ptr);\n> >  }\n> > -struct commit_list *get_shallow_commits(struct object_array *heads, int\n> > depth,\n> > -\t\tint shallow_flag, int not_shallow_flag)\n> > +struct commit_list *get_shallow_commits(struct object_array *heads,\n> > +\t\t\t\t\tstruct object_array *shallows, int\n> > *deepen_relative,\n> > +\t\t\t\t\tint depth, int shallow_flag, int\n> > not_shallow_flag)\n> >  {\n> > -\tsize_t i = 0;\n> > -\tint cur_depth = 0;\n> > +\tsize_t i = 0, j;\n> \n> We can declare `j` in the loop itself, as it's not used anywhere else.\n\nYes, sure.\n\n> \n> > @@ -168,16 +169,30 @@ struct commit_list *get_shallow_commits(struct\n> > object_array *heads, int depth,\n> >  \t\t}\n> >  \t\tparse_commit_or_die(commit);\n> >  \t\tcur_depth++;\n> > -\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n> > -\t\t    (is_repository_shallow(the_repository) && !commit-\n> > >parents &&\n> > -\t\t     (graft = lookup_commit_graft(the_repository, &commit-\n> > >object.oid)) != NULL &&\n> > -\t\t     graft->nr_parent < 0)) {\n> > -\t\t\tcommit_list_insert(commit, &result);\n> > -\t\t\tcommit->object.flags |= shallow_flag;\n> > -\t\t\tcommit = NULL;\n> > -\t\t\tcontinue;\n> > +\t\tif (shallows) {\n> > +\t\t\tfor (j = 0; j < shallows->nr; j++)\n> > +\t\t\t\tif (oideq(&commit->object.oid, &shallows-\n> > >objects[j].item->oid))\n> > +\t\t\t\t\tif ((!cur_depth_shallow) ||\n> > (cur_depth < cur_depth_shallow))\n> \n> The additional braces around the respective conditions are not needed.\n\nOf course.\n> \n> > +\t\t\t\t\t\tcur_depth_shallow =\n> > cur_depth;\n> > +\n> > +\t\t\tif ((is_repository_shallow(the_repository) &&\n> > !commit->parents &&\n> > +\t\t\t     (graft = lookup_commit_graft(the_repository,\n> > &commit->object.oid)) != NULL &&\n> > +\t\t\t     graft->nr_parent < 0)) {\n> > +\t\t\t\tcommit = NULL;\n> > +\t\t\t\tcontinue;\n> > +\t\t\t}\n> \n> This block here is almost the same as the one below. But there's some\n> confusing parts:\n> \n>   - Why don't we update `result` at all?\n> \n>   - Why don't we set the `shallow_flag`?\n> \n>   - Why don't we have to check for the passed-in depth?\n> \n> All of these parts feel somewhat surprising to me, as the function now\n> behaves so wildly different depending on whether or not `shallows` was\n> passed.\n> \n> I guess this is because we really only care about `cur_depth_shallow`?\n\nExactly, i merged the two functions with almost the same algorithm producing\ndifferent results depending on additional input parameter shallows. When\nshallows passed only current maximum absolute depth is returned in the extra\noutput parameter deepen_relative (if provided) and nothing else is done. The\npassed-in depth is not checked as it is irrelevant for this depth-measuring\nscenario.\n\n> \n> > diff --git a/upload-pack.c b/upload-pack.c\n> > index 2d2b70cbf2..4232eef34f 100644\n> > --- a/upload-pack.c\n> > +++ b/upload-pack.c\n> > @@ -704,54 +704,11 @@ error:\n> >  \treturn -1;\n> >  }\n> >  \n> > -static int get_reachable_list(struct upload_pack_data *data,\n> > -\t\t\t      struct object_array *reachable)\n> > +static void get_shallows_depth(struct upload_pack_data *data)\n> \n> I think this function is rather pointless, as there is only a single\n> caller and we only end up forwarding to `get_shallow_commits()`. Let's\n> inline it.\n\nTrue, i suppose you ment inline the function code instead of function call and\nnot making the function inline?\n\n> \n> > @@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int\n> > depth)\n> >  \t\t\tstruct object *object = data-\n> > >shallows.objects[i].item;\n> >  \t\t\tobject->flags |= NOT_SHALLOW;\n> >  \t\t}\n> > -\t} else if (data->deepen_relative) {\n> > -\t\tstruct object_array reachable_shallows = OBJECT_ARRAY_INIT;\n> > -\t\tstruct commit_list *result;\n> > -\n> > -\t\t/*\n> > -\t\t * Checking for reachable shallows requires that our refs\n> > be\n> > -\t\t * marked with OUR_REF.\n> > -\t\t */\n> > -\n> > \t\trefs_head_ref_namespaced(get_main_ref_store(the_repository),\n> > -\t\t\t\t\t check_ref, data);\n> > -\t\tfor_each_namespaced_ref_1(check_ref, data);\n> > -\n> > -\t\tget_reachable_list(data, &reachable_shallows);\n> > -\t\tresult = get_shallow_commits(&reachable_shallows,\n> > -\t\t\t\t\t     depth + 1,\n> > -\t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n> > -\t\tsend_shallow(data, result);\n> > -\t\tfree_commit_list(result);\n> > -\t\tobject_array_clear(&reachable_shallows);\n> >  \t} else {\n> >  \t\tstruct commit_list *result;\n> >  \n> > -\t\tresult = get_shallow_commits(&data->want_obj, depth,\n> > +\t\tif (data->deepen_relative)\n> > +\t\t\tget_shallows_depth(data);\n> \n> Okay, so here we now essentially call `get_shallow_commits()` twice. The\n> first time we compute `data->deepen_relative`, only to then pass it back\n> to `get_shallow_commits()` a second time. That feels quite strange to\n> me. Can't we have `get_shallow_commits()` handle this for us directly in\n> a single call?\n\nNicely put, i just wasn't (and still am not) confident enough to change code in\na way that would potentially affect any other scenarios than fetching relative-\ndeepen.\n\nThank you very much for the review and i'll try to address all your review\npoints in the next patch version.\n\n\nBest regards, Samo\n"},{"id":"536006","messageId":"c55447544d68c3f98532b1d32183465c8f175ff8.camel@t-2.net","threadId":"64603","inReplyTo":"aYyGTmS6fEb2QfBU@pks.im","subject":"Re: [PATCH v4 2/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik","fromEmail":"samo_pogacnik@t-2.net","sentAt":"2026-02-14T09:40:22Z","receivedAt":"2026-02-14T09:41:00Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"On Wed, 2026-02-11 at 14:38 +0100, Patrick Steinhardt wrote:\n> On Fri, Jan 16, 2026 at 10:31:01PM +0000, Samo Pogačnik via GitGitGadget\n> wrote:\n> \n> > @@ -881,29 +838,14 @@ static void deepen(struct upload_pack_data *data, int\n> > depth)\n> >  \t\t\tstruct object *object = data-\n> > >shallows.objects[i].item;\n> >  \t\t\tobject->flags |= NOT_SHALLOW;\n> >  \t\t}\n> > -\t} else if (data->deepen_relative) {\n> > -\t\tstruct object_array reachable_shallows = OBJECT_ARRAY_INIT;\n> > -\t\tstruct commit_list *result;\n> > -\n> > -\t\t/*\n> > -\t\t * Checking for reachable shallows requires that our refs\n> > be\n> > -\t\t * marked with OUR_REF.\n> > -\t\t */\n> > -\n> > \t\trefs_head_ref_namespaced(get_main_ref_store(the_repository),\n> > -\t\t\t\t\t check_ref, data);\n> > -\t\tfor_each_namespaced_ref_1(check_ref, data);\n> > -\n> > -\t\tget_reachable_list(data, &reachable_shallows);\n> > -\t\tresult = get_shallow_commits(&reachable_shallows,\n> > -\t\t\t\t\t     depth + 1,\n> > -\t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n> > -\t\tsend_shallow(data, result);\n> > -\t\tfree_commit_list(result);\n> > -\t\tobject_array_clear(&reachable_shallows);\n> >  \t} else {\n> >  \t\tstruct commit_list *result;\n> >  \n> > -\t\tresult = get_shallow_commits(&data->want_obj, depth,\n> > +\t\tif (data->deepen_relative)\n> > +\t\t\tget_shallows_depth(data);\n> \n> Okay, so here we now essentially call `get_shallow_commits()` twice. The\n> first time we compute `data->deepen_relative`, only to then pass it back\n> to `get_shallow_commits()` a second time. That feels quite strange to\n> me. Can't we have `get_shallow_commits()` handle this for us directly in\n> a single call?\n> \n> > +\t\tresult = get_shallow_commits(&data->want_obj, NULL, NULL,\n> > +\t\t\t\t\t     data->deepen_relative + depth,\n> >  \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n> >  \t\tsend_shallow(data, result);\n> >  \t\tfree_commit_list(result);\n> \n\nI have additional dilemma regarding handling this in a single call.\nWouldn't it be generally good/useful to have a separate function in shallow.c\njust for measuring current absolute depth instead of blending the measurement\ninto get_shallow_commits()?\n\nThanks, Samo\n"},{"id":"536053","messageId":"52d93e12de058d3ccaaebcf8c49a48d85e455171.camel@t-2.net","threadId":"64603","inReplyTo":"c55447544d68c3f98532b1d32183465c8f175ff8.camel@t-2.net","subject":"Re: [PATCH v4 2/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik","fromEmail":"samo_pogacnik@t-2.net","sentAt":"2026-02-15T11:19:17Z","receivedAt":"2026-02-15T11:19:48Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"On Sat, 2026-02-14 at 10:40 +0100, Samo Pogačnik wrote:\n> On Wed, 2026-02-11 at 14:38 +0100, Patrick Steinhardt wrote:\n> > On Fri, Jan 16, 2026 at 10:31:01PM +0000, Samo Pogačnik via GitGitGadget\n> > wrote:\n> > > \tobject_array_clear(&reachable_shallows);\n> > >  \t} else {\n> > >  \t\tstruct commit_list *result;\n> > >  \n> > > -\t\tresult = get_shallow_commits(&data->want_obj, depth,\n> > > +\t\tif (data->deepen_relative)\n> > > +\t\t\tget_shallows_depth(data);\n> > \n> > Okay, so here we now essentially call `get_shallow_commits()` twice. The\n> > first time we compute `data->deepen_relative`, only to then pass it back\n> > to `get_shallow_commits()` a second time. That feels quite strange to\n> > me. Can't we have `get_shallow_commits()` handle this for us directly in\n> > a single call?\n> > \n> > > +\t\tresult = get_shallow_commits(&data->want_obj, NULL, NULL,\n> > > +\t\t\t\t\t     data->deepen_relative +\n> > > depth,\n> > >  \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n> > >  \t\tsend_shallow(data, result);\n> > >  \t\tfree_commit_list(result);\n> > \n> \n> I have additional dilemma regarding handling this in a single call.\n> Wouldn't it be generally good/useful to have a separate function in shallow.c\n> just for measuring current absolute depth instead of blending the measurement\n> into get_shallow_commits()?\n> \n\nI prepared another version which i am going to post shortly. It does make a\nsingle call to 'get_shallow_commits()' in upload_pack.c, however in shallow.c\nthere is again a common internal function, which is called once for measuring\ncurrent depth and then again to get a list shallow commits. I do not know how to\nperform current depth measurement and shallows list retrieval with new depth\nextended by the same measurement simultaneously. However i find it important to\nkeep common algorithm in a single function for easier maintenance. I hope this\nis ok.\n\nBest regards, Samo\n"},{"id":"536059","messageId":"pull.2121.v5.git.git.1771186316.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v4.git.git.1768602661.gitgitgadget@gmail.com","subject":"[PATCH v5 0/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-15T20:11:54Z","receivedAt":"2026-02-15T20:12:00Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"When a shallowed repository gets deepened beyond the beginning of a merged\nbranch, we may endup with some shallows, that are behind the reachable ones.\nAdded test 'fetching deepen beyond merged branch' exposes that behaviour.\n\nOn the other hand, it seems that equivalent absolute depth driven fetches\nresult in all the correct shallows. That led to this proposal, which unifies\nabsolute and relative deepening in a way that the same get_shallow_commits()\ncall is used in both cases. The difference is only that depth is adapted for\nrelative deepening by measuring equivalent depth of current local shallow\ncommits in the current remote repo. Thus a new function get_shallows_depth()\nhas been added and the function get_reachable_list() became redundant /\nremoved.\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of each\nexisting shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative deepening\nanyway. That way we can allways summ 'data->deepen_relative' and 'depth'\nvalues, because 'data->deepen_relative' is always 0 in absolute deepening.\n\nSamo Pogačnik (2):\n  shallow: free local object_array allocations\n  shallow: handling fetch relative-deepen\n\n shallow.c             | 73 ++++++++++++++++++++++++++++++++++++-------\n shallow.h             |  2 ++\n t/t5500-fetch-pack.sh | 23 ++++++++++++++\n upload-pack.c         | 72 ++----------------------------------------\n 4 files changed, 88 insertions(+), 82 deletions(-)\n\n\nbase-commit: f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2121%2Fspog%2Ffix-fetch-deepen-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2121/spog/fix-fetch-deepen-v5\nPull-Request: https://github.com/git/git/pull/2121\n\nRange-diff vs v4:\n\n 1:  f8a8d077cd = 1:  f8a8d077cd shallow: free local object_array allocations\n 2:  e9b20ae06f ! 2:  8d48ba9cd1 shallow: handling fetch relative-deepen\n     @@ Commit message\n      \n          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n      \n     +    Fixing v4\n     +\n     +    Fixing v4 again\n     +\n       ## shallow.c ##\n      @@ shallow.c: static void free_depth_in_slab(int **ptr)\n       {\n     @@ shallow.c: static void free_depth_in_slab(int **ptr)\n       }\n      -struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n      -\t\tint shallow_flag, int not_shallow_flag)\n     -+struct commit_list *get_shallow_commits(struct object_array *heads,\n     -+\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n     -+\t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n     ++/*\n     ++ * This is a common internal function that can either return a list of\n     ++ * shallow commits or calculate the current maximum depth of a shallow\n     ++ * repository, depending on the input parameters.\n     ++ *\n     ++ * Depth calculation is triggered by passing the `shallows` parameter.\n     ++ * In this case, the computed depth is stored in `max_cur_depth` (if it is\n     ++ * provided), and the function returns NULL.\n     ++ *\n     ++ * Otherwise, `max_cur_depth` remains unchanged and the function returns\n     ++ * a list of shallow commits.\n     ++ */\n     ++static struct commit_list *get_shallows_or_depth(struct object_array *heads,\n     ++\t\t\t\tstruct object_array *shallows, int *max_cur_depth,\n     ++\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n       {\n     --\tsize_t i = 0;\n     + \tsize_t i = 0;\n      -\tint cur_depth = 0;\n     -+\tsize_t i = 0, j;\n      +\tint cur_depth = 0, cur_depth_shallow = 0;\n       \tstruct commit_list *result = NULL;\n       \tstruct object_array stack = OBJECT_ARRAY_INIT;\n     @@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, i\n      -\t\t\tcommit = NULL;\n      -\t\t\tcontinue;\n      +\t\tif (shallows) {\n     -+\t\t\tfor (j = 0; j < shallows->nr; j++)\n     ++\t\t\tfor (size_t j = 0; j < shallows->nr; j++)\n      +\t\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n     -+\t\t\t\t\tif ((!cur_depth_shallow) || (cur_depth < cur_depth_shallow))\n     ++\t\t\t\t\tif (!cur_depth_shallow || cur_depth < cur_depth_shallow)\n      +\t\t\t\t\t\tcur_depth_shallow = cur_depth;\n      +\n      +\t\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n     @@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, i\n       \t\t\tint **depth_slot = commit_depth_at(&depths, p->item);\n       \t\t\tif (!*depth_slot) {\n      @@ shallow.c: struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n     - \t}\n       \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n       \tobject_array_clear(&stack);\n     --\n     -+\tif (shallows && deepen_relative)\n     -+\t\t*deepen_relative = cur_depth_shallow;\n     + \n     ++\tif (shallows && max_cur_depth)\n     ++\t\t*max_cur_depth = cur_depth_shallow;\n       \treturn result;\n       }\n       \n     ++int get_shallows_depth(struct object_array *heads, struct object_array *shallows)\n     ++{\n     ++\tint max_cur_depth = 0;\n     ++\tget_shallows_or_depth(heads, shallows, &max_cur_depth, 0, 0, 0);\n     ++\treturn max_cur_depth;\n     ++\n     ++}\n     ++\n     ++struct commit_list *get_shallow_commits(struct object_array *heads,\n     ++\t\t\t\t\tstruct object_array *shallows, int deepen_relative,\n     ++\t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n     ++{\n     ++\tif (shallows && deepen_relative) {\n     ++\t\tdepth += get_shallows_depth(heads, shallows);\n     ++\t}\n     ++\treturn get_shallows_or_depth(heads, NULL, NULL,\n     ++\t\t\t\t     depth, shallow_flag, not_shallow_flag);\n     ++}\n     ++\n     + static void show_commit(struct commit *commit, void *data)\n     + {\n     + \tcommit_list_insert(commit, data);\n      \n       ## shallow.h ##\n      @@ shallow.h: int commit_shallow_file(struct repository *r, struct shallow_lock *lk);\n     + /* rollback $GIT_DIR/shallow and reset stat-validity checks */\n       void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);\n       \n     ++int get_shallows_depth(struct object_array *heads, struct object_array *shallows);\n       struct commit_list *get_shallow_commits(struct object_array *heads,\n     -+\t\t\t\t\tstruct object_array *shallows, int *deepen_relative,\n     ++\t\t\t\t\tstruct object_array *shallows, int deepen_relative,\n       \t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag);\n       struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n       \t\t\t\t\t\t    int shallow_flag, int not_shallow_flag);\n     @@ upload-pack.c: error:\n       \n      -static int get_reachable_list(struct upload_pack_data *data,\n      -\t\t\t      struct object_array *reachable)\n     -+static void get_shallows_depth(struct upload_pack_data *data)\n     - {\n     +-{\n      -\tstruct child_process cmd = CHILD_PROCESS_INIT;\n      -\tint i;\n      -\tstruct object *o;\n     @@ upload-pack.c: error:\n      -out:\n      -\tchild_process_clear(&cmd);\n      -\treturn ret;\n     -+\tget_shallow_commits(&data->want_obj, &data->shallows,\n     -+\t\t\t    &data->deepen_relative, 0,\n     -+\t\t\t    SHALLOW, NOT_SHALLOW);\n     - }\n     - \n     +-}\n     +-\n       static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)\n     + {\n     + \tstruct child_process cmd = CHILD_PROCESS_INIT;\n      @@ upload-pack.c: static void deepen(struct upload_pack_data *data, int depth)\n       \t\t\tstruct object *object = data->shallows.objects[i].item;\n       \t\t\tobject->flags |= NOT_SHALLOW;\n     @@ upload-pack.c: static void deepen(struct upload_pack_data *data, int depth)\n       \t\tstruct commit_list *result;\n       \n      -\t\tresult = get_shallow_commits(&data->want_obj, depth,\n     -+\t\tif (data->deepen_relative)\n     -+\t\t\tget_shallows_depth(data);\n     -+\n     -+\t\tresult = get_shallow_commits(&data->want_obj, NULL, NULL,\n     -+\t\t\t\t\t     data->deepen_relative + depth,\n     ++\t\tresult = get_shallow_commits(&data->want_obj, &data->shallows,\n     ++\t\t\t\t\t     data->deepen_relative, depth,\n       \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n       \t\tsend_shallow(data, result);\n       \t\tfree_commit_list(result);\n\n-- \ngitgitgadget\n"},{"id":"536060","messageId":"f8a8d077cd7203663406bf656287ed7e0d6e6d90.1771186316.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v5.git.git.1771186316.gitgitgadget@gmail.com","subject":"[PATCH v5 1/2] shallow: free local object_array allocations","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-15T20:11:55Z","receivedAt":"2026-02-15T20:12:02Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nThe local object_array 'stack' in get_shallow_commits() function\ndoes not free its dynamic elements before the function returns.\nAs a result elements remain allocated and their reference forgotten.\n\nAlso note, that test 'fetching deepen beyond merged branch' added by\n'shallow: handling fetch relative-deepen' patch fails without this\ncorrection in linux-leaks and linux-reftable-leaks test runs.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n shallow.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/shallow.c b/shallow.c\nindex 55b9cd9d3f..497a25836b 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -198,6 +198,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t}\n \t}\n \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n+\tobject_array_clear(&stack);\n \n \treturn result;\n }\n-- \ngitgitgadget\n\n"},{"id":"536061","messageId":"8d48ba9cd16966ea717c019f151cddf27a09c555.1771186316.git.gitgitgadget@gmail.com","threadId":"64603","inReplyTo":"pull.2121.v5.git.git.1771186316.gitgitgadget@gmail.com","subject":"[PATCH v5 2/2] shallow: handling fetch relative-deepen","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-15T20:11:56Z","receivedAt":"2026-02-15T20:12:04Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n\nWhen a shallowed repository gets deepened beyond the beginning of a\nmerged branch, we may end up with some shallows that are hidden behind\nthe reachable shallow commits. Added test 'fetching deepen beyond\nmerged branch' exposes that behaviour.\n\nAn example showing the problem based on added test:\n0. Whole initial git repo to be cloned from\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 (branch) five\n   | * ecb578a four\n   |/\n   * 0cb5d20 three\n   * 2b4e70d two\n   * 61ba98b one\n\n1. Initial shallow clone --depth=3 (all good)\n   Shallows:\n   2b4e70da2a10e1d3231a0ae2df396024735601f1\n   ecb578a3cf37198d122ae5df7efed9abaca17144\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a (grafted) four\n   * 0cb5d20 three\n   * 2b4e70d (grafted) two\n\n2. Deepen shallow clone with fetch --deepen=1 (NOT OK)\n   Shallows:\n   0cb5d204f4ef96ed241feb0f2088c9f4794ba758\n   61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a four\n   |/\n   * 0cb5d20 (grafted) three\n   ---\n   Note that second shallow commit 61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   is not reachable.\n\nOn the other hand, it seems that equivalent absolute depth driven\nfetches result in all the correct shallows. That led to this proposal,\nwhich unifies absolute and relative deepening in a way that the same\nget_shallow_commits() call is used in both cases. The difference is\nonly that depth is adapted for relative deepening by measuring\nequivalent depth of current local shallow commits in the current remote\nrepo. Thus a new function get_shallows_depth() has been added and the\nfunction get_reachable_list() became redundant / removed.\n\nSame example showing the corrected second step:\n2. Deepen shallow clone with fetch --deepen=1 (all good)\n   Shallow:\n   61ba98be443fd51c542eb66585a1f6d7e15fcdae\n   Graph:\n   *   033585d (HEAD -> main) Merge branch 'branch'\n   |\\\n   | * 984f8b1 five\n   | * ecb578a four\n   |/\n   * 0cb5d20 three\n   * 2b4e70d two\n   * 61ba98b (grafted) one\n\nThe get_shallows_depth() function also shares the logic of the\nget_shallow_commits() function, but it focuses on counting depth of\neach existing shallow commit. The minimum result is stored as\n'data->deepen_relative', which is set not to be zero for relative\ndeepening anyway. That way we can always sum 'data->deepen_relative'\nand 'depth' values, because 'data->deepen_relative' is always 0 in\nabsolute deepening.\nTo avoid duplicating logic between get_shallows_depth() and\nget_shallow_commits(), get_shallow_commits() was modified so that\nit is used by get_shallows_depth().\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n\nFixing v4\n\nFixing v4 again\n---\n shallow.c             | 72 +++++++++++++++++++++++++++++++++++--------\n shallow.h             |  2 ++\n t/t5500-fetch-pack.sh | 23 ++++++++++++++\n upload-pack.c         | 72 ++-----------------------------------------\n 4 files changed, 87 insertions(+), 82 deletions(-)\n\ndiff --git a/shallow.c b/shallow.c\nindex 497a25836b..a156006d88 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -130,11 +130,24 @@ static void free_depth_in_slab(int **ptr)\n {\n \tFREE_AND_NULL(*ptr);\n }\n-struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n-\t\tint shallow_flag, int not_shallow_flag)\n+/*\n+ * This is a common internal function that can either return a list of\n+ * shallow commits or calculate the current maximum depth of a shallow\n+ * repository, depending on the input parameters.\n+ *\n+ * Depth calculation is triggered by passing the `shallows` parameter.\n+ * In this case, the computed depth is stored in `max_cur_depth` (if it is\n+ * provided), and the function returns NULL.\n+ *\n+ * Otherwise, `max_cur_depth` remains unchanged and the function returns\n+ * a list of shallow commits.\n+ */\n+static struct commit_list *get_shallows_or_depth(struct object_array *heads,\n+\t\t\t\tstruct object_array *shallows, int *max_cur_depth,\n+\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n {\n \tsize_t i = 0;\n-\tint cur_depth = 0;\n+\tint cur_depth = 0, cur_depth_shallow = 0;\n \tstruct commit_list *result = NULL;\n \tstruct object_array stack = OBJECT_ARRAY_INIT;\n \tstruct commit *commit = NULL;\n@@ -168,16 +181,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t}\n \t\tparse_commit_or_die(commit);\n \t\tcur_depth++;\n-\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n-\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n-\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n-\t\t     graft->nr_parent < 0)) {\n-\t\t\tcommit_list_insert(commit, &result);\n-\t\t\tcommit->object.flags |= shallow_flag;\n-\t\t\tcommit = NULL;\n-\t\t\tcontinue;\n+\t\tif (shallows) {\n+\t\t\tfor (size_t j = 0; j < shallows->nr; j++)\n+\t\t\t\tif (oideq(&commit->object.oid, &shallows->objects[j].item->oid))\n+\t\t\t\t\tif (!cur_depth_shallow || cur_depth < cur_depth_shallow)\n+\t\t\t\t\t\tcur_depth_shallow = cur_depth;\n+\n+\t\t\tif ((is_repository_shallow(the_repository) && !commit->parents &&\n+\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n+\t\t\t     graft->nr_parent < 0)) {\n+\t\t\t\tcommit = NULL;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tif ((depth != INFINITE_DEPTH && cur_depth >= depth) ||\n+\t\t\t    (is_repository_shallow(the_repository) && !commit->parents &&\n+\t\t\t     (graft = lookup_commit_graft(the_repository, &commit->object.oid)) != NULL &&\n+\t\t\t     graft->nr_parent < 0)) {\n+\t\t\t\tcommit_list_insert(commit, &result);\n+\t\t\t\tcommit->object.flags |= shallow_flag;\n+\t\t\t\tcommit = NULL;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tcommit->object.flags |= not_shallow_flag;\n \t\t}\n-\t\tcommit->object.flags |= not_shallow_flag;\n \t\tfor (p = commit->parents, commit = NULL; p; p = p->next) {\n \t\t\tint **depth_slot = commit_depth_at(&depths, p->item);\n \t\t\tif (!*depth_slot) {\n@@ -200,9 +227,30 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n \tobject_array_clear(&stack);\n \n+\tif (shallows && max_cur_depth)\n+\t\t*max_cur_depth = cur_depth_shallow;\n \treturn result;\n }\n \n+int get_shallows_depth(struct object_array *heads, struct object_array *shallows)\n+{\n+\tint max_cur_depth = 0;\n+\tget_shallows_or_depth(heads, shallows, &max_cur_depth, 0, 0, 0);\n+\treturn max_cur_depth;\n+\n+}\n+\n+struct commit_list *get_shallow_commits(struct object_array *heads,\n+\t\t\t\t\tstruct object_array *shallows, int deepen_relative,\n+\t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag)\n+{\n+\tif (shallows && deepen_relative) {\n+\t\tdepth += get_shallows_depth(heads, shallows);\n+\t}\n+\treturn get_shallows_or_depth(heads, NULL, NULL,\n+\t\t\t\t     depth, shallow_flag, not_shallow_flag);\n+}\n+\n static void show_commit(struct commit *commit, void *data)\n {\n \tcommit_list_insert(commit, data);\ndiff --git a/shallow.h b/shallow.h\nindex ad591bd139..e3f0df57ad 100644\n--- a/shallow.h\n+++ b/shallow.h\n@@ -35,7 +35,9 @@ int commit_shallow_file(struct repository *r, struct shallow_lock *lk);\n /* rollback $GIT_DIR/shallow and reset stat-validity checks */\n void rollback_shallow_file(struct repository *r, struct shallow_lock *lk);\n \n+int get_shallows_depth(struct object_array *heads, struct object_array *shallows);\n struct commit_list *get_shallow_commits(struct object_array *heads,\n+\t\t\t\t\tstruct object_array *shallows, int deepen_relative,\n \t\t\t\t\tint depth, int shallow_flag, int not_shallow_flag);\n struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n \t\t\t\t\t\t    int shallow_flag, int not_shallow_flag);\ndiff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\nindex 2677cd5faa..5a8b30e1fd 100755\n--- a/t/t5500-fetch-pack.sh\n+++ b/t/t5500-fetch-pack.sh\n@@ -955,6 +955,29 @@ test_expect_success 'fetching deepen' '\n \t)\n '\n \n+test_expect_success 'fetching deepen beyond merged branch' '\n+\ttest_create_repo shallow-deepen-merged &&\n+\t(\n+\t\tcd shallow-deepen-merged &&\n+\t\tgit commit --allow-empty -m one &&\n+\t\tgit commit --allow-empty -m two &&\n+\t\tgit commit --allow-empty -m three &&\n+\t\tgit switch -c branch &&\n+\t\tgit commit --allow-empty -m four &&\n+\t\tgit commit --allow-empty -m five &&\n+\t\tgit switch main &&\n+\t\tgit merge --no-ff branch &&\n+\t\tcd - &&\n+\t\tgit clone --bare --depth 3 \"file://$(pwd)/shallow-deepen-merged\" deepen.git &&\n+\t\tgit -C deepen.git fetch origin --deepen=1 &&\n+\t\tgit -C deepen.git rev-list --all >actual &&\n+\t\tfor commit in $(sed \"/^$/d\" deepen.git/shallow)\n+\t\tdo\n+\t\t\ttest_grep \"$commit\" actual || exit 1\n+\t\tdone\n+\t)\n+'\n+\n test_negotiation_algorithm_default () {\n \ttest_when_finished rm -rf clientv0 clientv2 &&\n \trm -rf server client &&\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 2d2b70cbf2..88dac1b65c 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -704,56 +704,6 @@ error:\n \treturn -1;\n }\n \n-static int get_reachable_list(struct upload_pack_data *data,\n-\t\t\t      struct object_array *reachable)\n-{\n-\tstruct child_process cmd = CHILD_PROCESS_INIT;\n-\tint i;\n-\tstruct object *o;\n-\tchar namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */\n-\tconst unsigned hexsz = the_hash_algo->hexsz;\n-\tint ret;\n-\n-\tif (do_reachable_revlist(&cmd, &data->shallows, reachable,\n-\t\t\t\t data->allow_uor) < 0) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\twhile ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {\n-\t\tstruct object_id oid;\n-\t\tconst char *p;\n-\n-\t\tif (parse_oid_hex(namebuf, &oid, &p) || *p != '\\n')\n-\t\t\tbreak;\n-\n-\t\to = lookup_object(the_repository, &oid);\n-\t\tif (o && o->type == OBJ_COMMIT) {\n-\t\t\to->flags &= ~TMP_MARK;\n-\t\t}\n-\t}\n-\tfor (i = get_max_object_index(the_repository); 0 < i; i--) {\n-\t\to = get_indexed_object(the_repository, i - 1);\n-\t\tif (o && o->type == OBJ_COMMIT &&\n-\t\t    (o->flags & TMP_MARK)) {\n-\t\t\tadd_object_array(o, NULL, reachable);\n-\t\t\t\to->flags &= ~TMP_MARK;\n-\t\t}\n-\t}\n-\tclose(cmd.out);\n-\n-\tif (finish_command(&cmd)) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tret = 0;\n-\n-out:\n-\tchild_process_clear(&cmd);\n-\treturn ret;\n-}\n-\n static int has_unreachable(struct object_array *src, enum allow_uor allow_uor)\n {\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n@@ -881,29 +831,11 @@ static void deepen(struct upload_pack_data *data, int depth)\n \t\t\tstruct object *object = data->shallows.objects[i].item;\n \t\t\tobject->flags |= NOT_SHALLOW;\n \t\t}\n-\t} else if (data->deepen_relative) {\n-\t\tstruct object_array reachable_shallows = OBJECT_ARRAY_INIT;\n-\t\tstruct commit_list *result;\n-\n-\t\t/*\n-\t\t * Checking for reachable shallows requires that our refs be\n-\t\t * marked with OUR_REF.\n-\t\t */\n-\t\trefs_head_ref_namespaced(get_main_ref_store(the_repository),\n-\t\t\t\t\t check_ref, data);\n-\t\tfor_each_namespaced_ref_1(check_ref, data);\n-\n-\t\tget_reachable_list(data, &reachable_shallows);\n-\t\tresult = get_shallow_commits(&reachable_shallows,\n-\t\t\t\t\t     depth + 1,\n-\t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n-\t\tsend_shallow(data, result);\n-\t\tfree_commit_list(result);\n-\t\tobject_array_clear(&reachable_shallows);\n \t} else {\n \t\tstruct commit_list *result;\n \n-\t\tresult = get_shallow_commits(&data->want_obj, depth,\n+\t\tresult = get_shallow_commits(&data->want_obj, &data->shallows,\n+\t\t\t\t\t     data->deepen_relative, depth,\n \t\t\t\t\t     SHALLOW, NOT_SHALLOW);\n \t\tsend_shallow(data, result);\n \t\tfree_commit_list(result);\n-- \ngitgitgadget\n"},{"id":"536557","messageId":"xmqqseavujqy.fsf@gitster.g","threadId":"64603","inReplyTo":"pull.2121.v5.git.git.1771186316.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 0/2] shallow: handling fetch relative-deepen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-20T22:34:45Z","receivedAt":"2026-02-20T22:34:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Samo Pogačnik via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> When a shallowed repository gets deepened beyond the beginning of a merged\n> branch, we may endup with some shallows, that are behind the reachable ones.\n> Added test 'fetching deepen beyond merged branch' exposes that behaviour.\n\n\nWe didn't see any response to the latest round, and the comments on\nprevious rounds seem to have been addressed.  Is this ready to be\nmerged down to 'next' now?\n\nThanks.\n"}]}