{"thread":{"id":"66356","subject":"[PATCH] object-name: explain why <ref>~N fails in a shallow clone","startedAt":"2026-09-20T09:53:36Z","lastAt":"2026-09-23T18:56:05Z","messageCount":8,"participants":["Harald Nordgren via GitGitGadget","D. Ben Knoble","Junio C Hamano","Harald Nordgren"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"552905","messageId":"pull.2413.git.git.1789898013916.gitgitgadget@gmail.com","threadId":"66356","inReplyTo":null,"subject":"[PATCH] object-name: explain why <ref>~N fails in a shallow clone","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-20T09:53:33Z","receivedAt":"2026-09-20T09:53:36Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAsking for a commit's ancestor with <ref>~N or <ref>^N in a shallow\nclone that does not have N commits of history locally fails with a\nbare \"is not a commit\" error, with no indication that the repository\nbeing shallow is the reason, or what to do about it.\n\nAdd a hint, shown when the walk runs out of parents exactly at a\nrecorded shallow boundary, explaining that history was intentionally\ntruncated there. When <ref> looks like <remote>/<branch> and <remote>\nis configured, the suggested command names that remote and branch\ndirectly. For <ref>~N it suggests the exact --deepen needed,\naccounting for any history already present instead of just N. For\n<ref>^N the suggestion is always --deepen=1, regardless of N: a\nshallow boundary commit has no parents recorded locally at all, so\ndeepening by one generation fetches its complete real parent list in\none step, whether that commit turns out to have one parent or several.\nThe hint only fires when the search stops at an actual shallow\nboundary, not merely because the repository happens to be shallow\nelsewhere, so it does not misfire on a short history that is not\nshallow-truncated.\n\nThe advice is threaded through GET_OID_QUIETLY so it is not shown\nduring the internal re-resolution some commands do while building a\nbetter error message, which would otherwise print it twice for the\nsame failing argument.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    object-name: explain why ~N fails in a shallow clone\n    \n    Asking for a commit's ancestor with <ref>~N in a shallow clone that\n    doesn't have N commits of history locally fails with a \"is not a commit\"\n    error, with no indication that the repository being shallow is the\n    reason.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2413%2FHaraldNordgren%2Fshallow-history-advice-hint-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2413/HaraldNordgren/shallow-history-advice-hint-v1\nPull-Request: https://github.com/git/git/pull/2413\n\n Documentation/config/advice.adoc |   4 ++\n advice.c                         |   1 +\n advice.h                         |   1 +\n object-name.c                    |  75 +++++++++++++++++++--\n t/t1500-rev-parse.sh             | 108 +++++++++++++++++++++++++++++++\n 5 files changed, 184 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc\nindex 81f80a9274..5b44037fff 100644\n--- a/Documentation/config/advice.adoc\n+++ b/Documentation/config/advice.adoc\n@@ -128,6 +128,10 @@ all advice messages.\n \t\tgive directions on how to proceed from the current state.\n \tsequencerInUse::\n \t\tShown when a sequencer command is already in progress.\n+\tshallowHistory::\n+\t\tShown when `~<n>` or `^<n>` cannot resolve enough ancestors\n+\t\tbecause history stops at a shallow boundary, to suggest\n+\t\tfetching more history.\n \tskippedCherryPicks::\n \t\tShown when linkgit:git-rebase[1] skips a commit that has already\n \t\tbeen cherry-picked onto the upstream branch.\ndiff --git a/advice.c b/advice.c\nindex 63bf8b0c5f..e22706ead5 100644\n--- a/advice.c\n+++ b/advice.c\n@@ -80,6 +80,7 @@ static struct {\n \t[ADVICE_RM_HINTS]\t\t\t\t= { \"rmHints\" },\n \t[ADVICE_SEQUENCER_IN_USE]\t\t\t= { \"sequencerInUse\" },\n \t[ADVICE_SET_UPSTREAM_FAILURE]\t\t\t= { \"setUpstreamFailure\" },\n+\t[ADVICE_SHALLOW_HISTORY]\t\t\t= { \"shallowHistory\" },\n \t[ADVICE_SKIPPED_CHERRY_PICKS]\t\t\t= { \"skippedCherryPicks\" },\n \t[ADVICE_SPARSE_INDEX_EXPANDED]\t\t\t= { \"sparseIndexExpanded\" },\n \t[ADVICE_STATUS_AHEAD_BEHIND_WARNING]\t\t= { \"statusAheadBehindWarning\" },\ndiff --git a/advice.h b/advice.h\nindex 66f6cd6a77..e80e3e82cc 100644\n--- a/advice.h\n+++ b/advice.h\n@@ -47,6 +47,7 @@ enum advice_type {\n \tADVICE_RM_HINTS,\n \tADVICE_SEQUENCER_IN_USE,\n \tADVICE_SET_UPSTREAM_FAILURE,\n+\tADVICE_SHALLOW_HISTORY,\n \tADVICE_SKIPPED_CHERRY_PICKS,\n \tADVICE_SPARSE_INDEX_EXPANDED,\n \tADVICE_STATUS_AHEAD_BEHIND_WARNING,\ndiff --git a/object-name.c b/object-name.c\nindex 4eda8c8eac..0a18d1d2e3 100644\n--- a/object-name.c\n+++ b/object-name.c\n@@ -22,6 +22,7 @@\n #include \"repo-settings.h\"\n #include \"repository.h\"\n #include \"setup.h\"\n+#include \"shallow.h\"\n #include \"midx.h\"\n #include \"commit-reach.h\"\n #include \"date.h\"\n@@ -824,9 +825,65 @@ static int get_oid_basic(struct repository *r, const char *str, int len,\n \treturn 0;\n }\n \n+/*\n+ * When a \"name~<n>\" or \"name^<n>\" walk runs out of parents at \"commit\",\n+ * and that is because \"commit\" is where this shallow repository's history\n+ * was cut off (rather than commit genuinely being a root commit), let the\n+ * user know that fetching more history might be what they are after.\n+ *\n+ * \"suggested_depth\" is the --deepen value to recommend. For \"name^<n>\"\n+ * this is always 1: deepening by one generation fetches \"commit\"'s real\n+ * parent list in full, whatever it turns out to contain, regardless of\n+ * which parent index <n> asked for. If \"name\" looks like\n+ * \"<remote>/<branch>\" and <remote> is a configured remote, the suggested\n+ * command names that remote and branch instead of leaving them as\n+ * placeholders.\n+ */\n+static void advise_if_shallow_cutoff(struct repository *r,\n+\t\t\t\t     const char *name, int namelen,\n+\t\t\t\t     struct commit *commit,\n+\t\t\t\t     unsigned lookup_flags,\n+\t\t\t\t     int suggested_depth)\n+{\n+\tstruct commit_graft *graft;\n+\tconst char *slash;\n+\tstruct strbuf cmd = STRBUF_INIT;\n+\n+\tif (lookup_flags & GET_OID_QUIETLY)\n+\t\treturn;\n+\tif (!is_repository_shallow(r))\n+\t\treturn;\n+\tgraft = lookup_commit_graft(r, &commit->object.oid);\n+\tif (!graft || graft->nr_parent != -1)\n+\t\treturn;\n+\n+\tslash = memchr(name, '/', namelen);\n+\tif (slash) {\n+\t\tchar *remote_candidate = xstrndup(name, slash - name);\n+\t\tif (remote_is_configured(remote_get(remote_candidate), 0))\n+\t\t\tstrbuf_addf(&cmd, \"git fetch --deepen=%d %s %.*s\",\n+\t\t\t\t    suggested_depth, remote_candidate,\n+\t\t\t\t    (int)(name + namelen - (slash + 1)), slash + 1);\n+\t\tfree(remote_candidate);\n+\t}\n+\tif (!cmd.len)\n+\t\tstrbuf_addf(&cmd, \"git fetch --deepen=%d <remote> <branch>\",\n+\t\t\t    suggested_depth);\n+\n+\tadvise_if_enabled(ADVICE_SHALLOW_HISTORY,\n+\t\t\t   _(\"'%.*s' does not have that many ancestors locally.\\n\"\n+\t\t\t     \"History stops at %s because this repository is a\\n\"\n+\t\t\t     \"shallow clone. To fetch more of it, try:\\n\"\n+\t\t\t     \"\\n\"\n+\t\t\t     \"  %s\"),\n+\t\t\t   namelen, name, oid_to_hex(&commit->object.oid), cmd.buf);\n+\tstrbuf_release(&cmd);\n+}\n+\n static enum get_oid_result get_parent(struct repository *r,\n \t\t\t\t      const char *name, int len,\n-\t\t\t\t      struct object_id *result, int idx)\n+\t\t\t\t      struct object_id *result, int idx,\n+\t\t\t\t      unsigned lookup_flags)\n {\n \tstruct object_id oid;\n \tenum get_oid_result ret = get_oid_1(r, name, len, &oid,\n@@ -851,13 +908,15 @@ static enum get_oid_result get_parent(struct repository *r,\n \t\t}\n \t\tp = p->next;\n \t}\n+\tadvise_if_shallow_cutoff(r, name, len, commit, lookup_flags, 1);\n \treturn MISSING_OBJECT;\n }\n \n static enum get_oid_result get_nth_ancestor(struct repository *r,\n \t\t\t\t\t    const char *name, int len,\n \t\t\t\t\t    struct object_id *result,\n-\t\t\t\t\t    int generation)\n+\t\t\t\t\t    int generation,\n+\t\t\t\t\t    unsigned lookup_flags)\n {\n \tstruct object_id oid;\n \tstruct commit *commit;\n@@ -871,8 +930,14 @@ static enum get_oid_result get_nth_ancestor(struct repository *r,\n \t\treturn MISSING_OBJECT;\n \n \twhile (generation--) {\n-\t\tif (repo_parse_commit(r, commit) || !commit->parents)\n+\t\tif (repo_parse_commit(r, commit))\n \t\t\treturn MISSING_OBJECT;\n+\t\tif (!commit->parents) {\n+\t\t\t/* Remaining \"generation\" plus this failed step is the actual gap. */\n+\t\t\tadvise_if_shallow_cutoff(r, name, len, commit,\n+\t\t\t\t\t\t lookup_flags, generation + 1);\n+\t\t\treturn MISSING_OBJECT;\n+\t\t}\n \t\tcommit = commit->parents->item;\n \t}\n \toidcpy(result, &commit->object.oid);\n@@ -1119,9 +1184,9 @@ static enum get_oid_result get_oid_1(struct repository *r,\n \t\telse if (num > INT_MAX)\n \t\t\treturn MISSING_OBJECT;\n \t\tif (has_suffix == '^')\n-\t\t\treturn get_parent(r, name, len1, oid, num);\n+\t\t\treturn get_parent(r, name, len1, oid, num, lookup_flags);\n \t\t/* else if (has_suffix == '~') -- goes without saying */\n-\t\treturn get_nth_ancestor(r, name, len1, oid, num);\n+\t\treturn get_nth_ancestor(r, name, len1, oid, num, lookup_flags);\n \t}\n \n \tret = peel_onion(r, name, len, oid, lookup_flags);\ndiff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh\nindex 4174ca40c3..4b46ba546d 100755\n--- a/t/t1500-rev-parse.sh\n+++ b/t/t1500-rev-parse.sh\n@@ -193,6 +193,114 @@ test_expect_success 'rev-parse --is-shallow-repository in non-shallow repo' '\n \ttest_cmp expect actual\n '\n \n+check_shallow_history_advice () {\n+\tname=$1 oid=$2 cmd=$3 &&\n+\tgrep '^hint:' err >actual &&\n+\tcat >expect <<-EOF &&\n+\thint: '$name' does not have that many ancestors locally.\n+\thint: History stops at $oid because this repository is a\n+\thint: shallow clone. To fetch more of it, try:\n+\thint:\n+\thint:   $cmd\n+\thint: Disable this message with \"git config set advice.shallowHistory false\"\n+\tEOF\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'shallowHistory advice on ~N beyond shallow boundary' '\n+\ttest_commit shallow_advice_1 &&\n+\ttest_commit shallow_advice_2 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice &&\n+\ttest_when_finished \"rm -rf shallow-advice\" &&\n+\toid=$(git -C shallow-advice rev-parse origin/main) &&\n+\ttest_must_fail git -C shallow-advice rev-parse origin/main~1 2>err &&\n+\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\"git fetch --deepen=1 origin main\"\n+'\n+\n+test_expect_success 'shallowHistory advice accounts for depth already present' '\n+\ttest_commit shallow_partial_1 &&\n+\ttest_commit shallow_partial_2 &&\n+\ttest_commit shallow_partial_3 &&\n+\ttest_commit shallow_partial_4 &&\n+\ttest_commit shallow_partial_5 &&\n+\ttest_commit shallow_partial_6 &&\n+\tgit clone --no-local --depth=3 --branch main --single-branch \\\n+\t\t.git shallow-advice-partial &&\n+\ttest_when_finished \"rm -rf shallow-advice-partial\" &&\n+\t(\n+\t\tcd shallow-advice-partial &&\n+\t\toid=$(git rev-parse origin/main~2) &&\n+\t\ttest_must_fail git rev-parse origin/main~5 2>err &&\n+\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\t\"git fetch --deepen=3 origin main\" &&\n+\t\tgit fetch --deepen=3 origin &&\n+\t\tgit rev-parse origin/main~5 &&\n+\t\ttest_must_fail git rev-parse origin/main~6\n+\t)\n+'\n+\n+test_expect_success 'shallowHistory advice on ^N (first parent) beyond shallow boundary' '\n+\ttest_commit shallow_caret_1 &&\n+\ttest_commit shallow_caret_2 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-caret &&\n+\ttest_when_finished \"rm -rf shallow-advice-caret\" &&\n+\toid=$(git -C shallow-advice-caret rev-parse origin/main) &&\n+\ttest_must_fail git -C shallow-advice-caret rev-parse origin/main^1 2>err &&\n+\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\"git fetch --deepen=1 origin main\"\n+'\n+\n+test_expect_success 'shallowHistory advice on ^N suggests deepen=1 even for a merge parent' '\n+\ttest_commit shallow_merge_base &&\n+\tgit checkout -q -b shallow-merge-side &&\n+\ttest_commit shallow_merge_side1 &&\n+\tgit checkout -q main &&\n+\ttest_commit shallow_merge_main1 &&\n+\tgit merge -q --no-ff shallow-merge-side -m \"shallow merge commit\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-merge &&\n+\ttest_when_finished \"rm -rf shallow-advice-merge\" &&\n+\t(\n+\t\tcd shallow-advice-merge &&\n+\t\toid=$(git rev-parse origin/main) &&\n+\t\ttest_must_fail git rev-parse origin/main^2 2>err &&\n+\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\t\"git fetch --deepen=1 origin main\" &&\n+\t\tgit fetch -q --deepen=1 origin &&\n+\t\tgit rev-parse origin/main^1 &&\n+\t\tgit rev-parse origin/main^2\n+\t)\n+'\n+\n+test_expect_success 'shallowHistory advice can be disabled' '\n+\ttest_commit shallow_off_1 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-off &&\n+\ttest_when_finished \"rm -rf shallow-advice-off\" &&\n+\ttest_must_fail git -C shallow-advice-off \\\n+\t\t-c advice.shallowHistory=false rev-parse origin/main~1 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n+test_expect_success 'shallowHistory advice not shown for a non-shallow repository' '\n+\ttest_must_fail git rev-parse HEAD~100000 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n+test_expect_success 'shallowHistory advice not shown when resolution succeeds' '\n+\ttest_commit shallow_ok_1 &&\n+\ttest_commit shallow_ok_2 &&\n+\ttest_commit shallow_ok_3 &&\n+\tgit clone --no-local --depth=3 --branch main --single-branch \\\n+\t\t.git shallow-advice-ok &&\n+\ttest_when_finished \"rm -rf shallow-advice-ok\" &&\n+\tgit -C shallow-advice-ok rev-parse origin/main~1 >actual 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n test_expect_success 'rev-parse --show-object-format in repo' '\n \ttest_oid algo >expect &&\n \tgit rev-parse --show-object-format >actual &&\n\nbase-commit: d38352cd43ab9745686d697872408bc3249a153f\n-- \ngitgitgadget\n"},{"id":"552916","messageId":"CALnO6CB6DSwPKffy2BBgBk3fhJZXrMaoZm-ScBwgvoGQn_cwMQ@mail.gmail.com","threadId":"66356","inReplyTo":"pull.2413.git.git.1789898013916.gitgitgadget@gmail.com","subject":"Re: [PATCH] object-name: explain why <ref>~N fails in a shallow clone","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-09-20T21:59:59Z","receivedAt":"2026-09-20T22:00:13Z","isPatch":true,"body":"[This is not a complete review, just something I noticed]\n\nOn Sun, Sep 20, 2026 at 5:55 AM Harald Nordgren via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> Asking for a commit's ancestor with <ref>~N or <ref>^N in a shallow\n> clone that does not have N commits of history locally fails with a\n> bare \"is not a commit\" error, with no indication that the repository\n> being shallow is the reason, or what to do about it.\n>\n> Add a hint, shown when the walk runs out of parents exactly at a\n> recorded shallow boundary, explaining that history was intentionally\n> truncated there. When <ref> looks like <remote>/<branch> and <remote>\n> is configured, the suggested command names that remote and branch\n> directly.\n\nHere, we mention the most common \"ref\" cases directly, and it might\nmake sense to restrict this feature only to the use of refs (since we\nmight need such a ref to unshallow via fetch?).\n\n> For <ref>~N it suggests the exact --deepen needed,\n> accounting for any history already present instead of just N. For\n> <ref>^N the suggestion is always --deepen=1, regardless of N:\n\nBut here…\n\n> diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc\n> index 81f80a9274..5b44037fff 100644\n> --- a/Documentation/config/advice.adoc\n> +++ b/Documentation/config/advice.adoc\n> @@ -128,6 +128,10 @@ all advice messages.\n>                 give directions on how to proceed from the current state.\n>         sequencerInUse::\n>                 Shown when a sequencer command is already in progress.\n> +       shallowHistory::\n> +               Shown when `~<n>` or `^<n>` cannot resolve enough ancestors\n> +               because history stops at a shallow boundary, to suggest\n> +               fetching more history.\n>         skippedCherryPicks::\n>                 Shown when linkgit:git-rebase[1] skips a commit that has already\n>                 been cherry-picked onto the upstream branch.\n\n…and here, it's not clear to me what kinds of things trigger the hint.\nThe \"~N\" and \"^N\" syntax work with any commit-ish, after all!\n\nEither way, the hint documentation should probably not show \"bare\"\n\"~N\"/~^N\", since that's not syntax accepted by Git. The\ngitrevisions(7) manual uses \"<rev>~<n>\" and \"<rev>^[<n>]\".\n\nPerhaps you meant \"rev\" instead of \"ref\" in the commit message? Hm.\n\n-- \nD. Ben Knoble\n"},{"id":"552939","messageId":"xmqq8q4uo8fx.fsf@gitster.g","threadId":"66356","inReplyTo":"pull.2413.git.git.1789898013916.gitgitgadget@gmail.com","subject":"Re: [PATCH] object-name: explain why <ref>~N fails in a shallow clone","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-21T16:50:10Z","receivedAt":"2026-09-21T16:50:12Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> Asking for a commit's ancestor with <ref>~N or <ref>^N in a shallow\n> clone that does not have N commits of history locally fails with a\n> bare \"is not a commit\" error, with no indication that the repository\n> being shallow is the reason, or what to do about it.\n\nI am not sure if bringing up '^N' (the N-th parent of a merge) in an\nattempt to be more complete helps readers or confuses them.  Unlike\n'<rev>~N', where increasing N raises the required depth of a shallow\nclone to make the target revision available, both '<rev>^1' and\n'<rev>^43' of '<rev>' share the same depth.  If '<rev>' exists\nlocally and its first parent '<rev>^1' also does, it is likely that\n'<rev>^2' is also available, as they are at the same depth from\n'<rev>'.\n\nThe title of the commit does not share the problem, which is a good\nthing ;-).\n\n> Add a hint, shown when the walk runs out of parents exactly at a\n> recorded shallow boundary, explaining that history was intentionally\n> truncated there. When <ref> looks like <remote>/<branch> and <remote>\n> is configured, the suggested command names that remote and branch\n\nGood thinking.\n\n\nAs branch 'B' of remote 'R' is not necessarily stored locally at\n'refs/remotes/R/B', implementing the semantics correctly and showing\nthe correct remote name and their branch name by reverse mapping R/B\nback requires a bit of care, but it should not be impossibly hard.\n\n> directly. For <ref>~N it suggests the exact --deepen needed,\n> accounting for any history already present instead of just N. For\n> <ref>^N the suggestion is always --deepen=1, regardless of N: a\n> shallow boundary commit has no parents recorded locally at all, so\n> deepening by one generation fetches its complete real parent list in\n> one step, whether that commit turns out to have one parent or several.\n> The hint only fires when the search stops at an actual shallow\n> boundary, not merely because the repository happens to be shallow\n> elsewhere, so it does not misfire on a short history that is not\n> shallow-truncated.\n\nI think Ben also mentioned this, but <ref> is probably better\nwritten as <rev> in the above.  A ref(erence) like \"master\",\n\"origin/next\", or \"refs/remotes/origin/topic\" are all rev(ision)s,\nand this new advice feature is not limited to requests that are\nmade using references.\n\n    When the revision <rev> is given as a remote-tracking branch,\n    the remote and branch are exactly named in the suggested\n    command.  For <rev>~N, it suggests ...\n\n> The advice is threaded through GET_OID_QUIETLY so it is not shown\n> during the internal re-resolution some commands do while building a\n> better error message, which would otherwise print it twice for the\n> same failing argument.\n\nNice.\n\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>     object-name: explain why ~N fails in a shallow clone\n>     \n>     Asking for a commit's ancestor with <ref>~N in a shallow clone that\n>     doesn't have N commits of history locally fails with a \"is not a commit\"\n>     error, with no indication that the repository being shallow is the\n>     reason.\n\nIt may not be intuitive to new users that in a shallow clone \"git\nlog\" stops in the middle, instead of going down to the beginning of\nthe history, downloading necessary objects on demand.  But fixing it\nby adding such a feature is totally unrelated and outside the scope\nof this topic ;-).\n\n> diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc\n> index 81f80a9274..5b44037fff 100644\n> --- a/Documentation/config/advice.adoc\n> +++ b/Documentation/config/advice.adoc\n> @@ -128,6 +128,10 @@ all advice messages.\n>  \t\tgive directions on how to proceed from the current state.\n>  \tsequencerInUse::\n>  \t\tShown when a sequencer command is already in progress.\n> +\tshallowHistory::\n> +\t\tShown when `~<n>` or `^<n>` cannot resolve enough ancestors\n> +\t\tbecause history stops at a shallow boundary, to suggest\n> +\t\tfetching more history.\n\nIt is obvious that users would see such a message when they say\n\n    $ git show HEAD~20\n    $ git log HEAD~20..HEAD\n\nbut would they see the same when\n\n    $ git log -20 HEAD\n    $ git log --since=2.months HEAD\n\nand internally HEAD~20 fails to resolve?  Should they see the same\nhint?\n\n> +\ttest_must_fail git -C shallow-advice rev-parse origin/main~1 2>err &&\n> +\tcheck_shallow_history_advice origin/main \"$oid\" \\\n> +\t\t\"git fetch --deepen=1 origin main\"\n\nThis is very straight-forward.\n\n> +\t\ttest_must_fail git rev-parse origin/main~5 2>err &&\n> +\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n> +\t\t\t\"git fetch --deepen=3 origin main\" &&\n\nAgain, very straight-forward.\n\n> +\ttest_must_fail git -C shallow-advice-caret rev-parse origin/main^1 2>err &&\n> +\tcheck_shallow_history_advice origin/main \"$oid\" \\\n> +\t\t\"git fetch --deepen=1 origin main\"\n\nDitto.\n\n> +\t\ttest_must_fail git rev-parse origin/main^2 2>err &&\n> +\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n> +\t\t\t\"git fetch --deepen=1 origin main\" &&\n\nDitto.\n\n> +test_expect_success 'shallowHistory advice not shown for a non-shallow repository' '\n> +\ttest_must_fail git rev-parse HEAD~100000 2>err &&\n> +\ttest_grep ! \"^hint:\" err\n> +'\n\nOK.\n\n> +test_expect_success 'shallowHistory advice not shown when resolution succeeds' '\n> +\ttest_commit shallow_ok_1 &&\n> +\ttest_commit shallow_ok_2 &&\n> +\ttest_commit shallow_ok_3 &&\n> +\tgit clone --no-local --depth=3 --branch main --single-branch \\\n> +\t\t.git shallow-advice-ok &&\n> +\ttest_when_finished \"rm -rf shallow-advice-ok\" &&\n> +\tgit -C shallow-advice-ok rev-parse origin/main~1 >actual 2>err &&\n> +\ttest_grep ! \"^hint:\" err\n> +'\n\nOK.\n\nI guess the answer to my earlier \"does internally failing to resolve\ndue to graft point count?\" is \"no\"?\n\nThanks.\n"},{"id":"552956","messageId":"CAHwyqnX77+qsstUmvvLw5Jvfdoi4V3YVagMddYxnWodHR9oq-g@mail.gmail.com","threadId":"66356","inReplyTo":"xmqq8q4uo8fx.fsf@gitster.g","subject":"Re: [PATCH] object-name: explain why <ref>~N fails in a shallow clone","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-21T22:17:46Z","receivedAt":"2026-09-21T22:18:25Z","isPatch":true,"body":"> It is obvious that users would see such a message when they say\n>\n>     $ git show HEAD~20\n>     $ git log HEAD~20..HEAD\n>\n> but would they see the same when\n>\n>     $ git log -20 HEAD\n>     $ git log --since=2.months HEAD\n>\n> and internally HEAD~20 fails to resolve?  Should they see the same\n> hint?\n\nBut I think the 'log -20' doesn't even give an error, so where would\nwe show that message?\n\n\nHarald\n"},{"id":"552959","messageId":"xmqqqzimjjlp.fsf@gitster.g","threadId":"66356","inReplyTo":"CAHwyqnX77+qsstUmvvLw5Jvfdoi4V3YVagMddYxnWodHR9oq-g@mail.gmail.com","subject":"Re: [PATCH] object-name: explain why <ref>~N fails in a shallow clone","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-21T23:00:18Z","receivedAt":"2026-09-21T23:00:20Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> It is obvious that users would see such a message when they say\n>>\n>>     $ git show HEAD~20\n>>     $ git log HEAD~20..HEAD\n>>\n>> but would they see the same when\n>>\n>>     $ git log -20 HEAD\n>>     $ git log --since=2.months HEAD\n>>\n>> and internally HEAD~20 fails to resolve?  Should they see the same\n>> hint?\n>\n> But I think the 'log -20' doesn't even give an error, so where would\n> we show that message?\n\nWhere?  To their terminal ;-)?\n\nTo end users, \"git log -20 HEAD\" that stops only after showing 2\ncommits is just as puzzling as, if not more puzzling than, \"git show\nHEAD~20\" that complains \"What commit are you talking about?\"\n\nAt least the latter has an error message that says the commit the\nuser thought they gave (i.e., HEAD~20) was not interpreted as a\ncommit, and that might give them enough clue to remind them that\nthey are the responsible ones for the unexpected beahviour by\ninitially cloning the repository with a \"--depth\" option.  On the\nother hand, \"git log -20\" that stops way before they expect it would\ndoes not even give them such a hint.\n\nI thought that helping that case would have much more benefit, hence\nmy question.\n\n\n"},{"id":"552988","messageId":"pull.2413.v2.git.git.1790084326913.gitgitgadget@gmail.com","threadId":"66356","inReplyTo":"pull.2413.git.git.1789898013916.gitgitgadget@gmail.com","subject":"[PATCH v2] shallow: advise when a walk stops at a shallow boundary","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-22T13:38:46Z","receivedAt":"2026-09-22T13:38:51Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAsking for a commit's ancestor with <rev>~N in a shallow clone that\ndoes not have N commits of history locally fails with a bare \"is not\na commit\" error, with no indication that the repository being shallow\nis the reason, or what to do about it.\n\ngit log -<n> and git log --since=<date> have the opposite problem.\nInstead of failing, they silently stop once they reach the shallow\nboundary, with no error and nothing to suggest that more history\nexists upstream and simply was not fetched.\n\nAdd a hint, shown when a walk runs out of parents exactly at a\nrecorded shallow boundary, not merely because the repository happens\nto be shallow elsewhere. When the revision <rev> is given as a\nremote-tracking branch, the remote and branch are exactly named in the\nsuggested command, found by reverse mapping through the remote's real\nrefspec instead of assuming a refs/remotes/<remote>/ layout, since a\nrefspec can put branches anywhere. For <rev>~N it suggests the exact\n--deepen needed, accounting for history already present. For <rev>^N\nit always suggests --deepen=1: a shallow boundary commit has no\nparents recorded at all, so one more generation reveals its complete\nreal parent list regardless of which index was asked for.\n\nThe hint only suggests that older history might exist. A recorded\nshallow boundary does not distinguish a depth cutoff from a commit\nthat is the true root: cloning with a depth that happens to exactly\nmatch the available history still records that root commit as a\nboundary, and only the remote knows for certain whether deepening\nwould fetch anything new.\n\ngit log -<n> and git log --since only show the hint when the last\ncommit they printed is that same recorded boundary. A plain git log\nrunning out of history, or a git log -<n> or --since that is satisfied\nwell before reaching the boundary, stays silent as before. When the\noutput is paginated, the hint waits for the pager to close before\nprinting, since a full screen pager like less otherwise wipes it from\nthe terminal along with the rest of the output the moment it quits.\n\nThe advice is threaded through GET_OID_QUIETLY so it is not shown\nduring the internal re-resolution some commands do while building a\nbetter error message, which would otherwise print it twice for the\nsame failing argument.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    object-name: explain why ~N fails in a shallow clone\n    \n    Asking for a commit's ancestor with <rev>~N in a shallow clone that\n    doesn't have N commits of history locally fails with a \"is not a commit\"\n    error, with no indication that the repository being shallow is the\n    reason.\n    \n    Changes in v2:\n    \n     * <ref> renamed to <rev> throughout. Restrict the commit message and\n       advice.shallowHistory documentation to <rev>~N, since <rev>^N doesn't\n       get deeper with a higher index the way <rev>~N does.\n     * Bare <remote>/<branch> names are now resolved by reverse mapping\n       through the remote's configured refspec (remote_find_tracking),\n       instead of assuming branches live at refs/remotes/<remote>/<branch>.\n     * Extend the shallow history hint to git log -<n> and git log\n       --since=<date>, which previously stopped early at a shallow boundary\n       with no indication anything was missing.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2413%2FHaraldNordgren%2Fshallow-history-advice-hint-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2413/HaraldNordgren/shallow-history-advice-hint-v2\nPull-Request: https://github.com/git/git/pull/2413\n\nRange-diff vs v1:\n\n 1:  ba481667c9 ! 1:  305d22d33e object-name: explain why <ref>~N fails in a shallow clone\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    object-name: explain why <ref>~N fails in a shallow clone\n     +    shallow: advise when a walk stops at a shallow boundary\n      \n     -    Asking for a commit's ancestor with <ref>~N or <ref>^N in a shallow\n     -    clone that does not have N commits of history locally fails with a\n     -    bare \"is not a commit\" error, with no indication that the repository\n     -    being shallow is the reason, or what to do about it.\n     +    Asking for a commit's ancestor with <rev>~N in a shallow clone that\n     +    does not have N commits of history locally fails with a bare \"is not\n     +    a commit\" error, with no indication that the repository being shallow\n     +    is the reason, or what to do about it.\n      \n     -    Add a hint, shown when the walk runs out of parents exactly at a\n     -    recorded shallow boundary, explaining that history was intentionally\n     -    truncated there. When <ref> looks like <remote>/<branch> and <remote>\n     -    is configured, the suggested command names that remote and branch\n     -    directly. For <ref>~N it suggests the exact --deepen needed,\n     -    accounting for any history already present instead of just N. For\n     -    <ref>^N the suggestion is always --deepen=1, regardless of N: a\n     -    shallow boundary commit has no parents recorded locally at all, so\n     -    deepening by one generation fetches its complete real parent list in\n     -    one step, whether that commit turns out to have one parent or several.\n     -    The hint only fires when the search stops at an actual shallow\n     -    boundary, not merely because the repository happens to be shallow\n     -    elsewhere, so it does not misfire on a short history that is not\n     -    shallow-truncated.\n     +    git log -<n> and git log --since=<date> have the opposite problem.\n     +    Instead of failing, they silently stop once they reach the shallow\n     +    boundary, with no error and nothing to suggest that more history\n     +    exists upstream and simply was not fetched.\n     +\n     +    Add a hint, shown when a walk runs out of parents exactly at a\n     +    recorded shallow boundary, not merely because the repository happens\n     +    to be shallow elsewhere. When the revision <rev> is given as a\n     +    remote-tracking branch, the remote and branch are exactly named in the\n     +    suggested command, found by reverse mapping through the remote's real\n     +    refspec instead of assuming a refs/remotes/<remote>/ layout, since a\n     +    refspec can put branches anywhere. For <rev>~N it suggests the exact\n     +    --deepen needed, accounting for history already present. For <rev>^N\n     +    it always suggests --deepen=1: a shallow boundary commit has no\n     +    parents recorded at all, so one more generation reveals its complete\n     +    real parent list regardless of which index was asked for.\n     +\n     +    The hint only suggests that older history might exist. A recorded\n     +    shallow boundary does not distinguish a depth cutoff from a commit\n     +    that is the true root: cloning with a depth that happens to exactly\n     +    match the available history still records that root commit as a\n     +    boundary, and only the remote knows for certain whether deepening\n     +    would fetch anything new.\n     +\n     +    git log -<n> and git log --since only show the hint when the last\n     +    commit they printed is that same recorded boundary. A plain git log\n     +    running out of history, or a git log -<n> or --since that is satisfied\n     +    well before reaching the boundary, stays silent as before. When the\n     +    output is paginated, the hint waits for the pager to close before\n     +    printing, since a full screen pager like less otherwise wipes it from\n     +    the terminal along with the rest of the output the moment it quits.\n      \n          The advice is threaded through GET_OID_QUIETLY so it is not shown\n          during the internal re-resolution some commands do while building a\n     @@ Documentation/config/advice.adoc: all advice messages.\n       \tsequencerInUse::\n       \t\tShown when a sequencer command is already in progress.\n      +\tshallowHistory::\n     -+\t\tShown when `~<n>` or `^<n>` cannot resolve enough ancestors\n     -+\t\tbecause history stops at a shallow boundary, to suggest\n     -+\t\tfetching more history.\n     ++\t\tShown when `<rev>~<n>` cannot resolve enough ancestors of\n     ++\t\t`<rev>` because history stops at a shallow boundary, to\n     ++\t\tsuggest fetching more history.\n       \tskippedCherryPicks::\n       \t\tShown when linkgit:git-rebase[1] skips a commit that has already\n       \t\tbeen cherry-picked onto the upstream branch.\n     @@ advice.h: enum advice_type {\n       \tADVICE_SPARSE_INDEX_EXPANDED,\n       \tADVICE_STATUS_AHEAD_BEHIND_WARNING,\n      \n     + ## builtin/log.c ##\n     +@@\n     + #include \"commit-reach.h\"\n     + #include \"promisor-remote.h\"\n     + #include \"range-diff.h\"\n     ++#include \"shallow.h\"\n     + #include \"tmp-objdir.h\"\n     + #include \"tree.h\"\n     + #include \"userdiff.h\"\n     +@@ builtin/log.c: static void cmd_log_init(int argc, const char **argv, const char *prefix,\n     + \tcmd_log_init_finish(argc, argv, prefix, rev, opt, cfg);\n     + }\n     + \n     ++static void advise_if_log_stopped_at_shallow_boundary(struct rev_info *rev,\n     ++\t\t\t\t\t\t       struct commit *last_shown)\n     ++{\n     ++\tif (!last_shown)\n     ++\t\treturn;\n     ++\t/* a plain \"git log\" running out of history is expected */\n     ++\tif (rev->max_count < 0 && rev->max_age == (timestamp_t)-1)\n     ++\t\treturn;\n     ++\tif (!is_repository_shallow(the_repository))\n     ++\t\treturn;\n     ++\tif (!commit_is_shallow_boundary(the_repository, &last_shown->object.oid))\n     ++\t\treturn;\n     ++\twait_for_pager();\n     ++\tadvise_if_enabled(ADVICE_SHALLOW_HISTORY,\n     ++\t\t\t   _(\"'%s' stopped at %s because this repository is a shallow\\n\"\n     ++\t\t\t     \"clone, and might have more history upstream that was never fetched.\"),\n     ++\t\t\t   \"git log\",\n     ++\t\t\t   repo_find_unique_abbrev(the_repository,\n     ++\t\t\t\t\t\t    &last_shown->object.oid,\n     ++\t\t\t\t\t\t    DEFAULT_ABBREV));\n     ++}\n     ++\n     + static int cmd_log_walk_no_free(struct rev_info *rev)\n     + {\n     + \tstruct commit *commit;\n     ++\tstruct commit *last_shown = NULL;\n     + \tint saved_nrl = 0;\n     + \tint saved_dcctc = 0;\n     + \tint result;\n     +@@ builtin/log.c: static int cmd_log_walk_no_free(struct rev_info *rev)\n     + \t * retain that state information if replacing rev->diffopt in this loop\n     + \t */\n     + \twhile ((commit = get_revision(rev)) != NULL) {\n     ++\t\tlast_shown = commit;\n     + \t\tif (!log_tree_commit(rev, commit) && rev->max_count >= 0)\n     + \t\t\t/*\n     + \t\t\t * We decremented max_count in get_revision,\n     +@@ builtin/log.c: static int cmd_log_walk_no_free(struct rev_info *rev)\n     + \t\tif (rev->diffopt.degraded_cc_to_c)\n     + \t\t\tsaved_dcctc = 1;\n     + \t}\n     ++\tadvise_if_log_stopped_at_shallow_boundary(rev, last_shown);\n     + \trev->diffopt.degraded_cc_to_c = saved_dcctc;\n     + \trev->diffopt.needed_rename_limit = saved_nrl;\n     + \n     +\n       ## object-name.c ##\n      @@\n       #include \"repo-settings.h\"\n     @@ object-name.c: static int get_oid_basic(struct repository *r, const char *str, i\n       \treturn 0;\n       }\n       \n     ++struct remote_tracking_search {\n     ++\tchar *dst;\n     ++\tchar *remote_name;\n     ++\tchar *branch_name;\n     ++};\n     ++\n     ++static int search_remote_tracking(struct remote *remote, void *cb_data)\n     ++{\n     ++\tstruct remote_tracking_search *search = cb_data;\n     ++\tstruct refspec_item query;\n     ++\n     ++\tmemset(&query, 0, sizeof(query));\n     ++\tquery.dst = search->dst;\n     ++\tif (remote_find_tracking(remote, &query))\n     ++\t\treturn 0;\n     ++\tsearch->remote_name = xstrdup(remote->name);\n     ++\tsearch->branch_name = query.src;\n     ++\treturn 1;\n     ++}\n     ++\n     ++/*\n     ++ * If \"name\" resolves to a remote-tracking ref, find which configured\n     ++ * remote it actually belongs to and what branch of that remote's it is,\n     ++ * by reverse mapping through each remote's real fetch refspec instead\n     ++ * of assuming the common \"refs/remotes/<remote>/<branch>\" layout,\n     ++ * since a remote's refspec need not put its branches there at all. On a\n     ++ * match, fill \"remote_out\" and \"branch_out\" with newly allocated copies\n     ++ * and return true. Otherwise leave them untouched and return false.\n     ++ */\n     ++static bool parse_remote_tracking_name(struct repository *r,\n     ++\t\t\t\t       const char *name, int namelen,\n     ++\t\t\t\t       char **remote_out, char **branch_out)\n     ++{\n     ++\tstruct object_id oid;\n     ++\tchar *real_ref = NULL;\n     ++\tstruct remote_tracking_search search = { 0 };\n     ++\tconst char *branch_name;\n     ++\n     ++\tif (!repo_dwim_ref(r, name, namelen, &oid, &real_ref, 0) || !real_ref)\n     ++\t\treturn false;\n     ++\n     ++\tsearch.dst = real_ref;\n     ++\tif (!for_each_remote(search_remote_tracking, &search)) {\n     ++\t\tfree(real_ref);\n     ++\t\treturn false;\n     ++\t}\n     ++\tfree(real_ref);\n     ++\n     ++\tbranch_name = search.branch_name;\n     ++\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n     ++\t*remote_out = search.remote_name;\n     ++\t*branch_out = xstrdup(branch_name);\n     ++\tfree(search.branch_name);\n     ++\treturn true;\n     ++}\n     ++\n      +/*\n      + * When a \"name~<n>\" or \"name^<n>\" walk runs out of parents at \"commit\",\n      + * and that is because \"commit\" is where this shallow repository's history\n     -+ * was cut off (rather than commit genuinely being a root commit), let the\n     ++ * was cut off (rather than commit being a real root commit), let the\n      + * user know that fetching more history might be what they are after.\n      + *\n      + * \"suggested_depth\" is the --deepen value to recommend. For \"name^<n>\"\n      + * this is always 1: deepening by one generation fetches \"commit\"'s real\n      + * parent list in full, whatever it turns out to contain, regardless of\n     -+ * which parent index <n> asked for. If \"name\" looks like\n     -+ * \"<remote>/<branch>\" and <remote> is a configured remote, the suggested\n     -+ * command names that remote and branch instead of leaving them as\n     -+ * placeholders.\n     ++ * which parent index <n> asked for.\n      + */\n      +static void advise_if_shallow_cutoff(struct repository *r,\n      +\t\t\t\t     const char *name, int namelen,\n     @@ object-name.c: static int get_oid_basic(struct repository *r, const char *str, i\n      +\t\t\t\t     unsigned lookup_flags,\n      +\t\t\t\t     int suggested_depth)\n      +{\n     -+\tstruct commit_graft *graft;\n     -+\tconst char *slash;\n     ++\tchar *remote = NULL, *branch = NULL;\n      +\tstruct strbuf cmd = STRBUF_INIT;\n      +\n      +\tif (lookup_flags & GET_OID_QUIETLY)\n      +\t\treturn;\n      +\tif (!is_repository_shallow(r))\n      +\t\treturn;\n     -+\tgraft = lookup_commit_graft(r, &commit->object.oid);\n     -+\tif (!graft || graft->nr_parent != -1)\n     ++\tif (!commit_is_shallow_boundary(r, &commit->object.oid))\n      +\t\treturn;\n      +\n     -+\tslash = memchr(name, '/', namelen);\n     -+\tif (slash) {\n     -+\t\tchar *remote_candidate = xstrndup(name, slash - name);\n     -+\t\tif (remote_is_configured(remote_get(remote_candidate), 0))\n     -+\t\t\tstrbuf_addf(&cmd, \"git fetch --deepen=%d %s %.*s\",\n     -+\t\t\t\t    suggested_depth, remote_candidate,\n     -+\t\t\t\t    (int)(name + namelen - (slash + 1)), slash + 1);\n     -+\t\tfree(remote_candidate);\n     -+\t}\n     -+\tif (!cmd.len)\n     ++\tif (parse_remote_tracking_name(r, name, namelen, &remote, &branch))\n     ++\t\tstrbuf_addf(&cmd, \"git fetch --deepen=%d %s %s\",\n     ++\t\t\t    suggested_depth, remote, branch);\n     ++\telse\n      +\t\tstrbuf_addf(&cmd, \"git fetch --deepen=%d <remote> <branch>\",\n      +\t\t\t    suggested_depth);\n     ++\tfree(remote);\n     ++\tfree(branch);\n      +\n      +\tadvise_if_enabled(ADVICE_SHALLOW_HISTORY,\n      +\t\t\t   _(\"'%.*s' does not have that many ancestors locally.\\n\"\n      +\t\t\t     \"History stops at %s because this repository is a\\n\"\n     -+\t\t\t     \"shallow clone. To fetch more of it, try:\\n\"\n     ++\t\t\t     \"shallow clone, and might have more history upstream.\\n\"\n     ++\t\t\t     \"To check, try:\\n\"\n      +\t\t\t     \"\\n\"\n      +\t\t\t     \"  %s\"),\n     -+\t\t\t   namelen, name, oid_to_hex(&commit->object.oid), cmd.buf);\n     ++\t\t\t   namelen, name,\n     ++\t\t\t   repo_find_unique_abbrev(r, &commit->object.oid, DEFAULT_ABBREV),\n     ++\t\t\t   cmd.buf);\n      +\tstrbuf_release(&cmd);\n      +}\n      +\n     @@ object-name.c: static enum get_oid_result get_nth_ancestor(struct repository *r,\n       \twhile (generation--) {\n      -\t\tif (repo_parse_commit(r, commit) || !commit->parents)\n      +\t\tif (repo_parse_commit(r, commit))\n     - \t\t\treturn MISSING_OBJECT;\n     ++\t\t\treturn MISSING_OBJECT;\n      +\t\tif (!commit->parents) {\n      +\t\t\t/* Remaining \"generation\" plus this failed step is the actual gap. */\n      +\t\t\tadvise_if_shallow_cutoff(r, name, len, commit,\n      +\t\t\t\t\t\t lookup_flags, generation + 1);\n     -+\t\t\treturn MISSING_OBJECT;\n     + \t\t\treturn MISSING_OBJECT;\n      +\t\t}\n       \t\tcommit = commit->parents->item;\n       \t}\n     @@ object-name.c: static enum get_oid_result get_oid_1(struct repository *r,\n       \n       \tret = peel_onion(r, name, len, oid, lookup_flags);\n      \n     + ## shallow.c ##\n     +@@ shallow.c: int is_repository_shallow(struct repository *r)\n     + \treturn r->parsed_objects->is_shallow;\n     + }\n     + \n     ++int commit_is_shallow_boundary(struct repository *r,\n     ++\t\t\t       const struct object_id *oid)\n     ++{\n     ++\tstruct commit_graft *graft = lookup_commit_graft(r, oid);\n     ++\n     ++\treturn graft && graft->nr_parent == -1;\n     ++}\n     ++\n     + static void reset_repository_shallow(struct repository *r)\n     + {\n     + \tr->parsed_objects->is_shallow = -1;\n     +\n     + ## shallow.h ##\n     +@@ shallow.h: int register_shallow(struct repository *r, const struct object_id *oid);\n     + int unregister_shallow(const struct object_id *oid);\n     + int is_repository_shallow(struct repository *r);\n     + \n     ++/*\n     ++ * True if \"oid\" is where this shallow repository's history was cut off,\n     ++ * i.e. a shallow-graft commit with no parents recorded locally, as\n     ++ * opposed to a commit that is actually a root.\n     ++ */\n     ++int commit_is_shallow_boundary(struct repository *r,\n     ++\t\t\t       const struct object_id *oid);\n     ++\n     + /*\n     +  * Lock for updating the $GIT_DIR/shallow file.\n     +  *\n     +\n       ## t/t1500-rev-parse.sh ##\n      @@ t/t1500-rev-parse.sh: test_expect_success 'rev-parse --is-shallow-repository in non-shallow repo' '\n       \ttest_cmp expect actual\n     @@ t/t1500-rev-parse.sh: test_expect_success 'rev-parse --is-shallow-repository in\n      +\tcat >expect <<-EOF &&\n      +\thint: '$name' does not have that many ancestors locally.\n      +\thint: History stops at $oid because this repository is a\n     -+\thint: shallow clone. To fetch more of it, try:\n     ++\thint: shallow clone, and might have more history upstream.\n     ++\thint: To check, try:\n      +\thint:\n      +\thint:   $cmd\n      +\thint: Disable this message with \"git config set advice.shallowHistory false\"\n     @@ t/t1500-rev-parse.sh: test_expect_success 'rev-parse --is-shallow-repository in\n      +\tgit clone --no-local --depth=1 --branch main --single-branch \\\n      +\t\t.git shallow-advice &&\n      +\ttest_when_finished \"rm -rf shallow-advice\" &&\n     -+\toid=$(git -C shallow-advice rev-parse origin/main) &&\n     ++\toid=$(git -C shallow-advice rev-parse --short origin/main) &&\n      +\ttest_must_fail git -C shallow-advice rev-parse origin/main~1 2>err &&\n      +\tcheck_shallow_history_advice origin/main \"$oid\" \\\n      +\t\t\"git fetch --deepen=1 origin main\"\n     @@ t/t1500-rev-parse.sh: test_expect_success 'rev-parse --is-shallow-repository in\n      +\ttest_when_finished \"rm -rf shallow-advice-partial\" &&\n      +\t(\n      +\t\tcd shallow-advice-partial &&\n     -+\t\toid=$(git rev-parse origin/main~2) &&\n     ++\t\toid=$(git rev-parse --short origin/main~2) &&\n      +\t\ttest_must_fail git rev-parse origin/main~5 2>err &&\n      +\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n      +\t\t\t\"git fetch --deepen=3 origin main\" &&\n     @@ t/t1500-rev-parse.sh: test_expect_success 'rev-parse --is-shallow-repository in\n      +\tgit clone --no-local --depth=1 --branch main --single-branch \\\n      +\t\t.git shallow-advice-caret &&\n      +\ttest_when_finished \"rm -rf shallow-advice-caret\" &&\n     -+\toid=$(git -C shallow-advice-caret rev-parse origin/main) &&\n     ++\toid=$(git -C shallow-advice-caret rev-parse --short origin/main) &&\n      +\ttest_must_fail git -C shallow-advice-caret rev-parse origin/main^1 2>err &&\n      +\tcheck_shallow_history_advice origin/main \"$oid\" \\\n      +\t\t\"git fetch --deepen=1 origin main\"\n     @@ t/t1500-rev-parse.sh: test_expect_success 'rev-parse --is-shallow-repository in\n      +\ttest_when_finished \"rm -rf shallow-advice-merge\" &&\n      +\t(\n      +\t\tcd shallow-advice-merge &&\n     -+\t\toid=$(git rev-parse origin/main) &&\n     ++\t\toid=$(git rev-parse --short origin/main) &&\n      +\t\ttest_must_fail git rev-parse origin/main^2 2>err &&\n      +\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n      +\t\t\t\"git fetch --deepen=1 origin main\" &&\n     @@ t/t1500-rev-parse.sh: test_expect_success 'rev-parse --is-shallow-repository in\n       test_expect_success 'rev-parse --show-object-format in repo' '\n       \ttest_oid algo >expect &&\n       \tgit rev-parse --show-object-format >actual &&\n     +\n     + ## t/t4202-log.sh ##\n     +@@ t/t4202-log.sh: test_expect_success 'log --invert-grep --grep --author' '\n     + \ttest_cmp expect actual\n     + '\n     + \n     ++check_log_shallow_history_advice () {\n     ++\toid=$1 &&\n     ++\tgrep '^hint:' err >actual &&\n     ++\tcat >expect <<-EOF &&\n     ++\thint: 'git log' stopped at $oid because this repository is a shallow\n     ++\thint: clone, and might have more history upstream that was never fetched.\n     ++\thint: Disable this message with \"git config set advice.shallowHistory false\"\n     ++\tEOF\n     ++\ttest_cmp expect actual\n     ++}\n     ++\n     ++test_expect_success 'set up linear history for shallow log advice tests' '\n     ++\tgit checkout --orphan loghist &&\n     ++\ttest_commit loghist_1 &&\n     ++\ttest_commit loghist_2 &&\n     ++\ttest_commit loghist_3 &&\n     ++\ttest_commit loghist_4 &&\n     ++\ttest_commit loghist_5 &&\n     ++\tgit checkout main\n     ++'\n     ++\n     ++test_expect_success 'log -<n> advises when it runs out of history at a shallow boundary' '\n     ++\tgit clone --no-local --depth=2 --branch loghist --single-branch \\\n     ++\t\t.git shallow-log-advice &&\n     ++\ttest_when_finished \"rm -rf shallow-log-advice\" &&\n     ++\toid=$(git -C shallow-log-advice rev-parse --short origin/loghist~1) &&\n     ++\tgit -C shallow-log-advice log -5 --oneline origin/loghist >out 2>err &&\n     ++\tcheck_log_shallow_history_advice \"$oid\"\n     ++'\n     ++\n     ++test_expect_success 'log --since advises when it runs out of history at a shallow boundary' '\n     ++\tgit clone --no-local --depth=2 --branch loghist --single-branch \\\n     ++\t\t.git shallow-log-advice-since &&\n     ++\ttest_when_finished \"rm -rf shallow-log-advice-since\" &&\n     ++\toid=$(git -C shallow-log-advice-since rev-parse --short origin/loghist~1) &&\n     ++\tgit -C shallow-log-advice-since log --since=2000-01-01 --oneline \\\n     ++\t\torigin/loghist >out 2>err &&\n     ++\tcheck_log_shallow_history_advice \"$oid\"\n     ++'\n     ++\n     ++test_expect_success 'log without -<n> or --since does not advise at a shallow boundary' '\n     ++\tgit clone --no-local --depth=1 --branch loghist --single-branch \\\n     ++\t\t.git shallow-log-plain &&\n     ++\ttest_when_finished \"rm -rf shallow-log-plain\" &&\n     ++\tgit -C shallow-log-plain log --oneline origin/loghist >out 2>err &&\n     ++\ttest_grep ! \"^hint:\" err\n     ++'\n     ++\n     ++test_expect_success 'log -<n> does not advise when satisfied within local history' '\n     ++\tgit clone --no-local --depth=5 --branch loghist --single-branch \\\n     ++\t\t.git shallow-log-satisfied &&\n     ++\ttest_when_finished \"rm -rf shallow-log-satisfied\" &&\n     ++\tgit -C shallow-log-satisfied log -2 --oneline origin/loghist >out 2>err &&\n     ++\ttest_grep ! \"^hint:\" err\n     ++'\n     ++\n     ++test_expect_success 'log -<n> shallow history advice can be disabled' '\n     ++\tgit clone --no-local --depth=1 --branch loghist --single-branch \\\n     ++\t\t.git shallow-log-off &&\n     ++\ttest_when_finished \"rm -rf shallow-log-off\" &&\n     ++\tgit -C shallow-log-off -c advice.shallowHistory=false \\\n     ++\t\tlog -5 --oneline origin/loghist >out 2>err &&\n     ++\ttest_grep ! \"^hint:\" err\n     ++'\n     ++\n     ++test_expect_success 'log -<n> does not advise in a non-shallow repository' '\n     ++\ttest_when_finished \"rm -rf non-shallow-log\" &&\n     ++\tgit init -q non-shallow-log &&\n     ++\ttest_commit -C non-shallow-log only-commit &&\n     ++\tgit -C non-shallow-log log -20 --oneline >out 2>err &&\n     ++\ttest_grep ! \"^hint:\" err\n     ++'\n     ++\n     + test_done\n\n\n Documentation/config/advice.adoc |   4 +\n advice.c                         |   1 +\n advice.h                         |   1 +\n builtin/log.c                    |  26 +++++++\n object-name.c                    | 125 +++++++++++++++++++++++++++++--\n shallow.c                        |   8 ++\n shallow.h                        |   8 ++\n t/t1500-rev-parse.sh             | 109 +++++++++++++++++++++++++++\n t/t4202-log.sh                   |  73 ++++++++++++++++++\n 9 files changed, 350 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc\nindex 81f80a9274..bb1e508bb6 100644\n--- a/Documentation/config/advice.adoc\n+++ b/Documentation/config/advice.adoc\n@@ -128,6 +128,10 @@ all advice messages.\n \t\tgive directions on how to proceed from the current state.\n \tsequencerInUse::\n \t\tShown when a sequencer command is already in progress.\n+\tshallowHistory::\n+\t\tShown when `<rev>~<n>` cannot resolve enough ancestors of\n+\t\t`<rev>` because history stops at a shallow boundary, to\n+\t\tsuggest fetching more history.\n \tskippedCherryPicks::\n \t\tShown when linkgit:git-rebase[1] skips a commit that has already\n \t\tbeen cherry-picked onto the upstream branch.\ndiff --git a/advice.c b/advice.c\nindex 63bf8b0c5f..e22706ead5 100644\n--- a/advice.c\n+++ b/advice.c\n@@ -80,6 +80,7 @@ static struct {\n \t[ADVICE_RM_HINTS]\t\t\t\t= { \"rmHints\" },\n \t[ADVICE_SEQUENCER_IN_USE]\t\t\t= { \"sequencerInUse\" },\n \t[ADVICE_SET_UPSTREAM_FAILURE]\t\t\t= { \"setUpstreamFailure\" },\n+\t[ADVICE_SHALLOW_HISTORY]\t\t\t= { \"shallowHistory\" },\n \t[ADVICE_SKIPPED_CHERRY_PICKS]\t\t\t= { \"skippedCherryPicks\" },\n \t[ADVICE_SPARSE_INDEX_EXPANDED]\t\t\t= { \"sparseIndexExpanded\" },\n \t[ADVICE_STATUS_AHEAD_BEHIND_WARNING]\t\t= { \"statusAheadBehindWarning\" },\ndiff --git a/advice.h b/advice.h\nindex 66f6cd6a77..e80e3e82cc 100644\n--- a/advice.h\n+++ b/advice.h\n@@ -47,6 +47,7 @@ enum advice_type {\n \tADVICE_RM_HINTS,\n \tADVICE_SEQUENCER_IN_USE,\n \tADVICE_SET_UPSTREAM_FAILURE,\n+\tADVICE_SHALLOW_HISTORY,\n \tADVICE_SKIPPED_CHERRY_PICKS,\n \tADVICE_SPARSE_INDEX_EXPANDED,\n \tADVICE_STATUS_AHEAD_BEHIND_WARNING,\ndiff --git a/builtin/log.c b/builtin/log.c\nindex 350b35c556..22a40c7d28 100644\n--- a/builtin/log.c\n+++ b/builtin/log.c\n@@ -47,6 +47,7 @@\n #include \"commit-reach.h\"\n #include \"promisor-remote.h\"\n #include \"range-diff.h\"\n+#include \"shallow.h\"\n #include \"tmp-objdir.h\"\n #include \"tree.h\"\n #include \"userdiff.h\"\n@@ -396,9 +397,32 @@ static void cmd_log_init(int argc, const char **argv, const char *prefix,\n \tcmd_log_init_finish(argc, argv, prefix, rev, opt, cfg);\n }\n \n+static void advise_if_log_stopped_at_shallow_boundary(struct rev_info *rev,\n+\t\t\t\t\t\t       struct commit *last_shown)\n+{\n+\tif (!last_shown)\n+\t\treturn;\n+\t/* a plain \"git log\" running out of history is expected */\n+\tif (rev->max_count < 0 && rev->max_age == (timestamp_t)-1)\n+\t\treturn;\n+\tif (!is_repository_shallow(the_repository))\n+\t\treturn;\n+\tif (!commit_is_shallow_boundary(the_repository, &last_shown->object.oid))\n+\t\treturn;\n+\twait_for_pager();\n+\tadvise_if_enabled(ADVICE_SHALLOW_HISTORY,\n+\t\t\t   _(\"'%s' stopped at %s because this repository is a shallow\\n\"\n+\t\t\t     \"clone, and might have more history upstream that was never fetched.\"),\n+\t\t\t   \"git log\",\n+\t\t\t   repo_find_unique_abbrev(the_repository,\n+\t\t\t\t\t\t    &last_shown->object.oid,\n+\t\t\t\t\t\t    DEFAULT_ABBREV));\n+}\n+\n static int cmd_log_walk_no_free(struct rev_info *rev)\n {\n \tstruct commit *commit;\n+\tstruct commit *last_shown = NULL;\n \tint saved_nrl = 0;\n \tint saved_dcctc = 0;\n \tint result;\n@@ -412,6 +436,7 @@ static int cmd_log_walk_no_free(struct rev_info *rev)\n \t * retain that state information if replacing rev->diffopt in this loop\n \t */\n \twhile ((commit = get_revision(rev)) != NULL) {\n+\t\tlast_shown = commit;\n \t\tif (!log_tree_commit(rev, commit) && rev->max_count >= 0)\n \t\t\t/*\n \t\t\t * We decremented max_count in get_revision,\n@@ -437,6 +462,7 @@ static int cmd_log_walk_no_free(struct rev_info *rev)\n \t\tif (rev->diffopt.degraded_cc_to_c)\n \t\t\tsaved_dcctc = 1;\n \t}\n+\tadvise_if_log_stopped_at_shallow_boundary(rev, last_shown);\n \trev->diffopt.degraded_cc_to_c = saved_dcctc;\n \trev->diffopt.needed_rename_limit = saved_nrl;\n \ndiff --git a/object-name.c b/object-name.c\nindex 4eda8c8eac..cb601e1532 100644\n--- a/object-name.c\n+++ b/object-name.c\n@@ -22,6 +22,7 @@\n #include \"repo-settings.h\"\n #include \"repository.h\"\n #include \"setup.h\"\n+#include \"shallow.h\"\n #include \"midx.h\"\n #include \"commit-reach.h\"\n #include \"date.h\"\n@@ -824,9 +825,115 @@ static int get_oid_basic(struct repository *r, const char *str, int len,\n \treturn 0;\n }\n \n+struct remote_tracking_search {\n+\tchar *dst;\n+\tchar *remote_name;\n+\tchar *branch_name;\n+};\n+\n+static int search_remote_tracking(struct remote *remote, void *cb_data)\n+{\n+\tstruct remote_tracking_search *search = cb_data;\n+\tstruct refspec_item query;\n+\n+\tmemset(&query, 0, sizeof(query));\n+\tquery.dst = search->dst;\n+\tif (remote_find_tracking(remote, &query))\n+\t\treturn 0;\n+\tsearch->remote_name = xstrdup(remote->name);\n+\tsearch->branch_name = query.src;\n+\treturn 1;\n+}\n+\n+/*\n+ * If \"name\" resolves to a remote-tracking ref, find which configured\n+ * remote it actually belongs to and what branch of that remote's it is,\n+ * by reverse mapping through each remote's real fetch refspec instead\n+ * of assuming the common \"refs/remotes/<remote>/<branch>\" layout,\n+ * since a remote's refspec need not put its branches there at all. On a\n+ * match, fill \"remote_out\" and \"branch_out\" with newly allocated copies\n+ * and return true. Otherwise leave them untouched and return false.\n+ */\n+static bool parse_remote_tracking_name(struct repository *r,\n+\t\t\t\t       const char *name, int namelen,\n+\t\t\t\t       char **remote_out, char **branch_out)\n+{\n+\tstruct object_id oid;\n+\tchar *real_ref = NULL;\n+\tstruct remote_tracking_search search = { 0 };\n+\tconst char *branch_name;\n+\n+\tif (!repo_dwim_ref(r, name, namelen, &oid, &real_ref, 0) || !real_ref)\n+\t\treturn false;\n+\n+\tsearch.dst = real_ref;\n+\tif (!for_each_remote(search_remote_tracking, &search)) {\n+\t\tfree(real_ref);\n+\t\treturn false;\n+\t}\n+\tfree(real_ref);\n+\n+\tbranch_name = search.branch_name;\n+\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n+\t*remote_out = search.remote_name;\n+\t*branch_out = xstrdup(branch_name);\n+\tfree(search.branch_name);\n+\treturn true;\n+}\n+\n+/*\n+ * When a \"name~<n>\" or \"name^<n>\" walk runs out of parents at \"commit\",\n+ * and that is because \"commit\" is where this shallow repository's history\n+ * was cut off (rather than commit being a real root commit), let the\n+ * user know that fetching more history might be what they are after.\n+ *\n+ * \"suggested_depth\" is the --deepen value to recommend. For \"name^<n>\"\n+ * this is always 1: deepening by one generation fetches \"commit\"'s real\n+ * parent list in full, whatever it turns out to contain, regardless of\n+ * which parent index <n> asked for.\n+ */\n+static void advise_if_shallow_cutoff(struct repository *r,\n+\t\t\t\t     const char *name, int namelen,\n+\t\t\t\t     struct commit *commit,\n+\t\t\t\t     unsigned lookup_flags,\n+\t\t\t\t     int suggested_depth)\n+{\n+\tchar *remote = NULL, *branch = NULL;\n+\tstruct strbuf cmd = STRBUF_INIT;\n+\n+\tif (lookup_flags & GET_OID_QUIETLY)\n+\t\treturn;\n+\tif (!is_repository_shallow(r))\n+\t\treturn;\n+\tif (!commit_is_shallow_boundary(r, &commit->object.oid))\n+\t\treturn;\n+\n+\tif (parse_remote_tracking_name(r, name, namelen, &remote, &branch))\n+\t\tstrbuf_addf(&cmd, \"git fetch --deepen=%d %s %s\",\n+\t\t\t    suggested_depth, remote, branch);\n+\telse\n+\t\tstrbuf_addf(&cmd, \"git fetch --deepen=%d <remote> <branch>\",\n+\t\t\t    suggested_depth);\n+\tfree(remote);\n+\tfree(branch);\n+\n+\tadvise_if_enabled(ADVICE_SHALLOW_HISTORY,\n+\t\t\t   _(\"'%.*s' does not have that many ancestors locally.\\n\"\n+\t\t\t     \"History stops at %s because this repository is a\\n\"\n+\t\t\t     \"shallow clone, and might have more history upstream.\\n\"\n+\t\t\t     \"To check, try:\\n\"\n+\t\t\t     \"\\n\"\n+\t\t\t     \"  %s\"),\n+\t\t\t   namelen, name,\n+\t\t\t   repo_find_unique_abbrev(r, &commit->object.oid, DEFAULT_ABBREV),\n+\t\t\t   cmd.buf);\n+\tstrbuf_release(&cmd);\n+}\n+\n static enum get_oid_result get_parent(struct repository *r,\n \t\t\t\t      const char *name, int len,\n-\t\t\t\t      struct object_id *result, int idx)\n+\t\t\t\t      struct object_id *result, int idx,\n+\t\t\t\t      unsigned lookup_flags)\n {\n \tstruct object_id oid;\n \tenum get_oid_result ret = get_oid_1(r, name, len, &oid,\n@@ -851,13 +958,15 @@ static enum get_oid_result get_parent(struct repository *r,\n \t\t}\n \t\tp = p->next;\n \t}\n+\tadvise_if_shallow_cutoff(r, name, len, commit, lookup_flags, 1);\n \treturn MISSING_OBJECT;\n }\n \n static enum get_oid_result get_nth_ancestor(struct repository *r,\n \t\t\t\t\t    const char *name, int len,\n \t\t\t\t\t    struct object_id *result,\n-\t\t\t\t\t    int generation)\n+\t\t\t\t\t    int generation,\n+\t\t\t\t\t    unsigned lookup_flags)\n {\n \tstruct object_id oid;\n \tstruct commit *commit;\n@@ -871,8 +980,14 @@ static enum get_oid_result get_nth_ancestor(struct repository *r,\n \t\treturn MISSING_OBJECT;\n \n \twhile (generation--) {\n-\t\tif (repo_parse_commit(r, commit) || !commit->parents)\n+\t\tif (repo_parse_commit(r, commit))\n+\t\t\treturn MISSING_OBJECT;\n+\t\tif (!commit->parents) {\n+\t\t\t/* Remaining \"generation\" plus this failed step is the actual gap. */\n+\t\t\tadvise_if_shallow_cutoff(r, name, len, commit,\n+\t\t\t\t\t\t lookup_flags, generation + 1);\n \t\t\treturn MISSING_OBJECT;\n+\t\t}\n \t\tcommit = commit->parents->item;\n \t}\n \toidcpy(result, &commit->object.oid);\n@@ -1119,9 +1234,9 @@ static enum get_oid_result get_oid_1(struct repository *r,\n \t\telse if (num > INT_MAX)\n \t\t\treturn MISSING_OBJECT;\n \t\tif (has_suffix == '^')\n-\t\t\treturn get_parent(r, name, len1, oid, num);\n+\t\t\treturn get_parent(r, name, len1, oid, num, lookup_flags);\n \t\t/* else if (has_suffix == '~') -- goes without saying */\n-\t\treturn get_nth_ancestor(r, name, len1, oid, num);\n+\t\treturn get_nth_ancestor(r, name, len1, oid, num, lookup_flags);\n \t}\n \n \tret = peel_onion(r, name, len, oid, lookup_flags);\ndiff --git a/shallow.c b/shallow.c\nindex 8e244a5669..2e0cf09f32 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -92,6 +92,14 @@ int is_repository_shallow(struct repository *r)\n \treturn r->parsed_objects->is_shallow;\n }\n \n+int commit_is_shallow_boundary(struct repository *r,\n+\t\t\t       const struct object_id *oid)\n+{\n+\tstruct commit_graft *graft = lookup_commit_graft(r, oid);\n+\n+\treturn graft && graft->nr_parent == -1;\n+}\n+\n static void reset_repository_shallow(struct repository *r)\n {\n \tr->parsed_objects->is_shallow = -1;\ndiff --git a/shallow.h b/shallow.h\nindex 6a64db42c9..66c9e723f7 100644\n--- a/shallow.h\n+++ b/shallow.h\n@@ -15,6 +15,14 @@ int register_shallow(struct repository *r, const struct object_id *oid);\n int unregister_shallow(const struct object_id *oid);\n int is_repository_shallow(struct repository *r);\n \n+/*\n+ * True if \"oid\" is where this shallow repository's history was cut off,\n+ * i.e. a shallow-graft commit with no parents recorded locally, as\n+ * opposed to a commit that is actually a root.\n+ */\n+int commit_is_shallow_boundary(struct repository *r,\n+\t\t\t       const struct object_id *oid);\n+\n /*\n  * Lock for updating the $GIT_DIR/shallow file.\n  *\ndiff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh\nindex 4174ca40c3..ecadeddcdb 100755\n--- a/t/t1500-rev-parse.sh\n+++ b/t/t1500-rev-parse.sh\n@@ -193,6 +193,115 @@ test_expect_success 'rev-parse --is-shallow-repository in non-shallow repo' '\n \ttest_cmp expect actual\n '\n \n+check_shallow_history_advice () {\n+\tname=$1 oid=$2 cmd=$3 &&\n+\tgrep '^hint:' err >actual &&\n+\tcat >expect <<-EOF &&\n+\thint: '$name' does not have that many ancestors locally.\n+\thint: History stops at $oid because this repository is a\n+\thint: shallow clone, and might have more history upstream.\n+\thint: To check, try:\n+\thint:\n+\thint:   $cmd\n+\thint: Disable this message with \"git config set advice.shallowHistory false\"\n+\tEOF\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'shallowHistory advice on ~N beyond shallow boundary' '\n+\ttest_commit shallow_advice_1 &&\n+\ttest_commit shallow_advice_2 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice &&\n+\ttest_when_finished \"rm -rf shallow-advice\" &&\n+\toid=$(git -C shallow-advice rev-parse --short origin/main) &&\n+\ttest_must_fail git -C shallow-advice rev-parse origin/main~1 2>err &&\n+\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\"git fetch --deepen=1 origin main\"\n+'\n+\n+test_expect_success 'shallowHistory advice accounts for depth already present' '\n+\ttest_commit shallow_partial_1 &&\n+\ttest_commit shallow_partial_2 &&\n+\ttest_commit shallow_partial_3 &&\n+\ttest_commit shallow_partial_4 &&\n+\ttest_commit shallow_partial_5 &&\n+\ttest_commit shallow_partial_6 &&\n+\tgit clone --no-local --depth=3 --branch main --single-branch \\\n+\t\t.git shallow-advice-partial &&\n+\ttest_when_finished \"rm -rf shallow-advice-partial\" &&\n+\t(\n+\t\tcd shallow-advice-partial &&\n+\t\toid=$(git rev-parse --short origin/main~2) &&\n+\t\ttest_must_fail git rev-parse origin/main~5 2>err &&\n+\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\t\"git fetch --deepen=3 origin main\" &&\n+\t\tgit fetch --deepen=3 origin &&\n+\t\tgit rev-parse origin/main~5 &&\n+\t\ttest_must_fail git rev-parse origin/main~6\n+\t)\n+'\n+\n+test_expect_success 'shallowHistory advice on ^N (first parent) beyond shallow boundary' '\n+\ttest_commit shallow_caret_1 &&\n+\ttest_commit shallow_caret_2 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-caret &&\n+\ttest_when_finished \"rm -rf shallow-advice-caret\" &&\n+\toid=$(git -C shallow-advice-caret rev-parse --short origin/main) &&\n+\ttest_must_fail git -C shallow-advice-caret rev-parse origin/main^1 2>err &&\n+\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\"git fetch --deepen=1 origin main\"\n+'\n+\n+test_expect_success 'shallowHistory advice on ^N suggests deepen=1 even for a merge parent' '\n+\ttest_commit shallow_merge_base &&\n+\tgit checkout -q -b shallow-merge-side &&\n+\ttest_commit shallow_merge_side1 &&\n+\tgit checkout -q main &&\n+\ttest_commit shallow_merge_main1 &&\n+\tgit merge -q --no-ff shallow-merge-side -m \"shallow merge commit\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-merge &&\n+\ttest_when_finished \"rm -rf shallow-advice-merge\" &&\n+\t(\n+\t\tcd shallow-advice-merge &&\n+\t\toid=$(git rev-parse --short origin/main) &&\n+\t\ttest_must_fail git rev-parse origin/main^2 2>err &&\n+\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\t\"git fetch --deepen=1 origin main\" &&\n+\t\tgit fetch -q --deepen=1 origin &&\n+\t\tgit rev-parse origin/main^1 &&\n+\t\tgit rev-parse origin/main^2\n+\t)\n+'\n+\n+test_expect_success 'shallowHistory advice can be disabled' '\n+\ttest_commit shallow_off_1 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-off &&\n+\ttest_when_finished \"rm -rf shallow-advice-off\" &&\n+\ttest_must_fail git -C shallow-advice-off \\\n+\t\t-c advice.shallowHistory=false rev-parse origin/main~1 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n+test_expect_success 'shallowHistory advice not shown for a non-shallow repository' '\n+\ttest_must_fail git rev-parse HEAD~100000 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n+test_expect_success 'shallowHistory advice not shown when resolution succeeds' '\n+\ttest_commit shallow_ok_1 &&\n+\ttest_commit shallow_ok_2 &&\n+\ttest_commit shallow_ok_3 &&\n+\tgit clone --no-local --depth=3 --branch main --single-branch \\\n+\t\t.git shallow-advice-ok &&\n+\ttest_when_finished \"rm -rf shallow-advice-ok\" &&\n+\tgit -C shallow-advice-ok rev-parse origin/main~1 >actual 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n test_expect_success 'rev-parse --show-object-format in repo' '\n \ttest_oid algo >expect &&\n \tgit rev-parse --show-object-format >actual &&\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 06552a6271..ad22934c9d 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -2452,4 +2452,77 @@ test_expect_success 'log --invert-grep --grep --author' '\n \ttest_cmp expect actual\n '\n \n+check_log_shallow_history_advice () {\n+\toid=$1 &&\n+\tgrep '^hint:' err >actual &&\n+\tcat >expect <<-EOF &&\n+\thint: 'git log' stopped at $oid because this repository is a shallow\n+\thint: clone, and might have more history upstream that was never fetched.\n+\thint: Disable this message with \"git config set advice.shallowHistory false\"\n+\tEOF\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'set up linear history for shallow log advice tests' '\n+\tgit checkout --orphan loghist &&\n+\ttest_commit loghist_1 &&\n+\ttest_commit loghist_2 &&\n+\ttest_commit loghist_3 &&\n+\ttest_commit loghist_4 &&\n+\ttest_commit loghist_5 &&\n+\tgit checkout main\n+'\n+\n+test_expect_success 'log -<n> advises when it runs out of history at a shallow boundary' '\n+\tgit clone --no-local --depth=2 --branch loghist --single-branch \\\n+\t\t.git shallow-log-advice &&\n+\ttest_when_finished \"rm -rf shallow-log-advice\" &&\n+\toid=$(git -C shallow-log-advice rev-parse --short origin/loghist~1) &&\n+\tgit -C shallow-log-advice log -5 --oneline origin/loghist >out 2>err &&\n+\tcheck_log_shallow_history_advice \"$oid\"\n+'\n+\n+test_expect_success 'log --since advises when it runs out of history at a shallow boundary' '\n+\tgit clone --no-local --depth=2 --branch loghist --single-branch \\\n+\t\t.git shallow-log-advice-since &&\n+\ttest_when_finished \"rm -rf shallow-log-advice-since\" &&\n+\toid=$(git -C shallow-log-advice-since rev-parse --short origin/loghist~1) &&\n+\tgit -C shallow-log-advice-since log --since=2000-01-01 --oneline \\\n+\t\torigin/loghist >out 2>err &&\n+\tcheck_log_shallow_history_advice \"$oid\"\n+'\n+\n+test_expect_success 'log without -<n> or --since does not advise at a shallow boundary' '\n+\tgit clone --no-local --depth=1 --branch loghist --single-branch \\\n+\t\t.git shallow-log-plain &&\n+\ttest_when_finished \"rm -rf shallow-log-plain\" &&\n+\tgit -C shallow-log-plain log --oneline origin/loghist >out 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n+test_expect_success 'log -<n> does not advise when satisfied within local history' '\n+\tgit clone --no-local --depth=5 --branch loghist --single-branch \\\n+\t\t.git shallow-log-satisfied &&\n+\ttest_when_finished \"rm -rf shallow-log-satisfied\" &&\n+\tgit -C shallow-log-satisfied log -2 --oneline origin/loghist >out 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n+test_expect_success 'log -<n> shallow history advice can be disabled' '\n+\tgit clone --no-local --depth=1 --branch loghist --single-branch \\\n+\t\t.git shallow-log-off &&\n+\ttest_when_finished \"rm -rf shallow-log-off\" &&\n+\tgit -C shallow-log-off -c advice.shallowHistory=false \\\n+\t\tlog -5 --oneline origin/loghist >out 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n+test_expect_success 'log -<n> does not advise in a non-shallow repository' '\n+\ttest_when_finished \"rm -rf non-shallow-log\" &&\n+\tgit init -q non-shallow-log &&\n+\ttest_commit -C non-shallow-log only-commit &&\n+\tgit -C non-shallow-log log -20 --oneline >out 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n test_done\n\nbase-commit: d38352cd43ab9745686d697872408bc3249a153f\n-- \ngitgitgadget\n"},{"id":"553019","messageId":"xmqqo6dpc7ay.fsf@gitster.g","threadId":"66356","inReplyTo":"pull.2413.v2.git.git.1790084326913.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] shallow: advise when a walk stops at a shallow boundary","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-22T21:20:05Z","receivedAt":"2026-09-22T21:20:11Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> diff --git a/builtin/log.c b/builtin/log.c\n> index 350b35c556..22a40c7d28 100644\n> --- a/builtin/log.c\n> +++ b/builtin/log.c\n> @@ -47,6 +47,7 @@\n>  #include \"commit-reach.h\"\n>  #include \"promisor-remote.h\"\n>  #include \"range-diff.h\"\n> +#include \"shallow.h\"\n>  #include \"tmp-objdir.h\"\n>  #include \"tree.h\"\n>  #include \"userdiff.h\"\n> @@ -396,9 +397,32 @@ static void cmd_log_init(int argc, const char **argv, const char *prefix,\n>  \tcmd_log_init_finish(argc, argv, prefix, rev, opt, cfg);\n>  }\n>  \n> +static void advise_if_log_stopped_at_shallow_boundary(struct rev_info *rev,\n> +\t\t\t\t\t\t       struct commit *last_shown)\n> +{\n> +\tif (!last_shown)\n> +\t\treturn;\n> +\t/* a plain \"git log\" running out of history is expected */\n> +\tif (rev->max_count < 0 && rev->max_age == (timestamp_t)-1)\n> +\t\treturn;\n\n\"git log -999\" may run out of commits because the history genuinely\nmay only have 20 commits, or the clone was made shallowly and we\nonly happen to have 20 commits at hand.  The same is true for \"git\nlog\" that does not get any count.  So I do not quite see the reason\nwhy we want to give an early return in this function.\n\n> +\tif (!is_repository_shallow(the_repository))\n> +\t\treturn;\n> +\tif (!commit_is_shallow_boundary(the_repository, &last_shown->object.oid))\n> +\t\treturn;\n> +\twait_for_pager();\n> +\tadvise_if_enabled(ADVICE_SHALLOW_HISTORY,\n> +\t\t\t   _(\"'%s' stopped at %s because this repository is a shallow\\n\"\n> +\t\t\t     \"clone, and might have more history upstream that was never fetched.\"),\n> +\t\t\t   \"git log\",\n> +\t\t\t   repo_find_unique_abbrev(the_repository,\n> +\t\t\t\t\t\t    &last_shown->object.oid,\n> +\t\t\t\t\t\t    DEFAULT_ABBREV));\n> +}\n> +\n\nAnyway, sorry, I regret opening this can of worms X-<.  It is not\nthat your implementation and design is bad, it is the problem being\nsolved that is bad.  But ...\n\n>  static int cmd_log_walk_no_free(struct rev_info *rev)\n>  {\n>  \tstruct commit *commit;\n> +\tstruct commit *last_shown = NULL;\n>  \tint saved_nrl = 0;\n>  \tint saved_dcctc = 0;\n>  \tint result;\n> @@ -412,6 +436,7 @@ static int cmd_log_walk_no_free(struct rev_info *rev)\n>  \t * retain that state information if replacing rev->diffopt in this loop\n>  \t */\n>  \twhile ((commit = get_revision(rev)) != NULL) {\n> +\t\tlast_shown = commit;\n>  \t\tif (!log_tree_commit(rev, commit) && rev->max_count >= 0)\n>  \t\t\t/*\n>  \t\t\t * We decremented max_count in get_revision,\n> @@ -437,6 +462,7 @@ static int cmd_log_walk_no_free(struct rev_info *rev)\n>  \t\tif (rev->diffopt.degraded_cc_to_c)\n>  \t\t\tsaved_dcctc = 1;\n>  \t}\n> +\tadvise_if_log_stopped_at_shallow_boundary(rev, last_shown);\n\n... the \"last shown\" commit may or may not be at shallow boundary.\nIt may be a normal root commit, yet there may be truncated side\nhistory that we stopped traversing during the above loop.  If for\nexample we had a history like this (time flows from left to right):\n\n     ()---b---d---e\t(side branch)\n                   \\\n                    \\\n  a---------c--------f------g\t(trunk)\n\nwhere a side branch is much denser than the trunk, and shallow clone\ntruncated the history, hiding the parents of 'b', we may see that\nour traversal goes 'g', 'f', 'e', 'd', 'c', 'b', 'a' and the last\nshown commit may be 'a', which is a genuine root commit.  But behind\n'b' there may be hundreds of commits on the side branch that\neventually leads down to 'a'.  Doesn't the user want to be notified\nthat they are missing tons of history behind 'b' in such a case when\n'b' is shown and we stop traversing its parents?\n\nThat was the original motivation behind the issue I raised during\nthe previous review, and that is why I say I regret opening this can\nof worms.  If the commit 'a' in the history had parentes hidden\nbehind a shallow boundary (in other words, 'a' is not root), then\nfrom the same traversal, we would see the \"traversal stopped at\nshallow boundary\" advise, which means that we sometimes see it and\nsometimes we don't, even though in either case we are showing 'b' as\nif it were a root when it is not.\n\nI do not think of a good way to solve this, and showing \"your\ntraversal happened to have ended at the shallow boundary\" only\nsometimes in an unreliable way is probably counter-productive, I am\nafraid.\n\nSo please forget what I said in the previous review.  Even though it\nmay be a good piece of information to have somehow for the user to\nknow which commit has its parents hidden beyond a shallow boundary,\na regular get_revision() traversal loop is probalby not a good place\nto do so.\n\nWe might want to show the information by enriching \"log --graph\"\noutput but that is totally unrelated to what you are doing with this\n<rev>~N topic.\n\n> +test_expect_success 'shallowHistory advice accounts for depth already present' '\n> +\ttest_commit shallow_partial_1 &&\n> +\ttest_commit shallow_partial_2 &&\n> +\ttest_commit shallow_partial_3 &&\n> +\ttest_commit shallow_partial_4 &&\n> +\ttest_commit shallow_partial_5 &&\n> +\ttest_commit shallow_partial_6 &&\n> +\tgit clone --no-local --depth=3 --branch main --single-branch \\\n> +\t\t.git shallow-advice-partial &&\n> +\ttest_when_finished \"rm -rf shallow-advice-partial\" &&\n> +\t(\n> +\t\tcd shallow-advice-partial &&\n> +\t\toid=$(git rev-parse --short origin/main~2) &&\n> +\t\ttest_must_fail git rev-parse origin/main~5 2>err &&\n> +\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n> +\t\t\t\"git fetch --deepen=3 origin main\" &&\n\nWould wew see the same output if we asked for \"origin/main^^^^^\"?\nJust being curious.\n\n> +\t\tgit fetch --deepen=3 origin &&\n> +\t\tgit rev-parse origin/main~5 &&\n> +\t\ttest_must_fail git rev-parse origin/main~6\n> +\t)\n> +'\n\nThanks, and sorry about the ill-defined feature request.\n"},{"id":"553097","messageId":"pull.2413.v3.git.git.1790189759802.gitgitgadget@gmail.com","threadId":"66356","inReplyTo":"pull.2413.git.git.1789898013916.gitgitgadget@gmail.com","subject":"[PATCH v3] object-name: explain why <rev>~N fails in a shallow clone","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-23T18:55:59Z","receivedAt":"2026-09-23T18:56:05Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAsking for a commit's ancestor with <rev>~N in a shallow clone that\ndoes not have N commits of history locally fails with a bare \"is not\na commit\" error, with no indication that the repository being shallow\nis the reason, or what to do about it.\n\nAdd a hint, shown when a walk runs out of parents exactly at a\nrecorded shallow boundary, not merely because the repository happens\nto be shallow elsewhere. When the revision <rev> is given as a\nremote-tracking branch, the remote and branch are exactly named in the\nsuggested command, found by reverse mapping through the remote's real\nrefspec instead of assuming a refs/remotes/<remote>/ layout, since a\nrefspec can put branches anywhere. For <rev>~N it suggests the exact\n--deepen needed, accounting for history already present. For <rev>^N\nit always suggests --deepen=1: a shallow boundary commit has no\nparents recorded at all, so one more generation reveals its complete\nreal parent list regardless of which index was asked for.\n\nThe hint only suggests that older history might exist. A recorded\nshallow boundary does not distinguish a depth cutoff from a commit\nthat is the true root: cloning with a depth that happens to exactly\nmatch the available history still records that root commit as a\nboundary, and only the remote knows for certain whether deepening\nwould fetch anything new.\n\nThe advice is threaded through GET_OID_QUIETLY so it is not shown\nduring the internal re-resolution some commands do while building a\nbetter error message, which would otherwise print it twice for the\nsame failing argument. A chained <rev>^^^ resolves one parent at a\ntime, and each step re-resolves everything before it through a fresh,\nalways non-quiet lookup, so a chained caret landing past a shallow\nboundary printed the hint twice instead of once. Thread the quiet bit\nthrough that inner lookup too, so it stays quiet exactly when the\nouter resolution asked it to.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    object-name: explain why ~N fails in a shallow clone\n    \n    Asking for a commit's ancestor with <rev>~N in a shallow clone that\n    doesn't have N commits of history locally fails with a \"is not a commit\"\n    error, with no indication that the repository being shallow is the\n    reason.\n    \n    Changes in v3:\n    \n     * Drop the git log -<n>/--since extension entirely, per feedback that\n       last commit shown is an unreliable signal for a shallow boundary.\n     * Fix a bug for chained <rev>^^^ where it would print the hint twice.\n    \n    Changes in v2:\n    \n     * <ref> renamed to <rev> throughout. Restrict the commit message and\n       advice.shallowHistory documentation to <rev>~N, since <rev>^N doesn't\n       get deeper with a higher index the way <rev>~N does.\n     * Bare <remote>/<branch> names are now resolved by reverse mapping\n       through the remote's configured refspec (remote_find_tracking),\n       instead of assuming branches live at refs/remotes/<remote>/<branch>.\n     * Extend the shallow history hint to git log -<n> and git log\n       --since=<date>, which previously stopped early at a shallow boundary\n       with no indication anything was missing.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2413%2FHaraldNordgren%2Fshallow-history-advice-hint-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2413/HaraldNordgren/shallow-history-advice-hint-v3\nPull-Request: https://github.com/git/git/pull/2413\n\nRange-diff vs v2:\n\n 1:  305d22d33e ! 1:  d5f2128258 shallow: advise when a walk stops at a shallow boundary\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    shallow: advise when a walk stops at a shallow boundary\n     +    object-name: explain why <rev>~N fails in a shallow clone\n      \n          Asking for a commit's ancestor with <rev>~N in a shallow clone that\n          does not have N commits of history locally fails with a bare \"is not\n          a commit\" error, with no indication that the repository being shallow\n          is the reason, or what to do about it.\n      \n     -    git log -<n> and git log --since=<date> have the opposite problem.\n     -    Instead of failing, they silently stop once they reach the shallow\n     -    boundary, with no error and nothing to suggest that more history\n     -    exists upstream and simply was not fetched.\n     -\n          Add a hint, shown when a walk runs out of parents exactly at a\n          recorded shallow boundary, not merely because the repository happens\n          to be shallow elsewhere. When the revision <rev> is given as a\n     @@ Commit message\n          boundary, and only the remote knows for certain whether deepening\n          would fetch anything new.\n      \n     -    git log -<n> and git log --since only show the hint when the last\n     -    commit they printed is that same recorded boundary. A plain git log\n     -    running out of history, or a git log -<n> or --since that is satisfied\n     -    well before reaching the boundary, stays silent as before. When the\n     -    output is paginated, the hint waits for the pager to close before\n     -    printing, since a full screen pager like less otherwise wipes it from\n     -    the terminal along with the rest of the output the moment it quits.\n     -\n          The advice is threaded through GET_OID_QUIETLY so it is not shown\n          during the internal re-resolution some commands do while building a\n          better error message, which would otherwise print it twice for the\n     -    same failing argument.\n     +    same failing argument. A chained <rev>^^^ resolves one parent at a\n     +    time, and each step re-resolves everything before it through a fresh,\n     +    always non-quiet lookup, so a chained caret landing past a shallow\n     +    boundary printed the hint twice instead of once. Thread the quiet bit\n     +    through that inner lookup too, so it stays quiet exactly when the\n     +    outer resolution asked it to.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     @@ advice.h: enum advice_type {\n       \tADVICE_SPARSE_INDEX_EXPANDED,\n       \tADVICE_STATUS_AHEAD_BEHIND_WARNING,\n      \n     - ## builtin/log.c ##\n     -@@\n     - #include \"commit-reach.h\"\n     - #include \"promisor-remote.h\"\n     - #include \"range-diff.h\"\n     -+#include \"shallow.h\"\n     - #include \"tmp-objdir.h\"\n     - #include \"tree.h\"\n     - #include \"userdiff.h\"\n     -@@ builtin/log.c: static void cmd_log_init(int argc, const char **argv, const char *prefix,\n     - \tcmd_log_init_finish(argc, argv, prefix, rev, opt, cfg);\n     - }\n     - \n     -+static void advise_if_log_stopped_at_shallow_boundary(struct rev_info *rev,\n     -+\t\t\t\t\t\t       struct commit *last_shown)\n     -+{\n     -+\tif (!last_shown)\n     -+\t\treturn;\n     -+\t/* a plain \"git log\" running out of history is expected */\n     -+\tif (rev->max_count < 0 && rev->max_age == (timestamp_t)-1)\n     -+\t\treturn;\n     -+\tif (!is_repository_shallow(the_repository))\n     -+\t\treturn;\n     -+\tif (!commit_is_shallow_boundary(the_repository, &last_shown->object.oid))\n     -+\t\treturn;\n     -+\twait_for_pager();\n     -+\tadvise_if_enabled(ADVICE_SHALLOW_HISTORY,\n     -+\t\t\t   _(\"'%s' stopped at %s because this repository is a shallow\\n\"\n     -+\t\t\t     \"clone, and might have more history upstream that was never fetched.\"),\n     -+\t\t\t   \"git log\",\n     -+\t\t\t   repo_find_unique_abbrev(the_repository,\n     -+\t\t\t\t\t\t    &last_shown->object.oid,\n     -+\t\t\t\t\t\t    DEFAULT_ABBREV));\n     -+}\n     -+\n     - static int cmd_log_walk_no_free(struct rev_info *rev)\n     - {\n     - \tstruct commit *commit;\n     -+\tstruct commit *last_shown = NULL;\n     - \tint saved_nrl = 0;\n     - \tint saved_dcctc = 0;\n     - \tint result;\n     -@@ builtin/log.c: static int cmd_log_walk_no_free(struct rev_info *rev)\n     - \t * retain that state information if replacing rev->diffopt in this loop\n     - \t */\n     - \twhile ((commit = get_revision(rev)) != NULL) {\n     -+\t\tlast_shown = commit;\n     - \t\tif (!log_tree_commit(rev, commit) && rev->max_count >= 0)\n     - \t\t\t/*\n     - \t\t\t * We decremented max_count in get_revision,\n     -@@ builtin/log.c: static int cmd_log_walk_no_free(struct rev_info *rev)\n     - \t\tif (rev->diffopt.degraded_cc_to_c)\n     - \t\t\tsaved_dcctc = 1;\n     - \t}\n     -+\tadvise_if_log_stopped_at_shallow_boundary(rev, last_shown);\n     - \trev->diffopt.degraded_cc_to_c = saved_dcctc;\n     - \trev->diffopt.needed_rename_limit = saved_nrl;\n     - \n     -\n       ## object-name.c ##\n      @@\n       #include \"repo-settings.h\"\n     @@ object-name.c: static int get_oid_basic(struct repository *r, const char *str, i\n       {\n       \tstruct object_id oid;\n       \tenum get_oid_result ret = get_oid_1(r, name, len, &oid,\n     +-\t\t\t\t\t    GET_OID_COMMITTISH);\n     ++\t\t\t\t\t    GET_OID_COMMITTISH |\n     ++\t\t\t\t\t    (lookup_flags & GET_OID_QUIETLY));\n     + \tstruct commit *commit;\n     + \tstruct commit_list *p;\n     + \n      @@ object-name.c: static enum get_oid_result get_parent(struct repository *r,\n       \t\t}\n       \t\tp = p->next;\n     @@ object-name.c: static enum get_oid_result get_parent(struct repository *r,\n       {\n       \tstruct object_id oid;\n       \tstruct commit *commit;\n     + \tint ret;\n     + \n     +-\tret = get_oid_1(r, name, len, &oid, GET_OID_COMMITTISH);\n     ++\tret = get_oid_1(r, name, len, &oid,\n     ++\t\t\tGET_OID_COMMITTISH | (lookup_flags & GET_OID_QUIETLY));\n     + \tif (ret)\n     + \t\treturn ret;\n     + \tcommit = lookup_commit_reference(r, &oid);\n      @@ object-name.c: static enum get_oid_result get_nth_ancestor(struct repository *r,\n       \t\treturn MISSING_OBJECT;\n       \n     @@ t/t1500-rev-parse.sh: test_expect_success 'rev-parse --is-shallow-repository in\n      +\t\t\"git fetch --deepen=1 origin main\"\n      +'\n      +\n     ++test_expect_success 'shallowHistory advice on chained ^ shows the hint only once' '\n     ++\ttest_commit shallow_chain_1 &&\n     ++\tgit clone --no-local --depth=1 --branch main --single-branch \\\n     ++\t\t.git shallow-advice-chain &&\n     ++\ttest_when_finished \"rm -rf shallow-advice-chain\" &&\n     ++\toid=$(git -C shallow-advice-chain rev-parse --short origin/main) &&\n     ++\ttest_must_fail git -C shallow-advice-chain rev-parse origin/main^^ 2>err &&\n     ++\tcheck_shallow_history_advice origin/main \"$oid\" \\\n     ++\t\t\"git fetch --deepen=1 origin main\"\n     ++'\n     ++\n      +test_expect_success 'shallowHistory advice on ^N suggests deepen=1 even for a merge parent' '\n      +\ttest_commit shallow_merge_base &&\n      +\tgit checkout -q -b shallow-merge-side &&\n     @@ t/t1500-rev-parse.sh: test_expect_success 'rev-parse --is-shallow-repository in\n       test_expect_success 'rev-parse --show-object-format in repo' '\n       \ttest_oid algo >expect &&\n       \tgit rev-parse --show-object-format >actual &&\n     -\n     - ## t/t4202-log.sh ##\n     -@@ t/t4202-log.sh: test_expect_success 'log --invert-grep --grep --author' '\n     - \ttest_cmp expect actual\n     - '\n     - \n     -+check_log_shallow_history_advice () {\n     -+\toid=$1 &&\n     -+\tgrep '^hint:' err >actual &&\n     -+\tcat >expect <<-EOF &&\n     -+\thint: 'git log' stopped at $oid because this repository is a shallow\n     -+\thint: clone, and might have more history upstream that was never fetched.\n     -+\thint: Disable this message with \"git config set advice.shallowHistory false\"\n     -+\tEOF\n     -+\ttest_cmp expect actual\n     -+}\n     -+\n     -+test_expect_success 'set up linear history for shallow log advice tests' '\n     -+\tgit checkout --orphan loghist &&\n     -+\ttest_commit loghist_1 &&\n     -+\ttest_commit loghist_2 &&\n     -+\ttest_commit loghist_3 &&\n     -+\ttest_commit loghist_4 &&\n     -+\ttest_commit loghist_5 &&\n     -+\tgit checkout main\n     -+'\n     -+\n     -+test_expect_success 'log -<n> advises when it runs out of history at a shallow boundary' '\n     -+\tgit clone --no-local --depth=2 --branch loghist --single-branch \\\n     -+\t\t.git shallow-log-advice &&\n     -+\ttest_when_finished \"rm -rf shallow-log-advice\" &&\n     -+\toid=$(git -C shallow-log-advice rev-parse --short origin/loghist~1) &&\n     -+\tgit -C shallow-log-advice log -5 --oneline origin/loghist >out 2>err &&\n     -+\tcheck_log_shallow_history_advice \"$oid\"\n     -+'\n     -+\n     -+test_expect_success 'log --since advises when it runs out of history at a shallow boundary' '\n     -+\tgit clone --no-local --depth=2 --branch loghist --single-branch \\\n     -+\t\t.git shallow-log-advice-since &&\n     -+\ttest_when_finished \"rm -rf shallow-log-advice-since\" &&\n     -+\toid=$(git -C shallow-log-advice-since rev-parse --short origin/loghist~1) &&\n     -+\tgit -C shallow-log-advice-since log --since=2000-01-01 --oneline \\\n     -+\t\torigin/loghist >out 2>err &&\n     -+\tcheck_log_shallow_history_advice \"$oid\"\n     -+'\n     -+\n     -+test_expect_success 'log without -<n> or --since does not advise at a shallow boundary' '\n     -+\tgit clone --no-local --depth=1 --branch loghist --single-branch \\\n     -+\t\t.git shallow-log-plain &&\n     -+\ttest_when_finished \"rm -rf shallow-log-plain\" &&\n     -+\tgit -C shallow-log-plain log --oneline origin/loghist >out 2>err &&\n     -+\ttest_grep ! \"^hint:\" err\n     -+'\n     -+\n     -+test_expect_success 'log -<n> does not advise when satisfied within local history' '\n     -+\tgit clone --no-local --depth=5 --branch loghist --single-branch \\\n     -+\t\t.git shallow-log-satisfied &&\n     -+\ttest_when_finished \"rm -rf shallow-log-satisfied\" &&\n     -+\tgit -C shallow-log-satisfied log -2 --oneline origin/loghist >out 2>err &&\n     -+\ttest_grep ! \"^hint:\" err\n     -+'\n     -+\n     -+test_expect_success 'log -<n> shallow history advice can be disabled' '\n     -+\tgit clone --no-local --depth=1 --branch loghist --single-branch \\\n     -+\t\t.git shallow-log-off &&\n     -+\ttest_when_finished \"rm -rf shallow-log-off\" &&\n     -+\tgit -C shallow-log-off -c advice.shallowHistory=false \\\n     -+\t\tlog -5 --oneline origin/loghist >out 2>err &&\n     -+\ttest_grep ! \"^hint:\" err\n     -+'\n     -+\n     -+test_expect_success 'log -<n> does not advise in a non-shallow repository' '\n     -+\ttest_when_finished \"rm -rf non-shallow-log\" &&\n     -+\tgit init -q non-shallow-log &&\n     -+\ttest_commit -C non-shallow-log only-commit &&\n     -+\tgit -C non-shallow-log log -20 --oneline >out 2>err &&\n     -+\ttest_grep ! \"^hint:\" err\n     -+'\n     -+\n     - test_done\n\n\n Documentation/config/advice.adoc |   4 +\n advice.c                         |   1 +\n advice.h                         |   1 +\n object-name.c                    | 131 +++++++++++++++++++++++++++++--\n shallow.c                        |   8 ++\n shallow.h                        |   8 ++\n t/t1500-rev-parse.sh             | 120 ++++++++++++++++++++++++++++\n 7 files changed, 266 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc\nindex 81f80a9274..bb1e508bb6 100644\n--- a/Documentation/config/advice.adoc\n+++ b/Documentation/config/advice.adoc\n@@ -128,6 +128,10 @@ all advice messages.\n \t\tgive directions on how to proceed from the current state.\n \tsequencerInUse::\n \t\tShown when a sequencer command is already in progress.\n+\tshallowHistory::\n+\t\tShown when `<rev>~<n>` cannot resolve enough ancestors of\n+\t\t`<rev>` because history stops at a shallow boundary, to\n+\t\tsuggest fetching more history.\n \tskippedCherryPicks::\n \t\tShown when linkgit:git-rebase[1] skips a commit that has already\n \t\tbeen cherry-picked onto the upstream branch.\ndiff --git a/advice.c b/advice.c\nindex 63bf8b0c5f..e22706ead5 100644\n--- a/advice.c\n+++ b/advice.c\n@@ -80,6 +80,7 @@ static struct {\n \t[ADVICE_RM_HINTS]\t\t\t\t= { \"rmHints\" },\n \t[ADVICE_SEQUENCER_IN_USE]\t\t\t= { \"sequencerInUse\" },\n \t[ADVICE_SET_UPSTREAM_FAILURE]\t\t\t= { \"setUpstreamFailure\" },\n+\t[ADVICE_SHALLOW_HISTORY]\t\t\t= { \"shallowHistory\" },\n \t[ADVICE_SKIPPED_CHERRY_PICKS]\t\t\t= { \"skippedCherryPicks\" },\n \t[ADVICE_SPARSE_INDEX_EXPANDED]\t\t\t= { \"sparseIndexExpanded\" },\n \t[ADVICE_STATUS_AHEAD_BEHIND_WARNING]\t\t= { \"statusAheadBehindWarning\" },\ndiff --git a/advice.h b/advice.h\nindex 66f6cd6a77..e80e3e82cc 100644\n--- a/advice.h\n+++ b/advice.h\n@@ -47,6 +47,7 @@ enum advice_type {\n \tADVICE_RM_HINTS,\n \tADVICE_SEQUENCER_IN_USE,\n \tADVICE_SET_UPSTREAM_FAILURE,\n+\tADVICE_SHALLOW_HISTORY,\n \tADVICE_SKIPPED_CHERRY_PICKS,\n \tADVICE_SPARSE_INDEX_EXPANDED,\n \tADVICE_STATUS_AHEAD_BEHIND_WARNING,\ndiff --git a/object-name.c b/object-name.c\nindex 4eda8c8eac..7376c24601 100644\n--- a/object-name.c\n+++ b/object-name.c\n@@ -22,6 +22,7 @@\n #include \"repo-settings.h\"\n #include \"repository.h\"\n #include \"setup.h\"\n+#include \"shallow.h\"\n #include \"midx.h\"\n #include \"commit-reach.h\"\n #include \"date.h\"\n@@ -824,13 +825,120 @@ static int get_oid_basic(struct repository *r, const char *str, int len,\n \treturn 0;\n }\n \n+struct remote_tracking_search {\n+\tchar *dst;\n+\tchar *remote_name;\n+\tchar *branch_name;\n+};\n+\n+static int search_remote_tracking(struct remote *remote, void *cb_data)\n+{\n+\tstruct remote_tracking_search *search = cb_data;\n+\tstruct refspec_item query;\n+\n+\tmemset(&query, 0, sizeof(query));\n+\tquery.dst = search->dst;\n+\tif (remote_find_tracking(remote, &query))\n+\t\treturn 0;\n+\tsearch->remote_name = xstrdup(remote->name);\n+\tsearch->branch_name = query.src;\n+\treturn 1;\n+}\n+\n+/*\n+ * If \"name\" resolves to a remote-tracking ref, find which configured\n+ * remote it actually belongs to and what branch of that remote's it is,\n+ * by reverse mapping through each remote's real fetch refspec instead\n+ * of assuming the common \"refs/remotes/<remote>/<branch>\" layout,\n+ * since a remote's refspec need not put its branches there at all. On a\n+ * match, fill \"remote_out\" and \"branch_out\" with newly allocated copies\n+ * and return true. Otherwise leave them untouched and return false.\n+ */\n+static bool parse_remote_tracking_name(struct repository *r,\n+\t\t\t\t       const char *name, int namelen,\n+\t\t\t\t       char **remote_out, char **branch_out)\n+{\n+\tstruct object_id oid;\n+\tchar *real_ref = NULL;\n+\tstruct remote_tracking_search search = { 0 };\n+\tconst char *branch_name;\n+\n+\tif (!repo_dwim_ref(r, name, namelen, &oid, &real_ref, 0) || !real_ref)\n+\t\treturn false;\n+\n+\tsearch.dst = real_ref;\n+\tif (!for_each_remote(search_remote_tracking, &search)) {\n+\t\tfree(real_ref);\n+\t\treturn false;\n+\t}\n+\tfree(real_ref);\n+\n+\tbranch_name = search.branch_name;\n+\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n+\t*remote_out = search.remote_name;\n+\t*branch_out = xstrdup(branch_name);\n+\tfree(search.branch_name);\n+\treturn true;\n+}\n+\n+/*\n+ * When a \"name~<n>\" or \"name^<n>\" walk runs out of parents at \"commit\",\n+ * and that is because \"commit\" is where this shallow repository's history\n+ * was cut off (rather than commit being a real root commit), let the\n+ * user know that fetching more history might be what they are after.\n+ *\n+ * \"suggested_depth\" is the --deepen value to recommend. For \"name^<n>\"\n+ * this is always 1: deepening by one generation fetches \"commit\"'s real\n+ * parent list in full, whatever it turns out to contain, regardless of\n+ * which parent index <n> asked for.\n+ */\n+static void advise_if_shallow_cutoff(struct repository *r,\n+\t\t\t\t     const char *name, int namelen,\n+\t\t\t\t     struct commit *commit,\n+\t\t\t\t     unsigned lookup_flags,\n+\t\t\t\t     int suggested_depth)\n+{\n+\tchar *remote = NULL, *branch = NULL;\n+\tstruct strbuf cmd = STRBUF_INIT;\n+\n+\tif (lookup_flags & GET_OID_QUIETLY)\n+\t\treturn;\n+\tif (!is_repository_shallow(r))\n+\t\treturn;\n+\tif (!commit_is_shallow_boundary(r, &commit->object.oid))\n+\t\treturn;\n+\n+\tif (parse_remote_tracking_name(r, name, namelen, &remote, &branch))\n+\t\tstrbuf_addf(&cmd, \"git fetch --deepen=%d %s %s\",\n+\t\t\t    suggested_depth, remote, branch);\n+\telse\n+\t\tstrbuf_addf(&cmd, \"git fetch --deepen=%d <remote> <branch>\",\n+\t\t\t    suggested_depth);\n+\tfree(remote);\n+\tfree(branch);\n+\n+\tadvise_if_enabled(ADVICE_SHALLOW_HISTORY,\n+\t\t\t   _(\"'%.*s' does not have that many ancestors locally.\\n\"\n+\t\t\t     \"History stops at %s because this repository is a\\n\"\n+\t\t\t     \"shallow clone, and might have more history upstream.\\n\"\n+\t\t\t     \"To check, try:\\n\"\n+\t\t\t     \"\\n\"\n+\t\t\t     \"  %s\"),\n+\t\t\t   namelen, name,\n+\t\t\t   repo_find_unique_abbrev(r, &commit->object.oid, DEFAULT_ABBREV),\n+\t\t\t   cmd.buf);\n+\tstrbuf_release(&cmd);\n+}\n+\n static enum get_oid_result get_parent(struct repository *r,\n \t\t\t\t      const char *name, int len,\n-\t\t\t\t      struct object_id *result, int idx)\n+\t\t\t\t      struct object_id *result, int idx,\n+\t\t\t\t      unsigned lookup_flags)\n {\n \tstruct object_id oid;\n \tenum get_oid_result ret = get_oid_1(r, name, len, &oid,\n-\t\t\t\t\t    GET_OID_COMMITTISH);\n+\t\t\t\t\t    GET_OID_COMMITTISH |\n+\t\t\t\t\t    (lookup_flags & GET_OID_QUIETLY));\n \tstruct commit *commit;\n \tstruct commit_list *p;\n \n@@ -851,19 +959,22 @@ static enum get_oid_result get_parent(struct repository *r,\n \t\t}\n \t\tp = p->next;\n \t}\n+\tadvise_if_shallow_cutoff(r, name, len, commit, lookup_flags, 1);\n \treturn MISSING_OBJECT;\n }\n \n static enum get_oid_result get_nth_ancestor(struct repository *r,\n \t\t\t\t\t    const char *name, int len,\n \t\t\t\t\t    struct object_id *result,\n-\t\t\t\t\t    int generation)\n+\t\t\t\t\t    int generation,\n+\t\t\t\t\t    unsigned lookup_flags)\n {\n \tstruct object_id oid;\n \tstruct commit *commit;\n \tint ret;\n \n-\tret = get_oid_1(r, name, len, &oid, GET_OID_COMMITTISH);\n+\tret = get_oid_1(r, name, len, &oid,\n+\t\t\tGET_OID_COMMITTISH | (lookup_flags & GET_OID_QUIETLY));\n \tif (ret)\n \t\treturn ret;\n \tcommit = lookup_commit_reference(r, &oid);\n@@ -871,8 +982,14 @@ static enum get_oid_result get_nth_ancestor(struct repository *r,\n \t\treturn MISSING_OBJECT;\n \n \twhile (generation--) {\n-\t\tif (repo_parse_commit(r, commit) || !commit->parents)\n+\t\tif (repo_parse_commit(r, commit))\n+\t\t\treturn MISSING_OBJECT;\n+\t\tif (!commit->parents) {\n+\t\t\t/* Remaining \"generation\" plus this failed step is the actual gap. */\n+\t\t\tadvise_if_shallow_cutoff(r, name, len, commit,\n+\t\t\t\t\t\t lookup_flags, generation + 1);\n \t\t\treturn MISSING_OBJECT;\n+\t\t}\n \t\tcommit = commit->parents->item;\n \t}\n \toidcpy(result, &commit->object.oid);\n@@ -1119,9 +1236,9 @@ static enum get_oid_result get_oid_1(struct repository *r,\n \t\telse if (num > INT_MAX)\n \t\t\treturn MISSING_OBJECT;\n \t\tif (has_suffix == '^')\n-\t\t\treturn get_parent(r, name, len1, oid, num);\n+\t\t\treturn get_parent(r, name, len1, oid, num, lookup_flags);\n \t\t/* else if (has_suffix == '~') -- goes without saying */\n-\t\treturn get_nth_ancestor(r, name, len1, oid, num);\n+\t\treturn get_nth_ancestor(r, name, len1, oid, num, lookup_flags);\n \t}\n \n \tret = peel_onion(r, name, len, oid, lookup_flags);\ndiff --git a/shallow.c b/shallow.c\nindex 8e244a5669..2e0cf09f32 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -92,6 +92,14 @@ int is_repository_shallow(struct repository *r)\n \treturn r->parsed_objects->is_shallow;\n }\n \n+int commit_is_shallow_boundary(struct repository *r,\n+\t\t\t       const struct object_id *oid)\n+{\n+\tstruct commit_graft *graft = lookup_commit_graft(r, oid);\n+\n+\treturn graft && graft->nr_parent == -1;\n+}\n+\n static void reset_repository_shallow(struct repository *r)\n {\n \tr->parsed_objects->is_shallow = -1;\ndiff --git a/shallow.h b/shallow.h\nindex 6a64db42c9..66c9e723f7 100644\n--- a/shallow.h\n+++ b/shallow.h\n@@ -15,6 +15,14 @@ int register_shallow(struct repository *r, const struct object_id *oid);\n int unregister_shallow(const struct object_id *oid);\n int is_repository_shallow(struct repository *r);\n \n+/*\n+ * True if \"oid\" is where this shallow repository's history was cut off,\n+ * i.e. a shallow-graft commit with no parents recorded locally, as\n+ * opposed to a commit that is actually a root.\n+ */\n+int commit_is_shallow_boundary(struct repository *r,\n+\t\t\t       const struct object_id *oid);\n+\n /*\n  * Lock for updating the $GIT_DIR/shallow file.\n  *\ndiff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh\nindex 4174ca40c3..fbb6e6e582 100755\n--- a/t/t1500-rev-parse.sh\n+++ b/t/t1500-rev-parse.sh\n@@ -193,6 +193,126 @@ test_expect_success 'rev-parse --is-shallow-repository in non-shallow repo' '\n \ttest_cmp expect actual\n '\n \n+check_shallow_history_advice () {\n+\tname=$1 oid=$2 cmd=$3 &&\n+\tgrep '^hint:' err >actual &&\n+\tcat >expect <<-EOF &&\n+\thint: '$name' does not have that many ancestors locally.\n+\thint: History stops at $oid because this repository is a\n+\thint: shallow clone, and might have more history upstream.\n+\thint: To check, try:\n+\thint:\n+\thint:   $cmd\n+\thint: Disable this message with \"git config set advice.shallowHistory false\"\n+\tEOF\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'shallowHistory advice on ~N beyond shallow boundary' '\n+\ttest_commit shallow_advice_1 &&\n+\ttest_commit shallow_advice_2 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice &&\n+\ttest_when_finished \"rm -rf shallow-advice\" &&\n+\toid=$(git -C shallow-advice rev-parse --short origin/main) &&\n+\ttest_must_fail git -C shallow-advice rev-parse origin/main~1 2>err &&\n+\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\"git fetch --deepen=1 origin main\"\n+'\n+\n+test_expect_success 'shallowHistory advice accounts for depth already present' '\n+\ttest_commit shallow_partial_1 &&\n+\ttest_commit shallow_partial_2 &&\n+\ttest_commit shallow_partial_3 &&\n+\ttest_commit shallow_partial_4 &&\n+\ttest_commit shallow_partial_5 &&\n+\ttest_commit shallow_partial_6 &&\n+\tgit clone --no-local --depth=3 --branch main --single-branch \\\n+\t\t.git shallow-advice-partial &&\n+\ttest_when_finished \"rm -rf shallow-advice-partial\" &&\n+\t(\n+\t\tcd shallow-advice-partial &&\n+\t\toid=$(git rev-parse --short origin/main~2) &&\n+\t\ttest_must_fail git rev-parse origin/main~5 2>err &&\n+\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\t\"git fetch --deepen=3 origin main\" &&\n+\t\tgit fetch --deepen=3 origin &&\n+\t\tgit rev-parse origin/main~5 &&\n+\t\ttest_must_fail git rev-parse origin/main~6\n+\t)\n+'\n+\n+test_expect_success 'shallowHistory advice on ^N (first parent) beyond shallow boundary' '\n+\ttest_commit shallow_caret_1 &&\n+\ttest_commit shallow_caret_2 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-caret &&\n+\ttest_when_finished \"rm -rf shallow-advice-caret\" &&\n+\toid=$(git -C shallow-advice-caret rev-parse --short origin/main) &&\n+\ttest_must_fail git -C shallow-advice-caret rev-parse origin/main^1 2>err &&\n+\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\"git fetch --deepen=1 origin main\"\n+'\n+\n+test_expect_success 'shallowHistory advice on chained ^ shows the hint only once' '\n+\ttest_commit shallow_chain_1 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-chain &&\n+\ttest_when_finished \"rm -rf shallow-advice-chain\" &&\n+\toid=$(git -C shallow-advice-chain rev-parse --short origin/main) &&\n+\ttest_must_fail git -C shallow-advice-chain rev-parse origin/main^^ 2>err &&\n+\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\"git fetch --deepen=1 origin main\"\n+'\n+\n+test_expect_success 'shallowHistory advice on ^N suggests deepen=1 even for a merge parent' '\n+\ttest_commit shallow_merge_base &&\n+\tgit checkout -q -b shallow-merge-side &&\n+\ttest_commit shallow_merge_side1 &&\n+\tgit checkout -q main &&\n+\ttest_commit shallow_merge_main1 &&\n+\tgit merge -q --no-ff shallow-merge-side -m \"shallow merge commit\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-merge &&\n+\ttest_when_finished \"rm -rf shallow-advice-merge\" &&\n+\t(\n+\t\tcd shallow-advice-merge &&\n+\t\toid=$(git rev-parse --short origin/main) &&\n+\t\ttest_must_fail git rev-parse origin/main^2 2>err &&\n+\t\tcheck_shallow_history_advice origin/main \"$oid\" \\\n+\t\t\t\"git fetch --deepen=1 origin main\" &&\n+\t\tgit fetch -q --deepen=1 origin &&\n+\t\tgit rev-parse origin/main^1 &&\n+\t\tgit rev-parse origin/main^2\n+\t)\n+'\n+\n+test_expect_success 'shallowHistory advice can be disabled' '\n+\ttest_commit shallow_off_1 &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git shallow-advice-off &&\n+\ttest_when_finished \"rm -rf shallow-advice-off\" &&\n+\ttest_must_fail git -C shallow-advice-off \\\n+\t\t-c advice.shallowHistory=false rev-parse origin/main~1 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n+test_expect_success 'shallowHistory advice not shown for a non-shallow repository' '\n+\ttest_must_fail git rev-parse HEAD~100000 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n+test_expect_success 'shallowHistory advice not shown when resolution succeeds' '\n+\ttest_commit shallow_ok_1 &&\n+\ttest_commit shallow_ok_2 &&\n+\ttest_commit shallow_ok_3 &&\n+\tgit clone --no-local --depth=3 --branch main --single-branch \\\n+\t\t.git shallow-advice-ok &&\n+\ttest_when_finished \"rm -rf shallow-advice-ok\" &&\n+\tgit -C shallow-advice-ok rev-parse origin/main~1 >actual 2>err &&\n+\ttest_grep ! \"^hint:\" err\n+'\n+\n test_expect_success 'rev-parse --show-object-format in repo' '\n \ttest_oid algo >expect &&\n \tgit rev-parse --show-object-format >actual &&\n\nbase-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7\n-- \ngitgitgadget\n"}]}