{"thread":{"id":"64521","subject":"[PATCH] Fixed --shallow-since generating descendant borders","startedAt":"2025-11-22T10:38:36Z","lastAt":"2026-03-07T07:21:01Z","messageCount":10,"participants":["Samo Pogačnik via GitGitGadget","Junio C Hamano","Samo Pogačnik"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"531160","messageId":"pull.2107.git.git.1763807914242.gitgitgadget@gmail.com","threadId":"64521","inReplyTo":null,"subject":"[PATCH] Fixed --shallow-since generating descendant borders","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-11-22T10:38:34Z","receivedAt":"2025-11-22T10:38:36Z","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 shallow cloning based on a date, it happens that a list\nof commits is received, where some of the list border commits\nactually descend one from another. In such cases borders need\nto be expanded by additional parents and excluding the child\nas border.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n    Fixed --shallow-since generating descendant borders\n    \n    When shallow cloning based on a date, it happens that a list of commits\n    is received, where some of the list border commits actually descend one\n    from another. In such cases borders need to be expanded by additional\n    parents and excluding the child as border.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2107%2Fspog%2Ffix-shallow-since-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2107/spog/fix-shallow-since-v1\nPull-Request: https://github.com/git/git/pull/2107\n\n shallow.c | 35 ++++++++++++++++++++++++++++++++---\n 1 file changed, 32 insertions(+), 3 deletions(-)\n\ndiff --git a/shallow.c b/shallow.c\nindex 55b9cd9d3f..37079a0bf1 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -251,21 +251,50 @@ struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n \t * commit A is processed first, then commit B, whose parent is\n \t * A, later. If NOT_SHALLOW on A is cleared at step 1, B\n \t * itself is considered border at step 2, which is incorrect.\n+\t * We must also consider that B has multiple parents, some of\n+\t * them not being in the not_shallow_list, but must be added\n+\t * as border commits to the result.\n+\t *\n+\t * The general processing goes like this:\n+\t * 1. Above we've coloured the whole not_shallow_list of commits\n+\t *    with 'not_shallow'.\n+\t * 2. For each commit from the not_shallow_list (the code below)\n+\t *    we colour 'shallow' the commit and its parents, which are not\n+\t *    already coloured 'not_shallow'.\n+\t * 3. Commits with all parents being coloured only 'shallow' remain\n+\t *    shallow and are being added to result list.\n+\t * 4. Commits without all parents being coloured only 'shallow' are\n+\t *    being excluded as borders, however their parents coloured only\n+\t *    'shallow' are being added to the result borders list.\n \t */\n \tfor (p = not_shallow_list; p; p = p->next) {\n \t\tstruct commit *c = p->item;\n \t\tstruct commit_list *parent;\n+\t\tint must_not_be_shallow = 0;\n \n \t\tif (repo_parse_commit(the_repository, c))\n \t\t\tdie(\"unable to parse commit %s\",\n \t\t\t    oid_to_hex(&c->object.oid));\n \n \t\tfor (parent = c->parents; parent; parent = parent->next)\n-\t\t\tif (!(parent->item->object.flags & not_shallow_flag)) {\n+\t\t\tif (parent->item->object.flags & not_shallow_flag) {\n+\t\t\t\tmust_not_be_shallow = 1;\n+\t\t\t} else {\n \t\t\t\tc->object.flags |= shallow_flag;\n-\t\t\t\tcommit_list_insert(c, &result);\n-\t\t\t\tbreak;\n+\t\t\t\tparent->item->object.flags |= shallow_flag;\n \t\t\t}\n+\t\tif (must_not_be_shallow) {\n+\t\t\tc->object.flags &= ~shallow_flag;\n+\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n+\t\t\t\tif (parent->item->object.flags & shallow_flag) {\n+\t\t\t\t\tparent->item->object.flags |= not_shallow_flag;\n+\t\t\t\t\tcommit_list_insert(parent->item, &result);\n+\t\t\t\t}\n+\t\t} else {\n+\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n+\t\t\t\tparent->item->object.flags &= ~shallow_flag;\n+\t\t\tcommit_list_insert(c, &result);\n+\t\t}\n \t}\n \tfree_commit_list(not_shallow_list);\n \n\nbase-commit: debbc87557487aa9a8ed8a35367d17f8b4081c76\n-- \ngitgitgadget\n"},{"id":"531163","messageId":"xmqqo6ou2cmd.fsf@gitster.g","threadId":"64521","inReplyTo":"pull.2107.git.git.1763807914242.gitgitgadget@gmail.com","subject":"Re: [PATCH] Fixed --shallow-since generating descendant borders","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-22T17:36:26Z","receivedAt":"2025-11-22T17:36:29Z","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> Subject: Re: [PATCH] Fixed --shallow-since generating descendant borders\n\nA patch title wants maximum information density, and \"Fixed\" is a\nvague verb in that context, as it does not tell what existing\nbehaviour was wrong or what is the right alternative behaviour.\n\nAs \"git log --oneline --no-merges\" can show, our patches typically\nbegins with the area the change pertains to plus a colon.  \"git log\n--oneline shallow.c\" (which is the file this patch touches) shows a\nhandful ones that are prefixed with \"shallow:\".\n\nWhat to write after the \"<area>:\" prefix for this patch, I am not\nsure, as the body of the proposed log message does not discuss the\nbad effect of the suboptimal or wrong (I cannot even tell which one\nfrom the proposed log message) behaviour.\n\n> From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n>\n> When shallow cloning based on a date, it happens that a list\n> of commits is received, where some of the list border commits\n> actually descend one from another. In such cases borders need\n> to be expanded by additional parents and excluding the child\n> as border.\n\nMissing from the above description are\n\n - received by whom?\n\n - the reason why they want such a list is to do what?\n\n - when there are multiple borders that can be \"expanded\", and if\n   you leave it unexpanded (i.e., the behaviour of the current code)\n   what happens and why is it bad?  Is it breaking bad (e.g., clone\n   would be aborted, the resulting cloned repository does not pass\n   fsck), or is it suboptimal bad (e.g., we told the command that we\n   do not want commits older than date X, but we end up having more\n   commits)?\n\n - what is the cost of computing the \"expansion\", relative to the\n   above \"badness\"?  Fixing a breaking bad behaviour can of course\n   afford to spend more cycles than a suboptimal bad behaviour.\n\nThe usual way to compose a log message of this project is to\n\n - Give an observation on how the current system works in the\n   present tense (so no need to say \"Currently X is Y\", or\n   \"Previously X was Y\" to describe the state before your change;\n   just \"X is Y\" is enough), and discuss what you perceive as a\n   problem in it.\n\n - Propose a solution (optional---often, problem description\n   trivially leads to an obvious solution in reader's minds).\n\n - Give commands to somebody editing the codebase to \"make it so\",\n   instead of saying \"This commit does X\".\n\nin this order.\n\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2107%2Fspog%2Ffix-shallow-since-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2107/spog/fix-shallow-since-v1\n> Pull-Request: https://github.com/git/git/pull/2107\n>\n>  shallow.c | 35 ++++++++++++++++++++++++++++++++---\n>  1 file changed, 32 insertions(+), 3 deletions(-)\n\nWe'd want to protect this change from other people accidentally\nbreaking it in the future, and the best practice we have is to write\na test to observe end-user visible behaviour.  The whole helper\nfunction being touched by the patch came in the 27-patch series\nmerged at a460ea4a (Merge branch 'nd/shallow-deepen', 2016-10-10),\nand it seems to have added to t5500-fetch-pack.sh to cover the\n\"--shallow-since\" feature.  Perhaps this should add a few tests to\ndemonstrate existing \"breakage\" (i.e., a \"test_expect_success\" test\nthat would fail without the change in the patch to shallow.c we see\nbelow, and would succeed when the patch to shallow.c is applied).\n\nThanks.\n"},{"id":"531201","messageId":"pull.2107.v2.git.git.1763926552033.gitgitgadget@gmail.com","threadId":"64521","inReplyTo":"pull.2107.git.git.1763807914242.gitgitgadget@gmail.com","subject":"[PATCH v2] shallow: set borders which are all reachable after clone shallow since","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-11-23T19:35:52Z","receivedAt":"2025-11-23T19:35:55Z","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 shallow cloning based on a date, it happens that not all\nshallow border commits are reachable.\n\nOriginal implementation of a generic shallow boundary finder\nbased on rev-list sets a commit (from the initial list of border\ncommit candidates) to be the border commit as soon as it finds one\nof its parentis that wasn't on the list of initial candidates. This\nresults in a successful shallow clone, where some of its declared\nborder commits may not be reachable and they would not actually exist\nin the cloned repository. Thus the result may contradict existing\ncomment in the code, which correctly states that such commmit should\nnot be considered border.\n\nOne can inspect such case by running the added test scenario:\n- 'clone shallow since all borders reachable'\n\nThe modified implementation of a generic shallow boundary finder\nbased on rev-list ensures that all shallow border commits are reachable\nalso after being grafted. This is achieved by inspecting all parents\nof each initial border commit candidate. The border commit candidate\nis set border only when all its parents wern't on the initial list of\ncandidates. Otherwise the border commit candidate is not set as border\nhowever its parents that weren't on the list of candidates are set as\nborders.\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n    Fixed --shallow-since generating descendant borders\n    \n    When shallow cloning based on a date, it happens that a list of commits\n    is received, where some of the list border commits actually descend one\n    from another. In such cases borders need to be expanded by additional\n    parents and excluding the child as border.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2107%2Fspog%2Ffix-shallow-since-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2107/spog/fix-shallow-since-v2\nPull-Request: https://github.com/git/git/pull/2107\n\nRange-diff vs v1:\n\n 1:  aaeff44f83 ! 1:  479692c386 Fixed --shallow-since generating descendant borders\n     @@ Metadata\n      Author: Samo Pogačnik <samo_pogacnik@t-2.net>\n      \n       ## Commit message ##\n     -    Fixed --shallow-since generating descendant borders\n     +    shallow: set borders which are all reachable after clone shallow since\n      \n     -    When shallow cloning based on a date, it happens that a list\n     -    of commits is received, where some of the list border commits\n     -    actually descend one from another. In such cases borders need\n     -    to be expanded by additional parents and excluding the child\n     -    as border.\n     +    When shallow cloning based on a date, it happens that not all\n     +    shallow border commits are reachable.\n     +\n     +    Original implementation of a generic shallow boundary finder\n     +    based on rev-list sets a commit (from the initial list of border\n     +    commit candidates) to be the border commit as soon as it finds one\n     +    of its parentis that wasn't on the list of initial candidates. This\n     +    results in a successful shallow clone, where some of its declared\n     +    border commits may not be reachable and they would not actually exist\n     +    in the cloned repository. Thus the result may contradict existing\n     +    comment in the code, which correctly states that such commmit should\n     +    not be considered border.\n     +\n     +    One can inspect such case by running the added test scenario:\n     +    - 'clone shallow since all borders reachable'\n     +\n     +    The modified implementation of a generic shallow boundary finder\n     +    based on rev-list ensures that all shallow border commits are reachable\n     +    also after being grafted. This is achieved by inspecting all parents\n     +    of each initial border commit candidate. The border commit candidate\n     +    is set border only when all its parents wern't on the initial list of\n     +    candidates. Otherwise the border commit candidate is not set as border\n     +    however its parents that weren't on the list of candidates are set as\n     +    borders.\n      \n          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n      \n     @@ shallow.c: struct commit_list *get_shallow_commits_by_rev_list(struct strvec *ar\n       \t * commit A is processed first, then commit B, whose parent is\n       \t * A, later. If NOT_SHALLOW on A is cleared at step 1, B\n       \t * itself is considered border at step 2, which is incorrect.\n     -+\t * We must also consider that B has multiple parents, some of\n     -+\t * them not being in the not_shallow_list, but must be added\n     -+\t * as border commits to the result.\n     ++\t *\n     ++\t * We must also consider that B has multiple parents which may\n     ++\t * not all be marked NOT_SHALLOW (as they weren't traversed into\n     ++\t * the not_shallow_list from revs in the first place). Because of\n     ++\t * that an additional step is required to reconsider B as border.\n     ++\t * A commit from the not_shallow_list is considered border only\n     ++\t * when ALL its parents weren't on the not_shallow_list.\n     ++\t * When one or more parents of a commit from the not_shellow_list\n     ++\t * also come from that list, the commit is not considered border,\n     ++\t * but its non-listed parents are considered border commits.\n      +\t *\n      +\t * The general processing goes like this:\n     -+\t * 1. Above we've coloured the whole not_shallow_list of commits\n     -+\t *    with 'not_shallow'.\n     ++\t * 1. Above we've painted the whole not_shallow_list of commits\n     ++\t *    NOT_SHALLOW.\n      +\t * 2. For each commit from the not_shallow_list (the code below)\n     -+\t *    we colour 'shallow' the commit and its parents, which are not\n     -+\t *    already coloured 'not_shallow'.\n     -+\t * 3. Commits with all parents being coloured only 'shallow' remain\n     ++\t *    we paint SHALLOW this commit and its parent for all its\n     ++\t *    parents that had not yet been painted NOT_SHALLOW.\n     ++\t * 3. Commits with all parents being painted only SHALLOW remain\n      +\t *    shallow and are being added to result list.\n     -+\t * 4. Commits without all parents being coloured only 'shallow' are\n     -+\t *    being excluded as borders, however their parents coloured only\n     -+\t *    'shallow' are being added to the result borders list.\n     ++\t * 4. Commits without all parents being painted only SHALLOW are\n     ++\t *    being excluded as borders, however their parents painted only\n     ++\t *    SHALLOW are being added to the result borders list.\n       \t */\n       \tfor (p = not_shallow_list; p; p = p->next) {\n       \t\tstruct commit *c = p->item;\n     @@ shallow.c: struct commit_list *get_shallow_commits_by_rev_list(struct strvec *ar\n       \t}\n       \tfree_commit_list(not_shallow_list);\n       \n     +\n     + ## t/t5500-fetch-pack.sh ##\n     +@@ t/t5500-fetch-pack.sh: test_expect_success 'shallow since with commit graph and already-seen commit' '\n     + \t)\n     + '\n     + \n     ++test_expect_success 'clone shallow since all borders reachable' '\n     ++\ttest_create_repo shallow-since-all-borders-reachable &&\n     ++\t(\n     ++\trm -rf shallow123 &&\n     ++\tcd shallow-since-all-borders-reachable &&\n     ++\tGIT_COMMITTER_DATE=\"2025-08-19 12:34:56\" git commit --allow-empty -m one &&\n     ++\tGIT_COMMITTER_DATE=\"2025-08-20 12:34:56\" git switch -c branch &&\n     ++\tGIT_COMMITTER_DATE=\"2025-08-21 12:34:56\" git commit --allow-empty -m two &&\n     ++\tGIT_COMMITTER_DATE=\"2025-08-22 12:34:56\" git commit --allow-empty -m three &&\n     ++\tGIT_COMMITTER_DATE=\"2025-08-23 12:34:56\" git switch main &&\n     ++\tGIT_COMMITTER_DATE=\"2025-08-24 12:34:56\" git merge branch --no-ff &&\n     ++\tGIT_COMMITTER_DATE=\"2025-08-26 12:34:56\" git clone --shallow-since \"2025-08-21 12:34:56\" \"file://$(pwd)/.\" ../shallow123 &&\n     ++\tcd ../shallow123 &&\n     ++\techo \"Shallow borders:\" &&\n     ++\tcat .git/shallow &&\n     ++\t$(for commit in $(cat .git/shallow); do git rev-list $commit 1>/dev/null || exit 1; done)\n     ++\t)\n     ++'\n     ++\n     + test_expect_success 'shallow clone exclude tag two' '\n     + \ttest_create_repo shallow-exclude &&\n     + \t(\n\n\n shallow.c             | 42 +++++++++++++++++++++++++++++++++++++++---\n t/t5500-fetch-pack.sh | 19 +++++++++++++++++++\n 2 files changed, 58 insertions(+), 3 deletions(-)\n\ndiff --git a/shallow.c b/shallow.c\nindex 55b9cd9d3f..2929ac90ee 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -251,21 +251,57 @@ struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n \t * commit A is processed first, then commit B, whose parent is\n \t * A, later. If NOT_SHALLOW on A is cleared at step 1, B\n \t * itself is considered border at step 2, which is incorrect.\n+\t *\n+\t * We must also consider that B has multiple parents which may\n+\t * not all be marked NOT_SHALLOW (as they weren't traversed into\n+\t * the not_shallow_list from revs in the first place). Because of\n+\t * that an additional step is required to reconsider B as border.\n+\t * A commit from the not_shallow_list is considered border only\n+\t * when ALL its parents weren't on the not_shallow_list.\n+\t * When one or more parents of a commit from the not_shellow_list\n+\t * also come from that list, the commit is not considered border,\n+\t * but its non-listed parents are considered border commits.\n+\t *\n+\t * The general processing goes like this:\n+\t * 1. Above we've painted the whole not_shallow_list of commits\n+\t *    NOT_SHALLOW.\n+\t * 2. For each commit from the not_shallow_list (the code below)\n+\t *    we paint SHALLOW this commit and its parent for all its\n+\t *    parents that had not yet been painted NOT_SHALLOW.\n+\t * 3. Commits with all parents being painted only SHALLOW remain\n+\t *    shallow and are being added to result list.\n+\t * 4. Commits without all parents being painted only SHALLOW are\n+\t *    being excluded as borders, however their parents painted only\n+\t *    SHALLOW are being added to the result borders list.\n \t */\n \tfor (p = not_shallow_list; p; p = p->next) {\n \t\tstruct commit *c = p->item;\n \t\tstruct commit_list *parent;\n+\t\tint must_not_be_shallow = 0;\n \n \t\tif (repo_parse_commit(the_repository, c))\n \t\t\tdie(\"unable to parse commit %s\",\n \t\t\t    oid_to_hex(&c->object.oid));\n \n \t\tfor (parent = c->parents; parent; parent = parent->next)\n-\t\t\tif (!(parent->item->object.flags & not_shallow_flag)) {\n+\t\t\tif (parent->item->object.flags & not_shallow_flag) {\n+\t\t\t\tmust_not_be_shallow = 1;\n+\t\t\t} else {\n \t\t\t\tc->object.flags |= shallow_flag;\n-\t\t\t\tcommit_list_insert(c, &result);\n-\t\t\t\tbreak;\n+\t\t\t\tparent->item->object.flags |= shallow_flag;\n \t\t\t}\n+\t\tif (must_not_be_shallow) {\n+\t\t\tc->object.flags &= ~shallow_flag;\n+\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n+\t\t\t\tif (parent->item->object.flags & shallow_flag) {\n+\t\t\t\t\tparent->item->object.flags |= not_shallow_flag;\n+\t\t\t\t\tcommit_list_insert(parent->item, &result);\n+\t\t\t\t}\n+\t\t} else {\n+\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n+\t\t\t\tparent->item->object.flags &= ~shallow_flag;\n+\t\t\tcommit_list_insert(c, &result);\n+\t\t}\n \t}\n \tfree_commit_list(not_shallow_list);\n \ndiff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\nindex 2677cd5faa..12209887fb 100755\n--- a/t/t5500-fetch-pack.sh\n+++ b/t/t5500-fetch-pack.sh\n@@ -904,6 +904,25 @@ test_expect_success 'shallow since with commit graph and already-seen commit' '\n \t)\n '\n \n+test_expect_success 'clone shallow since all borders reachable' '\n+\ttest_create_repo shallow-since-all-borders-reachable &&\n+\t(\n+\trm -rf shallow123 &&\n+\tcd shallow-since-all-borders-reachable &&\n+\tGIT_COMMITTER_DATE=\"2025-08-19 12:34:56\" git commit --allow-empty -m one &&\n+\tGIT_COMMITTER_DATE=\"2025-08-20 12:34:56\" git switch -c branch &&\n+\tGIT_COMMITTER_DATE=\"2025-08-21 12:34:56\" git commit --allow-empty -m two &&\n+\tGIT_COMMITTER_DATE=\"2025-08-22 12:34:56\" git commit --allow-empty -m three &&\n+\tGIT_COMMITTER_DATE=\"2025-08-23 12:34:56\" git switch main &&\n+\tGIT_COMMITTER_DATE=\"2025-08-24 12:34:56\" git merge branch --no-ff &&\n+\tGIT_COMMITTER_DATE=\"2025-08-26 12:34:56\" git clone --shallow-since \"2025-08-21 12:34:56\" \"file://$(pwd)/.\" ../shallow123 &&\n+\tcd ../shallow123 &&\n+\techo \"Shallow borders:\" &&\n+\tcat .git/shallow &&\n+\t$(for commit in $(cat .git/shallow); do git rev-list $commit 1>/dev/null || exit 1; done)\n+\t)\n+'\n+\n test_expect_success 'shallow clone exclude tag two' '\n \ttest_create_repo shallow-exclude &&\n \t(\n\nbase-commit: debbc87557487aa9a8ed8a35367d17f8b4081c76\n-- \ngitgitgadget\n"},{"id":"531247","messageId":"xmqqh5ujuekq.fsf@gitster.g","threadId":"64521","inReplyTo":"pull.2107.v2.git.git.1763926552033.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] shallow: set borders which are all reachable after clone shallow since","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-25T00:43:33Z","receivedAt":"2025-11-25T00:43: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> From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= <samo_pogacnik@t-2.net>\n>\n> When shallow cloning based on a date, it happens that not all\n> shallow border commits are reachable.\n>\n> Original implementation of a generic shallow boundary finder\n> based on rev-list sets a commit (from the initial list of border\n> commit candidates) to be the border commit as soon as it finds one\n> of its parentis that wasn't on the list of initial candidates. This\n> results in a successful shallow clone, where some of its declared\n> border commits may not be reachable and they would not actually exist\n> in the cloned repository. Thus the result may contradict existing\n> comment in the code, which correctly states that such commmit should\n> not be considered border.\n>\n> One can inspect such case by running the added test scenario:\n> - 'clone shallow since all borders reachable'\n>\n> The modified implementation of a generic shallow boundary finder\n> based on rev-list ensures that all shallow border commits are reachable\n> also after being grafted. This is achieved by inspecting all parents\n> of each initial border commit candidate. The border commit candidate\n> is set border only when all its parents wern't on the initial list of\n> candidates. Otherwise the border commit candidate is not set as border\n> however its parents that weren't on the list of candidates are set as\n> borders.\n\nIt is a minor point, but there are \"boundary\" and \"border\" used more\nor less interchangeably in the proposed commit log message, and\nwould make the readers wonder if there are differences (I do not\nthink we use the word \"border\" anywhere in our documentation).  It\nis minor as we do not have such mixture in the end-user facing part\nof the documentation with this patch.\n\nI'll let those (cc'ed) who may be more familiar with, or, at least\nhave more code than I have in, the shallow infrastructure to comment\non the way the updated code uses the revision machinery.\n\nThanks.\n\n> diff --git a/shallow.c b/shallow.c\n> index 55b9cd9d3f..2929ac90ee 100644\n> --- a/shallow.c\n> +++ b/shallow.c\n> @@ -251,21 +251,57 @@ struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n>  \t * commit A is processed first, then commit B, whose parent is\n>  \t * A, later. If NOT_SHALLOW on A is cleared at step 1, B\n>  \t * itself is considered border at step 2, which is incorrect.\n> +\t *\n> +\t * We must also consider that B has multiple parents which may\n> +\t * not all be marked NOT_SHALLOW (as they weren't traversed into\n> +\t * the not_shallow_list from revs in the first place). Because of\n> +\t * that an additional step is required to reconsider B as border.\n> +\t * A commit from the not_shallow_list is considered border only\n> +\t * when ALL its parents weren't on the not_shallow_list.\n> +\t * When one or more parents of a commit from the not_shellow_list\n> +\t * also come from that list, the commit is not considered border,\n> +\t * but its non-listed parents are considered border commits.\n> +\t *\n> +\t * The general processing goes like this:\n> +\t * 1. Above we've painted the whole not_shallow_list of commits\n> +\t *    NOT_SHALLOW.\n> +\t * 2. For each commit from the not_shallow_list (the code below)\n> +\t *    we paint SHALLOW this commit and its parent for all its\n> +\t *    parents that had not yet been painted NOT_SHALLOW.\n> +\t * 3. Commits with all parents being painted only SHALLOW remain\n> +\t *    shallow and are being added to result list.\n> +\t * 4. Commits without all parents being painted only SHALLOW are\n> +\t *    being excluded as borders, however their parents painted only\n> +\t *    SHALLOW are being added to the result borders list.\n>  \t */\n>  \tfor (p = not_shallow_list; p; p = p->next) {\n>  \t\tstruct commit *c = p->item;\n>  \t\tstruct commit_list *parent;\n> +\t\tint must_not_be_shallow = 0;\n>  \n>  \t\tif (repo_parse_commit(the_repository, c))\n>  \t\t\tdie(\"unable to parse commit %s\",\n>  \t\t\t    oid_to_hex(&c->object.oid));\n>  \n>  \t\tfor (parent = c->parents; parent; parent = parent->next)\n> -\t\t\tif (!(parent->item->object.flags & not_shallow_flag)) {\n> +\t\t\tif (parent->item->object.flags & not_shallow_flag) {\n> +\t\t\t\tmust_not_be_shallow = 1;\n> +\t\t\t} else {\n>  \t\t\t\tc->object.flags |= shallow_flag;\n> -\t\t\t\tcommit_list_insert(c, &result);\n> -\t\t\t\tbreak;\n> +\t\t\t\tparent->item->object.flags |= shallow_flag;\n>  \t\t\t}\n> +\t\tif (must_not_be_shallow) {\n> +\t\t\tc->object.flags &= ~shallow_flag;\n> +\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n> +\t\t\t\tif (parent->item->object.flags & shallow_flag) {\n> +\t\t\t\t\tparent->item->object.flags |= not_shallow_flag;\n> +\t\t\t\t\tcommit_list_insert(parent->item, &result);\n> +\t\t\t\t}\n> +\t\t} else {\n> +\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n> +\t\t\t\tparent->item->object.flags &= ~shallow_flag;\n> +\t\t\tcommit_list_insert(c, &result);\n> +\t\t}\n>  \t}\n>  \tfree_commit_list(not_shallow_list);\n>  \n> diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\n> index 2677cd5faa..12209887fb 100755\n> --- a/t/t5500-fetch-pack.sh\n> +++ b/t/t5500-fetch-pack.sh\n> @@ -904,6 +904,25 @@ test_expect_success 'shallow since with commit graph and already-seen commit' '\n>  \t)\n>  '\n>  \n> +test_expect_success 'clone shallow since all borders reachable' '\n> +\ttest_create_repo shallow-since-all-borders-reachable &&\n> +\t(\n> +\trm -rf shallow123 &&\n> +\tcd shallow-since-all-borders-reachable &&\n> +\tGIT_COMMITTER_DATE=\"2025-08-19 12:34:56\" git commit --allow-empty -m one &&\n> +\tGIT_COMMITTER_DATE=\"2025-08-20 12:34:56\" git switch -c branch &&\n> +\tGIT_COMMITTER_DATE=\"2025-08-21 12:34:56\" git commit --allow-empty -m two &&\n> +\tGIT_COMMITTER_DATE=\"2025-08-22 12:34:56\" git commit --allow-empty -m three &&\n> +\tGIT_COMMITTER_DATE=\"2025-08-23 12:34:56\" git switch main &&\n> +\tGIT_COMMITTER_DATE=\"2025-08-24 12:34:56\" git merge branch --no-ff &&\n> +\tGIT_COMMITTER_DATE=\"2025-08-26 12:34:56\" git clone --shallow-since \"2025-08-21 12:34:56\" \"file://$(pwd)/.\" ../shallow123 &&\n> +\tcd ../shallow123 &&\n> +\techo \"Shallow borders:\" &&\n> +\tcat .git/shallow &&\n> +\t$(for commit in $(cat .git/shallow); do git rev-list $commit 1>/dev/null || exit 1; done)\n> +\t)\n> +'\n> +\n>  test_expect_success 'shallow clone exclude tag two' '\n>  \ttest_create_repo shallow-exclude &&\n>  \t(\n>\n> base-commit: debbc87557487aa9a8ed8a35367d17f8b4081c76\n"},{"id":"534293","messageId":"xmqqfr80xanx.fsf@gitster.g","threadId":"64521","inReplyTo":"xmqqh5ujuekq.fsf@gitster.g","subject":"Re: [PATCH v2] shallow: set borders which are all reachable after clone shallow since","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-20T20:59:46Z","receivedAt":"2026-01-20T20:59:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n>> ...\n>> The modified implementation of a generic shallow boundary finder\n>> based on rev-list ensures that all shallow border commits are reachable\n>> also after being grafted. This is achieved by inspecting all parents\n>> of each initial border commit candidate. The border commit candidate\n>> is set border only when all its parents wern't on the initial list of\n>> candidates. Otherwise the border commit candidate is not set as border\n>> however its parents that weren't on the list of candidates are set as\n>> borders.\n>\n> It is a minor point, but there are \"boundary\" and \"border\" used more\n> or less interchangeably in the proposed commit log message, and\n> would make the readers wonder if there are differences (I do not\n> think we use the word \"border\" anywhere in our documentation).  It\n> is minor as we do not have such mixture in the end-user facing part\n> of the documentation with this patch.\n>\n> I'll let those (cc'ed) who may be more familiar with, or, at least\n> have more code than I have in, the shallow infrastructure to comment\n> on the way the updated code uses the revision machinery.\n\nAfter this exchange, the topic has been dormant for almost full two\nmonths.  As I do not deal with shallow clones myself, even though I\nunderstand that some folks rely on it working, I'd really prefer to\nsee somebody who are familiar with the underlying logic to review\nthis patch if we were to move forward with it.\n\nThanks.\n\n\n"},{"id":"534741","messageId":"3253600a3c96144744d3371a7ec2a66cb87d4b60.camel@t-2.net","threadId":"64521","inReplyTo":"xmqqfr80xanx.fsf@gitster.g","subject":"Re: [PATCH v2] shallow: set borders which are all reachable after clone shallow since","fromName":"Samo Pogačnik","fromEmail":"samo_pogacnik@t-2.net","sentAt":"2026-01-28T04:23:53Z","receivedAt":"2026-01-28T04:30:34Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"On Tue, 2026-01-20 at 12:59 -0800, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > > ...\n> > > The modified implementation of a generic shallow boundary finder\n> > > based on rev-list ensures that all shallow border commits are reachable\n> > > also after being grafted. This is achieved by inspecting all parents\n> > > of each initial border commit candidate. The border commit candidate\n> > > is set border only when all its parents wern't on the initial list of\n> > > candidates. Otherwise the border commit candidate is not set as border\n> > > however its parents that weren't on the list of candidates are set as\n> > > borders.\n> > \n> > It is a minor point, but there are \"boundary\" and \"border\" used more\n> > or less interchangeably in the proposed commit log message, and\n> > would make the readers wonder if there are differences (I do not\n> > think we use the word \"border\" anywhere in our documentation).  It\n> > is minor as we do not have such mixture in the end-user facing part\n> > of the documentation with this patch.\n> > \n> > I'll let those (cc'ed) who may be more familiar with, or, at least\n> > have more code than I have in, the shallow infrastructure to comment\n> > on the way the updated code uses the revision machinery.\n> \n> After this exchange, the topic has been dormant for almost full two\n> months.  As I do not deal with shallow clones myself, even though I\n> understand that some folks rely on it working, I'd really prefer to\n> see somebody who are familiar with the underlying logic to review\n> this patch if we were to move forward with it.\n> \n\nI’m currently rewriting the patch and the commit message trying to\naddress the boundary/border dilemma. I hope to be able to send a new\nversion by the end of this week.\n\nBest regards,\nSamo\n"},{"id":"534917","messageId":"pull.2107.v3.git.git.1769876930544.gitgitgadget@gmail.com","threadId":"64521","inReplyTo":"pull.2107.v2.git.git.1763926552033.gitgitgadget@gmail.com","subject":"[PATCH v3] shallow: ensure all boundary commits are reachable with --shallow-since","fromName":"Samo Pogačnik via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-31T16:28:50Z","receivedAt":"2026-01-31T16:28:53Z","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 performing a shallow clone based on a date, it is possible for some\ndeclared shallow boundary commits to be unreachable.\n\nThe original implementation of the generic shallow boundary finder based\non rev-list marks a commit (from the initial list of boundary candidates)\nas shallow as soon as it finds one parent that is not in the initial\ncandidate list. This can result in a successful shallow clone where some\ndeclared boundary commits are not reachable and therefore do not exist\nin the cloned repository.\n\nIn such cases, the result contradicts the existing code comment, which\ncorrectly states that boundary commit candidates with a parent in the\nsame candidate list must not be considered boundary commits.\n\nThe added test case 'clone shallow-since all shallows reachable' exposes\nthe problem. For example:\n\n0. Original repository\n   Graph:\n   *   e5fbe33 (HEAD -> main) Apr_4th_13:14:15\n   |\\\n   | * 72f5b73 (branch) Apr_3rd_13:14:15\n   |/\n   * 0ba76c8 Apr_2nd_13:14:15\n   * f58ea3a Apr_1st_13:14:15\n\n1. Clone with --shallow-since=\"2005-04-03 13:14:15\"\n   Shallows:\n     e5fbe33724032807ab2d8636d4c9161f6716882d\n     72f5b73c5fec9a728adc42c23de1b87bb5b3ab16\n   Graph:\n     * e5fbe33 (grafted, HEAD -> main, ...) Apr_4th_13:14:15\n\n   Note that the second shallow commit,\n   72f5b73c5fec9a728adc42c23de1b87bb5b3ab16,\n   is not reachable.\n\nUpdate the generic shallow boundary finder to ensure that all shallow\nboundary commits are reachable. This is done by inspecting all parents of\neach initial boundary candidate. A candidate is marked shallow only if\nall of its parents are not in the initial candidate list. Otherwise, the\ncandidate itself is not marked shallow, but its parents that are not in\nthe candidate list are marked as boundary commits instead.\n\nWith the same example, the corrected behavior results in:\n\n1. Clone with --shallow-since=\"2005-04-03 13:14:15\"\n   Shallows:\n     72f5b73c5fec9a728adc42c23de1b87bb5b3ab16\n     0ba76c8b72273477b59026bea93fcc792bf48747\n   Graph:\n     *   e5fbe33 (HEAD -> main, ...) Apr_4th_13:14:15\n     |\\\n     | * 72f5b73 (grafted) Apr_3rd_13:14:15\n     * 0ba76c8 (grafted) Apr_2nd_13:14:15\n\nSigned-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n---\n    Fixed --shallow-since generating descendant borders\n    \n    When shallow cloning based on a date, it happens that a list of commits\n    is received, where some of the list border commits actually descend one\n    from another. In such cases borders need to be expanded by additional\n    parents and excluding the child as border.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2107%2Fspog%2Ffix-shallow-since-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2107/spog/fix-shallow-since-v3\nPull-Request: https://github.com/git/git/pull/2107\n\nRange-diff vs v2:\n\n 1:  479692c386 ! 1:  34df169b67 shallow: set borders which are all reachable after clone shallow since\n     @@ Metadata\n      Author: Samo Pogačnik <samo_pogacnik@t-2.net>\n      \n       ## Commit message ##\n     -    shallow: set borders which are all reachable after clone shallow since\n     +    shallow: ensure all boundary commits are reachable with --shallow-since\n      \n     -    When shallow cloning based on a date, it happens that not all\n     -    shallow border commits are reachable.\n     +    When performing a shallow clone based on a date, it is possible for some\n     +    declared shallow boundary commits to be unreachable.\n      \n     -    Original implementation of a generic shallow boundary finder\n     -    based on rev-list sets a commit (from the initial list of border\n     -    commit candidates) to be the border commit as soon as it finds one\n     -    of its parentis that wasn't on the list of initial candidates. This\n     -    results in a successful shallow clone, where some of its declared\n     -    border commits may not be reachable and they would not actually exist\n     -    in the cloned repository. Thus the result may contradict existing\n     -    comment in the code, which correctly states that such commmit should\n     -    not be considered border.\n     +    The original implementation of the generic shallow boundary finder based\n     +    on rev-list marks a commit (from the initial list of boundary candidates)\n     +    as shallow as soon as it finds one parent that is not in the initial\n     +    candidate list. This can result in a successful shallow clone where some\n     +    declared boundary commits are not reachable and therefore do not exist\n     +    in the cloned repository.\n      \n     -    One can inspect such case by running the added test scenario:\n     -    - 'clone shallow since all borders reachable'\n     +    In such cases, the result contradicts the existing code comment, which\n     +    correctly states that boundary commit candidates with a parent in the\n     +    same candidate list must not be considered boundary commits.\n      \n     -    The modified implementation of a generic shallow boundary finder\n     -    based on rev-list ensures that all shallow border commits are reachable\n     -    also after being grafted. This is achieved by inspecting all parents\n     -    of each initial border commit candidate. The border commit candidate\n     -    is set border only when all its parents wern't on the initial list of\n     -    candidates. Otherwise the border commit candidate is not set as border\n     -    however its parents that weren't on the list of candidates are set as\n     -    borders.\n     +    The added test case 'clone shallow-since all shallows reachable' exposes\n     +    the problem. For example:\n     +\n     +    0. Original repository\n     +       Graph:\n     +       *   e5fbe33 (HEAD -> main) Apr_4th_13:14:15\n     +       |\\\n     +       | * 72f5b73 (branch) Apr_3rd_13:14:15\n     +       |/\n     +       * 0ba76c8 Apr_2nd_13:14:15\n     +       * f58ea3a Apr_1st_13:14:15\n     +\n     +    1. Clone with --shallow-since=\"2005-04-03 13:14:15\"\n     +       Shallows:\n     +         e5fbe33724032807ab2d8636d4c9161f6716882d\n     +         72f5b73c5fec9a728adc42c23de1b87bb5b3ab16\n     +       Graph:\n     +         * e5fbe33 (grafted, HEAD -> main, ...) Apr_4th_13:14:15\n     +\n     +       Note that the second shallow commit,\n     +       72f5b73c5fec9a728adc42c23de1b87bb5b3ab16,\n     +       is not reachable.\n     +\n     +    Update the generic shallow boundary finder to ensure that all shallow\n     +    boundary commits are reachable. This is done by inspecting all parents of\n     +    each initial boundary candidate. A candidate is marked shallow only if\n     +    all of its parents are not in the initial candidate list. Otherwise, the\n     +    candidate itself is not marked shallow, but its parents that are not in\n     +    the candidate list are marked as boundary commits instead.\n     +\n     +    With the same example, the corrected behavior results in:\n     +\n     +    1. Clone with --shallow-since=\"2005-04-03 13:14:15\"\n     +       Shallows:\n     +         72f5b73c5fec9a728adc42c23de1b87bb5b3ab16\n     +         0ba76c8b72273477b59026bea93fcc792bf48747\n     +       Graph:\n     +         *   e5fbe33 (HEAD -> main, ...) Apr_4th_13:14:15\n     +         |\\\n     +         | * 72f5b73 (grafted) Apr_3rd_13:14:15\n     +         * 0ba76c8 (grafted) Apr_2nd_13:14:15\n      \n          Signed-off-by: Samo Pogačnik <samo_pogacnik@t-2.net>\n      \n       ## shallow.c ##\n     +@@ shallow.c: static void show_commit(struct commit *commit, void *data)\n     + }\n     + \n     + /*\n     +- * Given rev-list arguments, run rev-list. All reachable commits\n     +- * except border ones are marked with not_shallow_flag. Border commits\n     +- * are marked with shallow_flag. The list of border/shallow commits\n     +- * are also returned.\n     ++ * Given rev-list arguments, run rev-list. All reachable commits except\n     ++ * shallow boundary commits are marked with not_shallow_flag.\n     ++ * Returned is a list of boundary commits marked with shallow_flag only.\n     +  */\n     + struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n     + \t\t\t\t\t\t    int shallow_flag,\n      @@ shallow.c: struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n     - \t * commit A is processed first, then commit B, whose parent is\n     - \t * A, later. If NOT_SHALLOW on A is cleared at step 1, B\n     - \t * itself is considered border at step 2, which is incorrect.\n     + \tif (!not_shallow_list)\n     + \t\tdie(\"no commits selected for shallow requests\");\n     + \n     +-\t/* Mark all reachable commits as NOT_SHALLOW */\n     ++\t/* Mark all reachable (listed) commits as NOT_SHALLOW */\n     + \tfor (p = not_shallow_list; p; p = p->next)\n     + \t\tp->item->object.flags |= not_shallow_flag;\n     + \n     + \t/*\n     +-\t * mark border commits SHALLOW + NOT_SHALLOW.\n     +-\t * We cannot clear NOT_SHALLOW right now. Imagine border\n     +-\t * commit A is processed first, then commit B, whose parent is\n     +-\t * A, later. If NOT_SHALLOW on A is cleared at step 1, B\n     +-\t * itself is considered border at step 2, which is incorrect.\n     ++\t * Mark shallow commits from the list as SHALLOW + NOT_SHALLOW.\n     ++\t * Do not clear NOT_SHALLOW flags immediately. Consider two listed\n     ++\t * commits, B and its parent A, where A is shallow. If A is processed\n     ++\t * first and its NOT_SHALLOW flag is cleared immediately, B would later\n     ++\t * be incorrectly marked SHALLOW when processed.\n     ++\t *\n     ++\t * Also, listed commits may have multiple parents, and not all parents\n     ++\t * are necessarily listed (as they were not all traversed into the\n     ++\t * not_shallow_list from the revs in the first place — not marked\n     ++\t * NOT_SHALLOW). Therefore:\n      +\t *\n     -+\t * We must also consider that B has multiple parents which may\n     -+\t * not all be marked NOT_SHALLOW (as they weren't traversed into\n     -+\t * the not_shallow_list from revs in the first place). Because of\n     -+\t * that an additional step is required to reconsider B as border.\n     -+\t * A commit from the not_shallow_list is considered border only\n     -+\t * when ALL its parents weren't on the not_shallow_list.\n     -+\t * When one or more parents of a commit from the not_shellow_list\n     -+\t * also come from that list, the commit is not considered border,\n     -+\t * but its non-listed parents are considered border commits.\n     ++\t * - A listed commit is marked SHALLOW only if none of its parents are\n     ++\t *   listed.\n     ++\t * - If at least one parent of a listed commit is also listed, the\n     ++\t *   commit itself is not marked SHALLOW; however, any of its non-listed\n     ++\t *   parents are marked SHALLOW.\n      +\t *\n     -+\t * The general processing goes like this:\n     -+\t * 1. Above we've painted the whole not_shallow_list of commits\n     -+\t *    NOT_SHALLOW.\n     -+\t * 2. For each commit from the not_shallow_list (the code below)\n     -+\t *    we paint SHALLOW this commit and its parent for all its\n     -+\t *    parents that had not yet been painted NOT_SHALLOW.\n     -+\t * 3. Commits with all parents being painted only SHALLOW remain\n     -+\t *    shallow and are being added to result list.\n     -+\t * 4. Commits without all parents being painted only SHALLOW are\n     -+\t *    being excluded as borders, however their parents painted only\n     -+\t *    SHALLOW are being added to the result borders list.\n     ++\t * Processing overview:\n     ++\t * 1. All listed commits have already been marked NOT_SHALLOW are not\n     ++\t *    cleared until all shallow commits have been identified.\n     ++\t *\n     ++\t * 2. For each listed commit:\n     ++\t *    - Mark the commit SHALLOW if it has any parent that is not listed.\n     ++\t *    - Mark all non-listed parents as SHALLOW.\n     ++\t *    - If the commit has at least one listed parent, it is excluded\n     ++\t *      from the shallow result; however its parents marked only SHALLOW\n     ++\t *      are added instead.\n     ++\t *    - If all parents are marked only SHALLOW, the commit remains SHALLOW\n     ++\t *      and is added to the shallow result.\n       \t */\n       \tfor (p = not_shallow_list; p; p = p->next) {\n       \t\tstruct commit *c = p->item;\n     @@ shallow.c: struct commit_list *get_shallow_commits_by_rev_list(struct strvec *ar\n       \t\tif (repo_parse_commit(the_repository, c))\n       \t\t\tdie(\"unable to parse commit %s\",\n       \t\t\t    oid_to_hex(&c->object.oid));\n     ++\t\tif (!c->parents)\n     ++\t\t\tcontinue;\n       \n       \t\tfor (parent = c->parents; parent; parent = parent->next)\n      -\t\t\tif (!(parent->item->object.flags & not_shallow_flag)) {\n     @@ shallow.c: struct commit_list *get_shallow_commits_by_rev_list(struct strvec *ar\n      -\t\t\t\tbreak;\n      +\t\t\t\tparent->item->object.flags |= shallow_flag;\n       \t\t\t}\n     ++\n      +\t\tif (must_not_be_shallow) {\n      +\t\t\tc->object.flags &= ~shallow_flag;\n      +\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n     -+\t\t\t\tif (parent->item->object.flags & shallow_flag) {\n     -+\t\t\t\t\tparent->item->object.flags |= not_shallow_flag;\n     ++\t\t\t\tif ((parent->item->object.flags & shallow_flag) &&\n     ++\t\t\t\t    !(parent->item->object.flags & not_shallow_flag))\n      +\t\t\t\t\tcommit_list_insert(parent->item, &result);\n     -+\t\t\t\t}\n      +\t\t} else {\n      +\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n      +\t\t\t\tparent->item->object.flags &= ~shallow_flag;\n     @@ shallow.c: struct commit_list *get_shallow_commits_by_rev_list(struct strvec *ar\n       \t}\n       \tfree_commit_list(not_shallow_list);\n       \n     + \t/*\n     +-\t * Now we can clean up NOT_SHALLOW on border commits. Having\n     ++\t * Now we can clean up NOT_SHALLOW on shallow commits. Having\n     + \t * both flags set can confuse the caller.\n     + \t */\n     + \tfor (p = result; p; p = p->next) {\n      \n       ## t/t5500-fetch-pack.sh ##\n      @@ t/t5500-fetch-pack.sh: test_expect_success 'shallow since with commit graph and already-seen commit' '\n       \t)\n       '\n       \n     -+test_expect_success 'clone shallow since all borders reachable' '\n     -+\ttest_create_repo shallow-since-all-borders-reachable &&\n     ++test_expect_success 'clone shallow-since all shallows reachable' '\n     ++\ttest_create_repo shallow-since-all-shallows-reachable &&\n      +\t(\n     -+\trm -rf shallow123 &&\n     -+\tcd shallow-since-all-borders-reachable &&\n     -+\tGIT_COMMITTER_DATE=\"2025-08-19 12:34:56\" git commit --allow-empty -m one &&\n     -+\tGIT_COMMITTER_DATE=\"2025-08-20 12:34:56\" git switch -c branch &&\n     -+\tGIT_COMMITTER_DATE=\"2025-08-21 12:34:56\" git commit --allow-empty -m two &&\n     -+\tGIT_COMMITTER_DATE=\"2025-08-22 12:34:56\" git commit --allow-empty -m three &&\n     -+\tGIT_COMMITTER_DATE=\"2025-08-23 12:34:56\" git switch main &&\n     -+\tGIT_COMMITTER_DATE=\"2025-08-24 12:34:56\" git merge branch --no-ff &&\n     -+\tGIT_COMMITTER_DATE=\"2025-08-26 12:34:56\" git clone --shallow-since \"2025-08-21 12:34:56\" \"file://$(pwd)/.\" ../shallow123 &&\n     -+\tcd ../shallow123 &&\n     -+\techo \"Shallow borders:\" &&\n     -+\tcat .git/shallow &&\n     -+\t$(for commit in $(cat .git/shallow); do git rev-list $commit 1>/dev/null || exit 1; done)\n     ++\t\trm -rf shallow123 &&\n     ++\t\tcd shallow-since-all-shallows-reachable &&\n     ++\t\tGIT_COMMITTER_DATE=\"2005-04-01 13:14:15\" git commit --allow-empty -m Apr_1st &&\n     ++\t\tGIT_COMMITTER_DATE=\"2005-04-02 13:14:15\" git commit --allow-empty -m Apr_2nd &&\n     ++\t\tGIT_COMMITTER_DATE=\"2005-04-03 13:14:15\" git switch -c branch &&\n     ++\t\tGIT_COMMITTER_DATE=\"2005-04-03 13:14:15\" git commit --allow-empty -m Apr_3rd &&\n     ++\t\tGIT_COMMITTER_DATE=\"2005-04-04 13:14:15\" git switch main &&\n     ++\t\tGIT_COMMITTER_DATE=\"2005-04-04 13:14:15\" git merge branch --no-ff -m Apr_4th &&\n     ++\t\tgit clone --shallow-since \"2005-04-03 13:14:15\" \"file://$(pwd)/.\" ../shallow123 &&\n     ++\t\tcd ../shallow123 &&\n     ++\t\tfor commit in $(cat .git/shallow 2> /dev/null)\n     ++\t\tdo\n     ++\t\t\tgit rev-list $commit 1>/dev/null || exit 1\n     ++\t\tdone\n      +\t)\n      +'\n      +\n\n\n shallow.c             | 67 ++++++++++++++++++++++++++++++++++---------\n t/t5500-fetch-pack.sh | 20 +++++++++++++\n 2 files changed, 73 insertions(+), 14 deletions(-)\n\ndiff --git a/shallow.c b/shallow.c\nindex 55b9cd9d3f..cd99e5777f 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -208,10 +208,9 @@ static void show_commit(struct commit *commit, void *data)\n }\n \n /*\n- * Given rev-list arguments, run rev-list. All reachable commits\n- * except border ones are marked with not_shallow_flag. Border commits\n- * are marked with shallow_flag. The list of border/shallow commits\n- * are also returned.\n+ * Given rev-list arguments, run rev-list. All reachable commits except\n+ * shallow boundary commits are marked with not_shallow_flag.\n+ * Returned is a list of boundary commits marked with shallow_flag only.\n  */\n struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n \t\t\t\t\t\t    int shallow_flag,\n@@ -241,36 +240,76 @@ struct commit_list *get_shallow_commits_by_rev_list(struct strvec *argv,\n \tif (!not_shallow_list)\n \t\tdie(\"no commits selected for shallow requests\");\n \n-\t/* Mark all reachable commits as NOT_SHALLOW */\n+\t/* Mark all reachable (listed) commits as NOT_SHALLOW */\n \tfor (p = not_shallow_list; p; p = p->next)\n \t\tp->item->object.flags |= not_shallow_flag;\n \n \t/*\n-\t * mark border commits SHALLOW + NOT_SHALLOW.\n-\t * We cannot clear NOT_SHALLOW right now. Imagine border\n-\t * commit A is processed first, then commit B, whose parent is\n-\t * A, later. If NOT_SHALLOW on A is cleared at step 1, B\n-\t * itself is considered border at step 2, which is incorrect.\n+\t * Mark shallow commits from the list as SHALLOW + NOT_SHALLOW.\n+\t * Do not clear NOT_SHALLOW flags immediately. Consider two listed\n+\t * commits, B and its parent A, where A is shallow. If A is processed\n+\t * first and its NOT_SHALLOW flag is cleared immediately, B would later\n+\t * be incorrectly marked SHALLOW when processed.\n+\t *\n+\t * Also, listed commits may have multiple parents, and not all parents\n+\t * are necessarily listed (as they were not all traversed into the\n+\t * not_shallow_list from the revs in the first place — not marked\n+\t * NOT_SHALLOW). Therefore:\n+\t *\n+\t * - A listed commit is marked SHALLOW only if none of its parents are\n+\t *   listed.\n+\t * - If at least one parent of a listed commit is also listed, the\n+\t *   commit itself is not marked SHALLOW; however, any of its non-listed\n+\t *   parents are marked SHALLOW.\n+\t *\n+\t * Processing overview:\n+\t * 1. All listed commits have already been marked NOT_SHALLOW are not\n+\t *    cleared until all shallow commits have been identified.\n+\t *\n+\t * 2. For each listed commit:\n+\t *    - Mark the commit SHALLOW if it has any parent that is not listed.\n+\t *    - Mark all non-listed parents as SHALLOW.\n+\t *    - If the commit has at least one listed parent, it is excluded\n+\t *      from the shallow result; however its parents marked only SHALLOW\n+\t *      are added instead.\n+\t *    - If all parents are marked only SHALLOW, the commit remains SHALLOW\n+\t *      and is added to the shallow result.\n \t */\n \tfor (p = not_shallow_list; p; p = p->next) {\n \t\tstruct commit *c = p->item;\n \t\tstruct commit_list *parent;\n+\t\tint must_not_be_shallow = 0;\n \n \t\tif (repo_parse_commit(the_repository, c))\n \t\t\tdie(\"unable to parse commit %s\",\n \t\t\t    oid_to_hex(&c->object.oid));\n+\t\tif (!c->parents)\n+\t\t\tcontinue;\n \n \t\tfor (parent = c->parents; parent; parent = parent->next)\n-\t\t\tif (!(parent->item->object.flags & not_shallow_flag)) {\n+\t\t\tif (parent->item->object.flags & not_shallow_flag) {\n+\t\t\t\tmust_not_be_shallow = 1;\n+\t\t\t} else {\n \t\t\t\tc->object.flags |= shallow_flag;\n-\t\t\t\tcommit_list_insert(c, &result);\n-\t\t\t\tbreak;\n+\t\t\t\tparent->item->object.flags |= shallow_flag;\n \t\t\t}\n+\n+\t\tif (must_not_be_shallow) {\n+\t\t\tc->object.flags &= ~shallow_flag;\n+\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n+\t\t\t\tif ((parent->item->object.flags & shallow_flag) &&\n+\t\t\t\t    !(parent->item->object.flags & not_shallow_flag))\n+\t\t\t\t\tcommit_list_insert(parent->item, &result);\n+\t\t} else {\n+\t\t\tfor (parent = c->parents; parent; parent = parent->next)\n+\t\t\t\tparent->item->object.flags &= ~shallow_flag;\n+\t\t\tcommit_list_insert(c, &result);\n+\t\t}\n \t}\n \tfree_commit_list(not_shallow_list);\n \n \t/*\n-\t * Now we can clean up NOT_SHALLOW on border commits. Having\n+\t * Now we can clean up NOT_SHALLOW on shallow commits. Having\n \t * both flags set can confuse the caller.\n \t */\n \tfor (p = result; p; p = p->next) {\ndiff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh\nindex 2677cd5faa..44e274281a 100755\n--- a/t/t5500-fetch-pack.sh\n+++ b/t/t5500-fetch-pack.sh\n@@ -904,6 +904,26 @@ test_expect_success 'shallow since with commit graph and already-seen commit' '\n \t)\n '\n \n+test_expect_success 'clone shallow-since all shallows reachable' '\n+\ttest_create_repo shallow-since-all-shallows-reachable &&\n+\t(\n+\t\trm -rf shallow123 &&\n+\t\tcd shallow-since-all-shallows-reachable &&\n+\t\tGIT_COMMITTER_DATE=\"2005-04-01 13:14:15\" git commit --allow-empty -m Apr_1st &&\n+\t\tGIT_COMMITTER_DATE=\"2005-04-02 13:14:15\" git commit --allow-empty -m Apr_2nd &&\n+\t\tGIT_COMMITTER_DATE=\"2005-04-03 13:14:15\" git switch -c branch &&\n+\t\tGIT_COMMITTER_DATE=\"2005-04-03 13:14:15\" git commit --allow-empty -m Apr_3rd &&\n+\t\tGIT_COMMITTER_DATE=\"2005-04-04 13:14:15\" git switch main &&\n+\t\tGIT_COMMITTER_DATE=\"2005-04-04 13:14:15\" git merge branch --no-ff -m Apr_4th &&\n+\t\tgit clone --shallow-since \"2005-04-03 13:14:15\" \"file://$(pwd)/.\" ../shallow123 &&\n+\t\tcd ../shallow123 &&\n+\t\tfor commit in $(cat .git/shallow 2> /dev/null)\n+\t\tdo\n+\t\t\tgit rev-list $commit 1>/dev/null || exit 1\n+\t\tdone\n+\t)\n+'\n+\n test_expect_success 'shallow clone exclude tag two' '\n \ttest_create_repo shallow-exclude &&\n \t(\n\nbase-commit: debbc87557487aa9a8ed8a35367d17f8b4081c76\n-- \ngitgitgadget\n"},{"id":"535403","messageId":"a60fc6aed8ab7345219118f933ac0eb61140334f.camel@t-2.net","threadId":"64521","inReplyTo":"3253600a3c96144744d3371a7ec2a66cb87d4b60.camel@t-2.net","subject":"Re: [PATCH v2] shallow: set borders which are all reachable after clone shallow since","fromName":"Samo Pogačnik","fromEmail":"samo_pogacnik@t-2.net","sentAt":"2026-02-07T05:06:01Z","receivedAt":"2026-02-07T05:15:40Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"On Wed, 2026-01-28 at 05:23 +0100, Samo Pogačnik wrote:\n> On Tue, 2026-01-20 at 12:59 -0800, Junio C Hamano wrote:\n> > Junio C Hamano <gitster@pobox.com> writes:\n> > \n> > > > ...\n> > > > The modified implementation of a generic shallow boundary finder\n> > > > based on rev-list ensures that all shallow border commits are reachable\n> > > > also after being grafted. This is achieved by inspecting all parents\n> > > > of each initial border commit candidate. The border commit candidate\n> > > > is set border only when all its parents wern't on the initial list of\n> > > > candidates. Otherwise the border commit candidate is not set as border\n> > > > however its parents that weren't on the list of candidates are set as\n> > > > borders.\n> > > \n> > > It is a minor point, but there are \"boundary\" and \"border\" used more\n> > > or less interchangeably in the proposed commit log message, and\n> > > would make the readers wonder if there are differences (I do not\n> > > think we use the word \"border\" anywhere in our documentation).  It\n> > > is minor as we do not have such mixture in the end-user facing part\n> > > of the documentation with this patch.\n> > > \n> > > I'll let those (cc'ed) who may be more familiar with, or, at least\n> > > have more code than I have in, the shallow infrastructure to comment\n> > > on the way the updated code uses the revision machinery.\n> > \n> > After this exchange, the topic has been dormant for almost full two\n> > months.  As I do not deal with shallow clones myself, even though I\n> > understand that some folks rely on it working, I'd really prefer to\n> > see somebody who are familiar with the underlying logic to review\n> > this patch if we were to move forward with it.\n> > \n> \n> I’m currently rewriting the patch and the commit message trying to\n> address the boundary/border dilemma. I hope to be able to send a new\n> version by the end of this week.\n> \n\nI posted a new version of patch '[PATCH v3] shallow: ensure all boundary commits\nare reachable with --shallow-since' on 31st of January. I hope you've seen it.\n\nthanks, Samo\n"},{"id":"535404","messageId":"xmqqy0l5b06m.fsf@gitster.g","threadId":"64521","inReplyTo":"a60fc6aed8ab7345219118f933ac0eb61140334f.camel@t-2.net","subject":"Re: [PATCH v2] shallow: set borders which are all reachable after clone shallow since","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-07T05:19:13Z","receivedAt":"2026-02-07T05:19:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Samo Pogačnik <samo_pogacnik@t-2.net> writes:\n\n> On Wed, 2026-01-28 at 05:23 +0100, Samo Pogačnik wrote:\n>> On Tue, 2026-01-20 at 12:59 -0800, Junio C Hamano wrote:\n>> ...\n>> > After this exchange, the topic has been dormant for almost full two\n>> > months.  As I do not deal with shallow clones myself, even though I\n>> > understand that some folks rely on it working, I'd really prefer to\n>> > see somebody who are familiar with the underlying logic to review\n>> > this patch if we were to move forward with it.\n>> > \n>> \n>> I’m currently rewriting the patch and the commit message trying to\n>> address the boundary/border dilemma. I hope to be able to send a new\n>> version by the end of this week.\n>\n> I posted a new version of patch '[PATCH v3] shallow: ensure all boundary commits\n> are reachable with --shallow-since' on 31st of January. I hope you've seen it.\n>\n> thanks, Samo\n\nFor those of you on the original CC: list taken from v2 review\nthread, who may be more qualified to review this topic than I am,\nthe v3 is found at:\n\n  https://lore.kernel.org/git/pull.2107.v3.git.git.1769876930544.gitgitgadget@gmail.com/\n\nThanks.\n"},{"id":"538170","messageId":"418dfce27b2812a696dee791e81a0049400e50f4.camel@t-2.net","threadId":"64521","inReplyTo":"xmqqy0l5b06m.fsf@gitster.g","subject":"Re: [PATCH v2] shallow: set borders which are all reachable after clone shallow since","fromName":"Samo Pogačnik","fromEmail":"samo_pogacnik@t-2.net","sentAt":"2026-03-07T07:13:40Z","receivedAt":"2026-03-07T07:21:01Z","isPatch":true,"sender":{"key":"samo_pogacnik@t-2.net","avatar":"https://avatars.githubusercontent.com/u/7649004?v=4"},"body":"On Fri, 2026-02-06 at 21:19 -0800, Junio C Hamano wrote:\n> Samo Pogačnik <samo_pogacnik@t-2.net> writes:\n> \n> > On Wed, 2026-01-28 at 05:23 +0100, Samo Pogačnik wrote:\n> > > On Tue, 2026-01-20 at 12:59 -0800, Junio C Hamano wrote:\n> > > ...\n> > > > After this exchange, the topic has been dormant for almost full two\n> > > > months.  As I do not deal with shallow clones myself, even though I\n> > > > understand that some folks rely on it working, I'd really prefer to\n> > > > see somebody who are familiar with the underlying logic to review\n> > > > this patch if we were to move forward with it.\n> > > > \n> > > \n> > > I’m currently rewriting the patch and the commit message trying to\n> > > address the boundary/border dilemma. I hope to be able to send a new\n> > > version by the end of this week.\n> > \n> > I posted a new version of patch '[PATCH v3] shallow: ensure all boundary\n> > commits\n> > are reachable with --shallow-since' on 31st of January. I hope you've seen\n> > it.\n> > \n> > thanks, Samo\n> \n> For those of you on the original CC: list taken from v2 review\n> thread, who may be more qualified to review this topic than I am,\n> the v3 is found at:\n> \n>  \n> https://lore.kernel.org/git/pull.2107.v3.git.git.1769876930544.gitgitgadget@gmail.com/\n> \n\nCould you please check this correction.\n\nThanks, Samo\n\n"}]}