{"thread":{"id":"66616","subject":"[PATCH] sequencer: refuse to revert shallow boundary commits by default","startedAt":"2026-10-21T10:09:41Z","lastAt":"2026-11-04T14:22:21Z","messageCount":11,"participants":["Nakul V S via GitGitGadget","Phillip Wood","Junio C Hamano","Nakul V S"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"555707","messageId":"pull.2273.git.1792577381142.gitgitgadget@gmail.com","threadId":"66616","inReplyTo":null,"subject":"[PATCH] sequencer: refuse to revert shallow boundary commits by default","fromName":"Nakul V S via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-21T10:09:41Z","receivedAt":"2026-10-21T10:09:41Z","isPatch":true,"sender":{"key":"name:Nakul V S","avatar":null},"body":"From: Nakul V S <vsnakul@gmail.com>\n\nWhen a repository is cloned with --depth=1, Git records the boundary\ncommit in .git/shallow and rewrites its parent links via the graft\nmechanism, making it appear as a root commit. Reverting such a commit\nwill therefore delete all tracked files, which is almost always\nunintended by the user.\n\nAdd a check in do_pick_commit() that detects when the target commit is\na shallow boundary (nr_parent < 0 in the commit graft) and refuses the\noperation by default. A new --allow-shallow-boundary flag is provided\nfor the rare cases where the user genuinely intends this behavior.\n\nSigned-off-by: Nakul V S <vsnakul@gmail.com>\n---\n    sequencer: refuse to revert shallow boundary commits by default\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2273%2Fvsnakul%2Fsequencer-shallow-boundary-check-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2273/vsnakul/sequencer-shallow-boundary-check-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2273\n\n Documentation/git-revert.adoc | 12 +++++++++++-\n builtin/revert.c              |  4 +++-\n sequencer.c                   | 14 ++++++++++++++\n sequencer.h                   |  1 +\n t/t3501-revert-cherry-pick.sh | 25 +++++++++++++++++++++++++\n 5 files changed, 54 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-revert.adoc b/Documentation/git-revert.adoc\nindex ffba365e63..4395df62e2 100644\n--- a/Documentation/git-revert.adoc\n+++ b/Documentation/git-revert.adoc\n@@ -8,7 +8,7 @@ git-revert - Revert some existing commits\n SYNOPSIS\n --------\n [verse]\n-'git revert' [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] <commit>...\n+'git revert' [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] [--allow-shallow-boundary] <commit>...\n 'git revert' (--continue | --skip | --abort | --quit)\n \n DESCRIPTION\n@@ -122,6 +122,16 @@ include::rerere-options.adoc[]\n \tconfiguration variable can be used to enable this option by\n \tdefault.\n \n+--allow-shallow-boundary::\n+\tBy default, 'git revert' refuses to revert a commit that is a\n+\tshallow boundary (i.e. a commit listed in `.git/shallow`),\n+\tbecause its true parent history is absent from the clone and\n+\treverting it would remove all tracked files. This option\n+\toverrides that safeguard and allows the revert to proceed.\n++\n+To restore full history before reverting, use\n+`git fetch --unshallow` instead.\n+\n \n SEQUENCER SUBCOMMANDS\n ---------------------\ndiff --git a/builtin/revert.c b/builtin/revert.c\nindex 52100a20cb..f27397fdec 100644\n--- a/builtin/revert.c\n+++ b/builtin/revert.c\n@@ -24,7 +24,7 @@\n  */\n \n static const char * const revert_usage[] = {\n-\tN_(\"git revert [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] <commit>...\"),\n+\tN_(\"git revert [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] [--allow-shallow-boundary] <commit>...\"),\n \tN_(\"git revert (--continue | --skip | --abort | --quit)\"),\n \tNULL\n };\n@@ -128,6 +128,8 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,\n \t\tOPT_BOOL('e', \"edit\", &opts->edit, N_(\"edit the commit message\")),\n \t\tOPT_NOOP_NOARG('r', NULL),\n \t\tOPT_BOOL('s', \"signoff\", &opts->signoff, N_(\"add a Signed-off-by trailer\")),\n+\t\tOPT_BOOL(0, \"allow-shallow-boundary\", &opts->allow_shallow_boundary,\n+\t\t\t N_(\"allow reverting a shallow boundary commit\")),\n \t\tOPT_CALLBACK('m', \"mainline\", opts, N_(\"parent-number\"),\n \t\t\t     N_(\"select mainline parent\"), option_parse_m),\n \t\tOPT_RERERE_AUTOUPDATE(&opts->allow_rerere_auto),\ndiff --git a/sequencer.c b/sequencer.c\nindex efbfd1315b..6de59bf6aa 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -49,6 +49,7 @@\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n #include \"branch.h\"\n+#include \"shallow.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -2366,6 +2367,19 @@ static enum pick_result do_pick_commit(struct repository *r,\n \t}\n \tdiscard_index(r->index);\n \n+\tif (is_repository_shallow(r)) {\n+\t\tstruct commit_graft *graft = lookup_commit_graft(r, &commit->object.oid);\n+\t\tif (graft && graft->nr_parent < 0 && !opts->allow_shallow_boundary) {\n+\t\t\treturn error(_(\"%s: commit %s is a shallow boundary commit.\\n\"\n+\t\t\t\t       \"Reverting it will remove all tracked files because its\\n\"\n+\t\t\t\t       \"true parent history is not present in this clone.\\n\"\n+\t\t\t\t       \"Use --allow-shallow-boundary to override, or\\n\"\n+\t\t\t\t       \"'git fetch --unshallow' to restore full history first.\"),\n+\t\t\t\t     command_to_string(command),\n+\t\t\t\t     oid_to_hex(&commit->object.oid));\n+\t\t}\n+\t}\n+\n \tif (!commit->parents)\n \t\tparent = NULL;\n \telse if (commit->parents->next) {\ndiff --git a/sequencer.h b/sequencer.h\nindex a5d12a1070..f334bc5357 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -57,6 +57,7 @@ struct replay_opts {\n \tint committer_date_is_author_date;\n \tint ignore_date;\n \tint commit_use_reference;\n+\tint allow_shallow_boundary;\n \n \tstruct strvec trailer_args;\n \ndiff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh\nindex 939e7a16a6..508464004f 100755\n--- a/t/t3501-revert-cherry-pick.sh\n+++ b/t/t3501-revert-cherry-pick.sh\n@@ -256,4 +256,29 @@ test_expect_success 'cherry-pick is unaware of --reference (for now)' '\n \ttest_grep \"^usage: git cherry-pick\" actual\n '\n \n+# Tests for shallow boundary safeguard\n+\n+test_expect_success 'setup shallow boundary tests' '\n+\tgit init src &&\n+\ttest_commit -C src A &&\n+\ttest_commit -C src B &&\n+\tgit clone --no-local --depth=1 src shallow-repo\n+'\n+\n+test_expect_success 'revert of shallow boundary commit is refused by default' '\n+\ttest_must_fail git -C shallow-repo revert --no-edit HEAD 2>err &&\n+\ttest_grep \"shallow boundary commit\" err\n+'\n+\n+test_expect_success 'revert of shallow boundary succeeds with --allow-shallow-boundary' '\n+\tgit -C shallow-repo revert --no-edit --allow-shallow-boundary HEAD\n+'\n+\n+test_expect_success 'revert in non-shallow repo is unaffected' '\n+\tgit init non-shallow &&\n+\ttest_commit -C non-shallow A &&\n+\ttest_commit -C non-shallow B &&\n+\tgit -C non-shallow revert --no-edit HEAD\n+'\n+\n test_done\n\nbase-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926\n-- \ngitgitgadget\n\n"},{"id":"555748","messageId":"7179f371-a709-476f-8e94-807d7b62ce6e@gmail.com","threadId":"66616","inReplyTo":"pull.2273.git.1792577381142.gitgitgadget@gmail.com","subject":"Re: [PATCH] sequencer: refuse to revert shallow boundary commits by default","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-21T15:20:42Z","receivedAt":"2026-10-21T15:20:42Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Nakul\n\nOn 21/10/2026 11:09, Nakul V S via GitGitGadget wrote:\n> From: Nakul V S <vsnakul@gmail.com>\n> > When a repository is cloned with --depth=1, Git records the boundary\n> commit in .git/shallow and rewrites its parent links via the graft\n> mechanism, making it appear as a root commit. Reverting such a commit\n> will therefore delete all tracked files, which is almost always\n> unintended by the user.\n> > Add a check in do_pick_commit() that detects when the target commit is\n> a shallow boundary (nr_parent < 0 in the commit graft) and refuses the\n> operation by default. A new --allow-shallow-boundary flag is provided\n> for the rare cases where the user genuinely intends this behavior.\n\nThanks for working on this, it is a useful and timely improvement as there was some discussion about improving the behavior of git with regard to shallow boundaries at the recent git merge conference. I wonder if it would be better to refuse to cherry-pick and rebase a shallow boundary commit as well. Do you know of a sensible use for reverting a shallow boundary or is the new option added out of caution in case someone wants it?\n\n> diff --git a/builtin/revert.c b/builtin/revert.c\n> index 52100a20cb..f27397fdec 100644\n> --- a/builtin/revert.c\n> +++ b/builtin/revert.c\n> @@ -24,7 +24,7 @@\n>    */\n>   >   static const char * const revert_usage[] = {\n> -\tN_(\"git revert [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] <commit>...\"),\n> +\tN_(\"git revert [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] [--allow-shallow-boundary] <commit>...\"),\n\nFor such a rarely used option I don't think we need to add it to the synopsis (we already omit some other options like --rerere-autoupdate)\n\n> diff --git a/sequencer.c b/sequencer.c\n> index efbfd1315b..6de59bf6aa 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -2366,6 +2367,19 @@ static enum pick_result do_pick_commit(struct repository *r,\n>   \t}\n>   \tdiscard_index(r->index);\n>   > +\tif (is_repository_shallow(r)) {\n> +\t\tstruct commit_graft *graft = lookup_commit_graft(r, &commit->object.oid);\n> +\t\tif (graft && graft->nr_parent < 0 && !opts->allow_shallow_boundary) {\n\nIt would be good to extract this into a helper function so that we don't have to copy the code to detect a shallow boundary each time we need to detect one.\n\nThanks\n\nPhillip\n\n> +\t\t\treturn error(_(\"%s: commit %s is a shallow boundary commit.\\n\"\n> +\t\t\t\t       \"Reverting it will remove all tracked files because its\\n\"\n> +\t\t\t\t       \"true parent history is not present in this clone.\\n\"\n> +\t\t\t\t       \"Use --allow-shallow-boundary to override, or\\n\"\n> +\t\t\t\t       \"'git fetch --unshallow' to restore full history first.\"),\n> +\t\t\t\t     command_to_string(command),\n> +\t\t\t\t     oid_to_hex(&commit->object.oid));\n> +\t\t}\n> +\t}\n> +\n>   \tif (!commit->parents)\n>   \t\tparent = NULL;\n>   \telse if (commit->parents->next) {\n> diff --git a/sequencer.h b/sequencer.h\n> index a5d12a1070..f334bc5357 100644\n> --- a/sequencer.h\n> +++ b/sequencer.h\n> @@ -57,6 +57,7 @@ struct replay_opts {\n>   \tint committer_date_is_author_date;\n>   \tint ignore_date;\n>   \tint commit_use_reference;\n> +\tint allow_shallow_boundary;\n>   >   \tstruct strvec trailer_args;\n>   > diff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh\n> index 939e7a16a6..508464004f 100755\n> --- a/t/t3501-revert-cherry-pick.sh\n> +++ b/t/t3501-revert-cherry-pick.sh\n> @@ -256,4 +256,29 @@ test_expect_success 'cherry-pick is unaware of --reference (for now)' '\n>   \ttest_grep \"^usage: git cherry-pick\" actual\n>   '\n>   > +# Tests for shallow boundary safeguard\n> +\n> +test_expect_success 'setup shallow boundary tests' '\n> +\tgit init src &&\n> +\ttest_commit -C src A &&\n> +\ttest_commit -C src B &&\n> +\tgit clone --no-local --depth=1 src shallow-repo\n> +'\n> +\n> +test_expect_success 'revert of shallow boundary commit is refused by default' '\n> +\ttest_must_fail git -C shallow-repo revert --no-edit HEAD 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err\n> +'\n> +\n> +test_expect_success 'revert of shallow boundary succeeds with --allow-shallow-boundary' '\n> +\tgit -C shallow-repo revert --no-edit --allow-shallow-boundary HEAD\n> +'\n> +\n> +test_expect_success 'revert in non-shallow repo is unaffected' '\n> +\tgit init non-shallow &&\n> +\ttest_commit -C non-shallow A &&\n> +\ttest_commit -C non-shallow B &&\n> +\tgit -C non-shallow revert --no-edit HEAD\n> +'\n> +\n>   test_done\n> > base-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926\n\n\n"},{"id":"555782","messageId":"xmqqqzhina26.fsf@gitster.g","threadId":"66616","inReplyTo":"7179f371-a709-476f-8e94-807d7b62ce6e@gmail.com","subject":"Re: [PATCH] sequencer: refuse to revert shallow boundary commits by default","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-21T19:11:13Z","receivedAt":"2026-10-21T19:11:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Hi Nakul\n>\n> On 21/10/2026 11:09, Nakul V S via GitGitGadget wrote:\n>> From: Nakul V S <vsnakul@gmail.com>\n>> \n>> When a repository is cloned with --depth=1, Git records the boundary\n>> commit in .git/shallow and rewrites its parent links via the graft\n>> mechanism, making it appear as a root commit. Reverting such a commit\n>> will therefore delete all tracked files, which is almost always\n>> unintended by the user.\n>> \n>> Add a check in do_pick_commit() that detects when the target commit is\n>> a shallow boundary (nr_parent < 0 in the commit graft) and refuses the\n>> operation by default. A new --allow-shallow-boundary flag is provided\n>> for the rare cases where the user genuinely intends this behavior.\n>\n> Thanks for working on this, it is a useful and timely improvement as \n> there was some discussion about improving the behavior of git with \n> regard to shallow boundaries at the recent git merge conference. I \n> wonder if it would be better to refuse to cherry-pick and rebase a \n> shallow boundary commit as well. Do you know of a sensible use for \n> reverting a shallow boundary or is the new option added out of caution \n> in case someone wants it?\n\nVery good point.\n\nThe next natural question is where the modification should go if we\nwant to make sure we protect these shallow boundaries not just from\nrevert but also from \"cherry-pick\", \"rebase\", and possibly \"commit\n--amend\".  Is sequencer.c:do_pick_commit() the right place to cover\nall of them?  If not, would it make sense to have a helper function\nthat different code paths to protect shallow boundaries from these\noperations can call, and add the first call to that helper funcion\nto the location this patch adds new code to?\n\nThanks.\n\n"},{"id":"556308","messageId":"235e7dcd-2d29-47e4-b68f-9b02f80085dd@gmail.com","threadId":"66616","inReplyTo":"xmqqqzhina26.fsf@gitster.g","subject":"Re: [PATCH] sequencer: refuse to revert shallow boundary commits by default","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-26T10:58:44Z","receivedAt":"2026-10-26T10:58:44Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 21/10/2026 20:11, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>> On 21/10/2026 11:09, Nakul V S via GitGitGadget wrote:\n>>\n>> Thanks for working on this, it is a useful and timely improvement as\n>> there was some discussion about improving the behavior of git with\n>> regard to shallow boundaries at the recent git merge conference. I\n>> wonder if it would be better to refuse to cherry-pick and rebase a\n>> shallow boundary commit as well. Do you know of a sensible use for\n>> reverting a shallow boundary or is the new option added out of caution\n>> in case someone wants it?\n> > Very good point.\n> > The next natural question is where the modification should go if we\n> want to make sure we protect these shallow boundaries not just from\n> revert but also from \"cherry-pick\", \"rebase\", and possibly \"commit\n> --amend\".  Is sequencer.c:do_pick_commit() the right place to cover\n> all of them?  If not, would it make sense to have a helper function\n> that different code paths to protect shallow boundaries from these\n> operations can call, and add the first call to that helper funcion\n> to the location this patch adds new code to?\n\nFrom the user's point of view, I think do_pick_commit() is a bit late to error out. It would be nicer to refuse to add a shallow commit to the todo list in the first place which means we'd need a helper function that's called from sequencer_make_script(), make_script_with_merges(), single_pick() and sequencer_pick_revisions(). That is a bit of a pain but would make for a better UI.\n\nThanks\n\nPhillip\n\n\n"},{"id":"557022","messageId":"pull.2273.v2.git.1793455522168.gitgitgadget@gmail.com","threadId":"66616","inReplyTo":"pull.2273.git.1792577381142.gitgitgadget@gmail.com","subject":"[PATCH v2] sequencer: refuse to pick or revert shallow boundary commits","fromName":"Nakul V S via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-31T14:05:22Z","receivedAt":"2026-10-31T14:05:22Z","isPatch":true,"sender":{"key":"name:Nakul V S","avatar":null},"body":"From: Nakul V S <vsnakul@gmail.com>\n\nIn a shallow repository, the commits at the edge of the downloaded\nhistory are recorded in $GIT_DIR/shallow and have their parents hidden\nfrom the revision machinery.  Such a \"shallow boundary\" commit looks\nlike a root commit that added every file in its tree at once.\n\nReverting it therefore removes every tracked file, and cherry-picking\nor rebasing it adds all of them again.  In a \"git clone --depth=1\" this\nis the only commit there is, so \"git revert HEAD\" empties the working\ntree, which is almost never what the user meant.\n\nRefuse to do so by default for \"revert\", \"cherry-pick\" and \"rebase\".\nFor \"revert\" and \"cherry-pick\" only, add --allow-shallow-boundary to\noverride the check; it is remembered across --continue.  \"rebase\"\nalways refuses, and the error message suggests \"git fetch --unshallow\"\ninstead.\n\nThe check is made when the list of commits to replay is built, not when\neach commit is picked, so a command that is going to be refused does so\nbefore it has done anything: no commit is created, HEAD and the working\ntree are untouched, and no sequencer or rebase state is left behind for\nthe user to clean up.\n\nThe \"apply\" backend of \"rebase\" does not go through the sequencer, so\nit checks the commits \"git format-patch\" is about to turn into patches\nbefore it rewinds HEAD.  This is skipped altogether in a repository\nthat is not shallow.\n\nA \"pick\" or \"revert\" line for a shallow boundary commit that the user\ntypes into the todo list by hand is not checked, as the check is not\nmade again after the todo list has been edited.  \"git commit --amend\"\nis also left alone for now.\n\nSigned-off-by: Nakul V S <vsnakul@gmail.com>\n---\n    sequencer: refuse to pick or revert shallow boundary commits by default\n    \n    Changes since v1:\n    \n     * Also refuse cherry-pick and rebase (merge, interactive and --apply\n       backends), as suggested by Phillip.\n     * Check when the list of commits is built rather than in\n       do_pick_commit(), so a refused command leaves no state behind. Uses a\n       single shared helper.\n     * --allow-shallow-boundary is now accepted by cherry-pick as well, and\n       is saved across --continue.\n     * Dropped the option from the revert synopsis.\n     * rebase --autostash restores the user's changes if the rebase is\n       refused.\n     * Rebased onto current master.\n    \n    Not covered here: git commit --amend, and pick/revert lines typed into\n    the rebase todo by hand.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2273%2Fvsnakul%2Fsequencer-shallow-boundary-check-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2273/vsnakul/sequencer-shallow-boundary-check-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2273\n\nRange-diff vs v1:\n\n 1:  526b96f6ba ! 1:  fef8178d4d sequencer: refuse to revert shallow boundary commits by default\n     @@ Metadata\n      Author: Nakul V S <vsnakul@gmail.com>\n      \n       ## Commit message ##\n     -    sequencer: refuse to revert shallow boundary commits by default\n     +    sequencer: refuse to pick or revert shallow boundary commits\n      \n     -    When a repository is cloned with --depth=1, Git records the boundary\n     -    commit in .git/shallow and rewrites its parent links via the graft\n     -    mechanism, making it appear as a root commit. Reverting such a commit\n     -    will therefore delete all tracked files, which is almost always\n     -    unintended by the user.\n     +    In a shallow repository, the commits at the edge of the downloaded\n     +    history are recorded in $GIT_DIR/shallow and have their parents hidden\n     +    from the revision machinery.  Such a \"shallow boundary\" commit looks\n     +    like a root commit that added every file in its tree at once.\n      \n     -    Add a check in do_pick_commit() that detects when the target commit is\n     -    a shallow boundary (nr_parent < 0 in the commit graft) and refuses the\n     -    operation by default. A new --allow-shallow-boundary flag is provided\n     -    for the rare cases where the user genuinely intends this behavior.\n     +    Reverting it therefore removes every tracked file, and cherry-picking\n     +    or rebasing it adds all of them again.  In a \"git clone --depth=1\" this\n     +    is the only commit there is, so \"git revert HEAD\" empties the working\n     +    tree, which is almost never what the user meant.\n     +\n     +    Refuse to do so by default for \"revert\", \"cherry-pick\" and \"rebase\".\n     +    For \"revert\" and \"cherry-pick\" only, add --allow-shallow-boundary to\n     +    override the check; it is remembered across --continue.  \"rebase\"\n     +    always refuses, and the error message suggests \"git fetch --unshallow\"\n     +    instead.\n     +\n     +    The check is made when the list of commits to replay is built, not when\n     +    each commit is picked, so a command that is going to be refused does so\n     +    before it has done anything: no commit is created, HEAD and the working\n     +    tree are untouched, and no sequencer or rebase state is left behind for\n     +    the user to clean up.\n     +\n     +    The \"apply\" backend of \"rebase\" does not go through the sequencer, so\n     +    it checks the commits \"git format-patch\" is about to turn into patches\n     +    before it rewinds HEAD.  This is skipped altogether in a repository\n     +    that is not shallow.\n     +\n     +    A \"pick\" or \"revert\" line for a shallow boundary commit that the user\n     +    types into the todo list by hand is not checked, as the check is not\n     +    made again after the todo list has been edited.  \"git commit --amend\"\n     +    is also left alone for now.\n      \n          Signed-off-by: Nakul V S <vsnakul@gmail.com>\n      \n     + ## Documentation/git-cherry-pick.adoc ##\n     +@@ Documentation/git-cherry-pick.adoc: fail unless one of `--empty=keep` or `--allow-empty` are specified.\n     + \tPass the merge strategy-specific option through to the\n     + \tmerge strategy.  See linkgit:git-merge[1] for details.\n     + \n     ++--allow-shallow-boundary::\n     ++\tBy default, 'git cherry-pick' refuses to pick a commit that is a\n     ++\tshallow boundary (i.e. a commit listed in `.git/shallow`).\n     ++\tIts parents are missing from this repository, so it looks like\n     ++\ta root commit that added every file in its tree, and picking it\n     ++\twould add all of them.  This option overrides that safeguard.\n     +++\n     ++To get the missing history instead, use `git fetch --unshallow`.\n     ++\n     + include::rerere-options.adoc[]\n     + \n     + SEQUENCER SUBCOMMANDS\n     +\n     + ## Documentation/git-rebase.adoc ##\n     +@@ Documentation/git-rebase.adoc: repository that you share.\n     + See also <<RECOVERING_FROM_UPSTREAM_REBASE,RECOVERING FROM UPSTREAM REBASE>>\n     + below.\n     + \n     ++If the commits to be rebased include a shallow boundary commit (a\n     ++commit listed in `.git/shallow`), the rebase refuses to start, because\n     ++the missing parents make that commit look like a root commit that added\n     ++every file in its tree.  This applies to both the `merge` and `apply`\n     ++backends, and nothing is changed when the rebase is refused.  Run\n     ++`git fetch --unshallow` to get the missing history first.\n     ++\n     + When the rebase is run, it will first execute a `pre-rebase` hook if one\n     + exists.  You can use this hook to do sanity checks and reject the rebase\n     + if it isn't appropriate.  Please see the template `pre-rebase` hook script\n     +\n       ## Documentation/git-revert.adoc ##\n     -@@ Documentation/git-revert.adoc: git-revert - Revert some existing commits\n     - SYNOPSIS\n     - --------\n     - [verse]\n     --'git revert' [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] <commit>...\n     -+'git revert' [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] [--allow-shallow-boundary] <commit>...\n     - 'git revert' (--continue | --skip | --abort | --quit)\n     - \n     - DESCRIPTION\n      @@ Documentation/git-revert.adoc: include::rerere-options.adoc[]\n       \tconfiguration variable can be used to enable this option by\n       \tdefault.\n       \n      +--allow-shallow-boundary::\n      +\tBy default, 'git revert' refuses to revert a commit that is a\n     -+\tshallow boundary (i.e. a commit listed in `.git/shallow`),\n     -+\tbecause its true parent history is absent from the clone and\n     -+\treverting it would remove all tracked files. This option\n     -+\toverrides that safeguard and allows the revert to proceed.\n     ++\tshallow boundary (i.e. a commit listed in `.git/shallow`).\n     ++\tIts parents are missing from this repository, so it looks like\n     ++\ta root commit that added every file in its tree, and reverting\n     ++\tit would remove all of them.  This option overrides that\n     ++\tsafeguard.\n      ++\n     -+To restore full history before reverting, use\n     -+`git fetch --unshallow` instead.\n     ++To get the missing history instead, use `git fetch --unshallow`.\n      +\n       \n       SEQUENCER SUBCOMMANDS\n       ---------------------\n      \n     - ## builtin/revert.c ##\n     + ## builtin/rebase.c ##\n      @@\n     -  */\n     - \n     - static const char * const revert_usage[] = {\n     --\tN_(\"git revert [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] <commit>...\"),\n     -+\tN_(\"git revert [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] [--allow-shallow-boundary] <commit>...\"),\n     - \tN_(\"git revert (--continue | --skip | --abort | --quit)\"),\n     - \tNULL\n     - };\n     + #include \"sequencer.h\"\n     + #include \"rebase-interactive.h\"\n     + #include \"reset.h\"\n     ++#include \"shallow.h\"\n     + #include \"trace2.h\"\n     + #include \"hook.h\"\n     + #include \"trailer.h\"\n     +@@ builtin/rebase.c: static int init_basic_state(struct replay_opts *opts, const char *head_name,\n     + \treturn write_basic_state(opts, head_name, onto, orig_head);\n     + }\n     + \n     ++static int cleanup_autostash(struct rebase_options *opts);\n     ++\n     + static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)\n     + {\n     + \tint ret = -1;\n     +@@ builtin/rebase.c: static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)\n     + \t\t\t\t    &make_script_args, flags);\n     + \tif (ret) {\n     + \t\terror(_(\"could not generate todo list\"));\n     ++\t\t/* Nothing has been started yet; put back what we stashed */\n     ++\t\tcleanup_autostash(opts);\n     + \t\tgoto cleanup;\n     + \t}\n     + \n     +@@ builtin/rebase.c: static int move_to_original_branch(struct rebase_options *opts)\n     + \treturn ret;\n     + }\n     + \n     ++/*\n     ++ * The \"apply\" backend does not go through the sequencer, so check here that\n     ++ * none of the commits \"git format-patch\" is about to turn into patches is a\n     ++ * shallow boundary commit.  This walks the same set of commits as run_am().\n     ++ */\n     ++static int check_no_shallow_boundary_to_apply(struct rebase_options *opts)\n     ++{\n     ++\tstruct rev_info revs;\n     ++\tstruct strvec args = STRVEC_INIT;\n     ++\tstruct commit *commit;\n     ++\tint ret = 0;\n     ++\n     ++\tif (!is_repository_shallow(the_repository))\n     ++\t\treturn 0;\n     ++\n     ++\trepo_init_revisions(the_repository, &revs, NULL);\n     ++\trevs.max_parents = 1;\n     ++\trevs.cherry_pick = 1;\n     ++\trevs.right_only = 1;\n     ++\trevs.limited = 1;\n     ++\n     ++\tstrvec_push(&args, \"\");\n     ++\tstrvec_pushf(&args, \"%s...%s\",\n     ++\t\t     oid_to_hex(opts->root ?\n     ++\t\t\t\t&opts->onto->object.oid :\n     ++\t\t\t\t&opts->upstream->object.oid),\n     ++\t\t     oid_to_hex(&opts->orig_head->object.oid));\n     ++\tif (opts->restrict_revision)\n     ++\t\tstrvec_pushf(&args, \"^%s\",\n     ++\t\t\t     oid_to_hex(&opts->restrict_revision->object.oid));\n     ++\n     ++\tsetup_revisions_from_strvec(&args, &revs, NULL);\n     ++\tif (prepare_revision_walk(&revs)) {\n     ++\t\tret = error(_(\"revision walk setup failed\"));\n     ++\t\tgoto out;\n     ++\t}\n     ++\twhile ((commit = get_revision(&revs))) {\n     ++\t\tif (sequencer_refuse_shallow_boundary(the_repository, commit, 0)) {\n     ++\t\t\tret = -1;\n     ++\t\t\tbreak;\n     ++\t\t}\n     ++\t}\n     ++\n     ++out:\n     ++\trelease_revisions(&revs);\n     ++\tstrvec_clear(&args);\n     ++\treturn ret;\n     ++}\n     ++\n     + static int run_am(struct rebase_options *opts)\n     + {\n     + \tstruct child_process am = CHILD_PROCESS_INIT;\n     +@@ builtin/rebase.c: int cmd_rebase(int argc,\n     + \tif (is_merge(&options))\n     + \t\tgoto run_rebase;\n     + \n     ++\t/* Fail before HEAD or the working tree has been touched */\n     ++\tif (check_no_shallow_boundary_to_apply(&options)) {\n     ++\t\tret = -1;\n     ++\t\tgoto cleanup_autostash;\n     ++\t}\n     ++\n     + \t/* Detach HEAD and reset the tree */\n     + \tif (options.flags & REBASE_NO_QUIET)\n     + \t\tprintf(_(\"First, rewinding head to replay your work on top of \"\n     +\n     + ## builtin/revert.c ##\n      @@ builtin/revert.c: static int run_sequencer(int argc, const char **argv, const char *prefix,\n       \t\tOPT_BOOL('e', \"edit\", &opts->edit, N_(\"edit the commit message\")),\n       \t\tOPT_NOOP_NOARG('r', NULL),\n       \t\tOPT_BOOL('s', \"signoff\", &opts->signoff, N_(\"add a Signed-off-by trailer\")),\n      +\t\tOPT_BOOL(0, \"allow-shallow-boundary\", &opts->allow_shallow_boundary,\n     -+\t\t\t N_(\"allow reverting a shallow boundary commit\")),\n     ++\t\t\t N_(\"allow picking or reverting a shallow boundary commit\")),\n       \t\tOPT_CALLBACK('m', \"mainline\", opts, N_(\"parent-number\"),\n       \t\t\t     N_(\"select mainline parent\"), option_parse_m),\n       \t\tOPT_RERERE_AUTOUPDATE(&opts->allow_rerere_auto),\n     +@@ builtin/revert.c: static int run_sequencer(int argc, const char **argv, const char *prefix,\n     + \t\t\t\t\"--strategy-option\", opts->xopts.nr ? 1 : 0,\n     + \t\t\t\t\"-x\", opts->record_origin,\n     + \t\t\t\t\"--ff\", opts->allow_ff,\n     ++\t\t\t\t\"--allow-shallow-boundary\", opts->allow_shallow_boundary,\n     + \t\t\t\t\"--rerere-autoupdate\", opts->allow_rerere_auto == RERERE_AUTOUPDATE,\n     + \t\t\t\t\"--no-rerere-autoupdate\", opts->allow_rerere_auto == RERERE_NOAUTOUPDATE,\n     + \t\t\t\t\"--keep-redundant-commits\", opts->keep_redundant_commits,\n      \n       ## sequencer.c ##\n      @@\n     @@ sequencer.c\n       \n       #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n       \n     -@@ sequencer.c: static enum pick_result do_pick_commit(struct repository *r,\n     +@@ sequencer.c: static int populate_opts_cb(const char *key, const char *value,\n     + \t\topts->record_origin = git_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n     + \telse if (!strcmp(key, \"options.allow-ff\"))\n     + \t\topts->allow_ff = git_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n     ++\telse if (!strcmp(key, \"options.allow-shallow-boundary\"))\n     ++\t\topts->allow_shallow_boundary =\n     ++\t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n     + \telse if (!strcmp(key, \"options.mainline\"))\n     + \t\topts->mainline = git_config_int(key, value, ctx->kvi);\n     + \telse if (!strcmp(key, \"options.strategy\"))\n     +@@ sequencer.c: int write_basic_state(struct replay_opts *opts, const char *head_name,\n     + \treturn 0;\n     + }\n     + \n     ++/*\n     ++ * A shallow boundary commit has had its parents hidden, so it looks like\n     ++ * a root commit that added every file at once.  Picking or reverting it\n     ++ * would not do what the user expects, so refuse before anything is\n     ++ * started.\n     ++ */\n     ++int sequencer_refuse_shallow_boundary(struct repository *r,\n     ++\t\t\t\t      const struct commit *commit, int allow)\n     ++{\n     ++\tif (allow || !is_shallow_boundary_commit(r, &commit->object.oid))\n     ++\t\treturn 0;\n     ++\treturn error(_(\"commit %s is a shallow boundary commit; its parents \"\n     ++\t\t       \"are missing from this clone (use 'git fetch --unshallow' \"\n     ++\t\t       \"to get them)\"),\n     ++\t\t     oid_to_hex(&commit->object.oid));\n     ++}\n     ++\n     + static int walk_revs_populate_todo(struct todo_list *todo_list,\n     + \t\t\t\tstruct replay_opts *opts)\n     + {\n     +@@ sequencer.c: static int walk_revs_populate_todo(struct todo_list *todo_list,\n     + \tencoding = get_log_output_encoding();\n     + \n     + \twhile ((commit = get_revision(opts->revs))) {\n     +-\t\tstruct todo_item *item = append_new_todo(todo_list);\n     +-\t\tconst char *commit_buffer = repo_logmsg_reencode(the_repository,\n     +-\t\t\t\t\t\t\t\t commit, NULL,\n     +-\t\t\t\t\t\t\t\t encoding);\n     ++\t\tstruct todo_item *item;\n     ++\t\tconst char *commit_buffer;\n     + \t\tconst char *subject;\n     + \t\tint subject_len;\n     + \n     ++\t\tif (sequencer_refuse_shallow_boundary(the_repository, commit,\n     ++\t\t\t\t\t    opts->allow_shallow_boundary))\n     ++\t\t\treturn -1;\n     ++\n     ++\t\titem = append_new_todo(todo_list);\n     ++\t\tcommit_buffer = repo_logmsg_reencode(the_repository, commit,\n     ++\t\t\t\t\t\t     NULL, encoding);\n     + \t\titem->command = command;\n     + \t\titem->commit = commit;\n     + \t\titem->arg_offset = 0;\n     +@@ sequencer.c: static int save_opts(struct replay_opts *opts)\n     + \tif (opts->allow_ff)\n     + \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n     + \t\t\t\t\t\"options.allow-ff\", NULL, \"true\");\n     ++\tif (opts->allow_shallow_boundary)\n     ++\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n     ++\t\t\t\t\"options.allow-shallow-boundary\", NULL, \"true\");\n     + \tif (opts->mainline) {\n     + \t\tstruct strbuf buf = STRBUF_INIT;\n     + \t\tstrbuf_addf(&buf, \"%d\", opts->mainline);\n     +@@ sequencer.c: static int single_pick(struct repository *r,\n     + \tint check_todo;\n     + \tstruct todo_item item;\n     + \n     ++\tif (sequencer_refuse_shallow_boundary(r, cmit, opts->allow_shallow_boundary))\n     ++\t\treturn -1;\n     ++\n     + \titem.command = opts->action == REPLAY_PICK ?\n     + \t\t\tTODO_PICK : TODO_REVERT;\n     + \titem.commit = cmit;\n     +@@ sequencer.c: static int make_script_with_merges(struct pretty_print_context *pp,\n     + \tint rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;\n     + \tint root_with_onto = flags & TODO_LIST_ROOT_WITH_ONTO;\n     + \tint skipped_commit = 0;\n     ++\tint ret = 0;\n     + \tstruct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;\n     + \tstruct strbuf label_from_message = STRBUF_INIT;\n     + \tstruct commit_list *commits = NULL, **tail = &commits, *iter;\n     +@@ sequencer.c: static int make_script_with_merges(struct pretty_print_context *pp,\n     + \t\t}\n     + \t\tif (is_empty && !keep_empty)\n     + \t\t\tcontinue;\n     ++\t\tif (sequencer_refuse_shallow_boundary(the_repository, commit, 0)) {\n     ++\t\t\tret = -1;\n     ++\t\t\tgoto cleanup;\n     ++\t\t}\n     + \n     + \t\tstrbuf_reset(&oneline);\n     + \t\tpretty_print_commit(pp, commit, &oneline);\n     +@@ sequencer.c: static int make_script_with_merges(struct pretty_print_context *pp,\n     + \t\tcommit_list_free(list);\n       \t}\n     - \tdiscard_index(r->index);\n     - \n     -+\tif (is_repository_shallow(r)) {\n     -+\t\tstruct commit_graft *graft = lookup_commit_graft(r, &commit->object.oid);\n     -+\t\tif (graft && graft->nr_parent < 0 && !opts->allow_shallow_boundary) {\n     -+\t\t\treturn error(_(\"%s: commit %s is a shallow boundary commit.\\n\"\n     -+\t\t\t\t       \"Reverting it will remove all tracked files because its\\n\"\n     -+\t\t\t\t       \"true parent history is not present in this clone.\\n\"\n     -+\t\t\t\t       \"Use --allow-shallow-boundary to override, or\\n\"\n     -+\t\t\t\t       \"'git fetch --unshallow' to restore full history first.\"),\n     -+\t\t\t\t     command_to_string(command),\n     -+\t\t\t\t     oid_to_hex(&commit->object.oid));\n     + \n     ++cleanup:\n     + \tcommit_list_free(commits);\n     + \tcommit_list_free(tips);\n     + \n     +@@ sequencer.c: static int make_script_with_merges(struct pretty_print_context *pp,\n     + \thashmap_clear_and_free(&state.labels, struct labels_entry, entry);\n     + \tstrbuf_release(&state.buf);\n     + \n     +-\treturn 0;\n     ++\treturn ret;\n     + }\n     + \n     + int sequencer_make_script(struct repository *r, struct strbuf *out,\n     +@@ sequencer.c: int sequencer_make_script(struct repository *r, struct strbuf *out,\n     + \t\t}\n     + \t\tif (is_empty && !keep_empty)\n     + \t\t\tcontinue;\n     ++\t\tif (sequencer_refuse_shallow_boundary(r, commit, 0)) {\n     ++\t\t\tret = -1;\n     ++\t\t\tgoto cleanup;\n      +\t\t}\n     -+\t}\n     -+\n     - \tif (!commit->parents)\n     - \t\tparent = NULL;\n     - \telse if (commit->parents->next) {\n     + \t\tstrbuf_addf(out, \"%s %s \", insn,\n     + \t\t\t    oid_to_hex(&commit->object.oid));\n     + \t\tpretty_print_commit(&pp, commit, out);\n      \n       ## sequencer.h ##\n      @@ sequencer.h: struct replay_opts {\n     @@ sequencer.h: struct replay_opts {\n       \n       \tstruct strvec trailer_args;\n       \n     +@@ sequencer.h: int sequencer_remove_state(struct replay_opts *opts);\n     + #define TODO_LIST_REAPPLY_CHERRY_PICKS (1U << 7)\n     + #define TODO_LIST_WARN_SKIPPED_CHERRY_PICKS (1U << 8)\n     + \n     ++/*\n     ++ * Return -1 after reporting an error if \"commit\" is a shallow boundary\n     ++ * commit and \"allow\" is zero, 0 otherwise.\n     ++ */\n     ++int sequencer_refuse_shallow_boundary(struct repository *r,\n     ++\t\t\t\t      const struct commit *commit, int allow);\n     + int sequencer_make_script(struct repository *r, struct strbuf *out,\n     + \t\t\t  struct strvec *argv, unsigned flags);\n     + \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 is_shallow_boundary_commit(struct repository *r, const struct object_id *oid)\n     ++{\n     ++\tstruct commit_graft *graft;\n     ++\tif (!is_repository_shallow(r))\n     ++\t\treturn 0;\n     ++\tgraft = lookup_commit_graft(r, oid);\n     ++\treturn graft && graft->nr_parent < 0;\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: void set_alternate_shallow_file(struct repository *r, const char *path);\n     + 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     ++int is_shallow_boundary_commit(struct repository *r, const struct object_id *oid);\n     + \n     + /*\n     +  * Lock for updating the $GIT_DIR/shallow file.\n      \n       ## t/t3501-revert-cherry-pick.sh ##\n      @@ t/t3501-revert-cherry-pick.sh: test_expect_success 'cherry-pick is unaware of --reference (for now)' '\n     @@ t/t3501-revert-cherry-pick.sh: test_expect_success 'cherry-pick is unaware of --\n      +# Tests for shallow boundary safeguard\n      +\n      +test_expect_success 'setup shallow boundary tests' '\n     -+\tgit init src &&\n     -+\ttest_commit -C src A &&\n     -+\ttest_commit -C src B &&\n     -+\tgit clone --no-local --depth=1 src shallow-repo\n     ++\tgit init shallow-src &&\n     ++\ttest_commit -C shallow-src A &&\n     ++\ttest_commit -C shallow-src B &&\n     ++\ttest_commit -C shallow-src C &&\n     ++\tgit clone --no-local --depth=2 \"file://$(pwd)/shallow-src\" shallow-repo &&\n     ++\tgit -C shallow-repo rev-parse --verify B >boundary &&\n     ++\ttest_cmp shallow-repo/.git/shallow boundary\n     ++'\n     ++\n     ++test_expect_success 'revert of shallow boundary commit is refused' '\n     ++\tgit -C shallow-repo reset --hard C &&\n     ++\ttest_must_fail git -C shallow-repo revert --no-edit B 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\ttest_path_is_missing shallow-repo/.git/sequencer &&\n     ++\ttest_path_is_missing shallow-repo/.git/REVERT_HEAD &&\n     ++\tgit -C shallow-repo rev-parse C >expect &&\n     ++\tgit -C shallow-repo rev-parse HEAD >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'cherry-pick of shallow boundary commit is refused' '\n     ++\tgit -C shallow-repo checkout -q --detach C &&\n     ++\tgit -C shallow-repo rm -rq . &&\n     ++\tgit -C shallow-repo commit -q -m empty &&\n     ++\tgit -C shallow-repo rev-parse HEAD >expect &&\n     ++\ttest_must_fail git -C shallow-repo cherry-pick B 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\tgit -C shallow-repo rev-parse HEAD >actual &&\n     ++\ttest_cmp expect actual &&\n     ++\tgit -C shallow-repo reset -q --hard C\n     ++'\n     ++\n     ++test_expect_success 'revert of non-boundary commit in shallow repo works' '\n     ++\tgit -C shallow-repo revert --no-edit C &&\n     ++\tgit -C shallow-repo reset -q --hard C\n     ++'\n     ++\n     ++test_expect_success 'multi-commit revert is refused before anything is reverted' '\n     ++\ttest_must_fail git -C shallow-repo revert --no-edit C B 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\ttest_path_is_missing shallow-repo/.git/sequencer &&\n     ++\tgit -C shallow-repo rev-parse C >expect &&\n     ++\tgit -C shallow-repo rev-parse HEAD >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success '--allow-shallow-boundary overrides the revert check' '\n     ++\tgit -C shallow-repo revert --no-edit --allow-shallow-boundary B &&\n     ++\ttest_path_is_missing shallow-repo/B.t &&\n     ++\tgit -C shallow-repo reset -q --hard C\n     ++'\n     ++\n     ++test_expect_success '--allow-shallow-boundary overrides the cherry-pick check' '\n     ++\tgit -C shallow-repo checkout -q --detach C &&\n     ++\tgit -C shallow-repo rm -rq . &&\n     ++\tgit -C shallow-repo commit -q -m empty &&\n     ++\tgit -C shallow-repo cherry-pick --allow-shallow-boundary B &&\n     ++\ttest_path_is_file shallow-repo/B.t &&\n     ++\tgit -C shallow-repo reset -q --hard C\n     ++'\n     ++\n     ++test_expect_success '--allow-shallow-boundary survives --continue' '\n     ++\ttest_commit -C shallow-repo conflict-1 file one &&\n     ++\ttest_commit -C shallow-repo conflict-2 file two &&\n     ++\ttest_must_fail git -C shallow-repo revert --no-edit \\\n     ++\t\t--allow-shallow-boundary conflict-1 B &&\n     ++\techo resolved >shallow-repo/file &&\n     ++\tgit -C shallow-repo add file &&\n     ++\tgit -C shallow-repo revert --continue &&\n     ++\ttest_path_is_missing shallow-repo/B.t &&\n     ++\tgit -C shallow-repo reset -q --hard C\n     ++'\n     ++\n     ++test_expect_success 'revert and cherry-pick in a non-shallow repo are unaffected' '\n     ++\tgit init full-repo &&\n     ++\ttest_commit -C full-repo A &&\n     ++\ttest_commit -C full-repo B &&\n     ++\tgit -C full-repo revert --no-edit B &&\n     ++\tgit -C full-repo cherry-pick B &&\n     ++\ttest_path_is_file full-repo/B.t\n     ++'\n     ++\n     ++test_expect_success 'rebase of a range containing a shallow boundary is refused' '\n     ++\tgit -C shallow-repo checkout -q --detach C &&\n     ++\tgit -C shallow-repo rev-parse C >expect &&\n     ++\ttest_must_fail git -C shallow-repo rebase --root 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\ttest_path_is_missing shallow-repo/.git/rebase-merge &&\n     ++\tgit -C shallow-repo rev-parse HEAD >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'rebase --rebase-merges is refused as well' '\n     ++\ttest_must_fail git -C shallow-repo rebase --rebase-merges --root 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\ttest_path_is_missing shallow-repo/.git/rebase-merge\n     ++'\n     ++\n     ++test_expect_success 'rebase --apply of a range containing a shallow boundary is refused' '\n     ++\tgit -C shallow-repo checkout -q --detach C &&\n     ++\tgit -C shallow-repo checkout -q --orphan unrelated &&\n     ++\tgit -C shallow-repo rm -rfq . &&\n     ++\ttest_commit -C shallow-repo unrelated &&\n     ++\tgit -C shallow-repo checkout -q --detach C &&\n     ++\tgit -C shallow-repo rev-parse C >expect &&\n     ++\ttest_must_fail git -C shallow-repo rebase --apply \\\n     ++\t\t--onto unrelated unrelated 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\ttest_path_is_missing shallow-repo/.git/rebase-apply &&\n     ++\tgit -C shallow-repo rev-parse HEAD >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'rebase --apply restores an autostash when refusing' '\n     ++\techo dirty >shallow-repo/C.t &&\n     ++\ttest_must_fail git -C shallow-repo rebase --apply --autostash \\\n     ++\t\t--onto unrelated unrelated 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\techo dirty >expect &&\n     ++\ttest_cmp expect shallow-repo/C.t &&\n     ++\tgit -C shallow-repo stash list >stashes &&\n     ++\ttest_must_be_empty stashes &&\n     ++\tgit -C shallow-repo checkout -q -- C.t\n     ++'\n     ++\n     ++test_expect_success 'rebase --apply that does not replay the boundary is unaffected' '\n     ++\tgit -C shallow-repo checkout -q -B apply-topic C &&\n     ++\ttest_commit -C shallow-repo E &&\n     ++\tgit -C shallow-repo rebase --apply --onto unrelated C &&\n     ++\ttest_path_is_file shallow-repo/E.t\n     ++'\n     ++\n     ++test_expect_success 'rebase that does not replay the boundary is unaffected' '\n     ++\tgit -C shallow-repo checkout -q -B topic C &&\n     ++\ttest_commit -C shallow-repo D &&\n     ++\tgit -C shallow-repo rebase C\n     ++'\n     ++\n     ++test_expect_success 'rebase restores an autostash when refusing (merge backend)' '\n     ++\tgit -C shallow-repo checkout -q --detach C &&\n     ++\techo dirty >shallow-repo/C.t &&\n     ++\ttest_must_fail git -C shallow-repo rebase --autostash --root 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\ttest_path_is_missing shallow-repo/.git/rebase-merge &&\n     ++\techo dirty >expect &&\n     ++\ttest_cmp expect shallow-repo/C.t &&\n     ++\tgit -C shallow-repo stash list >stashes &&\n     ++\ttest_must_be_empty stashes &&\n     ++\tgit -C shallow-repo checkout -q -- C.t\n     ++'\n     ++\n     ++test_expect_success 'interactive rebase restores an autostash when refusing' '\n     ++\techo dirty >shallow-repo/C.t &&\n     ++\ttest_must_fail env GIT_SEQUENCE_EDITOR=: \\\n     ++\t\tgit -C shallow-repo rebase -i --autostash --root 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\ttest_path_is_missing shallow-repo/.git/rebase-merge &&\n     ++\techo dirty >expect &&\n     ++\ttest_cmp expect shallow-repo/C.t &&\n     ++\tgit -C shallow-repo stash list >stashes &&\n     ++\ttest_must_be_empty stashes &&\n     ++\tgit -C shallow-repo checkout -q -- C.t\n     ++'\n     ++\n     ++test_expect_success 'rebase onto new upstream work in a shallow clone still works' '\n     ++\tgit clone --no-local --depth=1 \"file://$(pwd)/shallow-src\" shallow-local &&\n     ++\ttest_commit -C shallow-local local &&\n     ++\ttest_commit -C shallow-src upstream &&\n     ++\tgit -C shallow-local fetch origin &&\n     ++\tgit -C shallow-local rebase origin/HEAD &&\n     ++\ttest_path_is_file shallow-local/upstream.t &&\n     ++\ttest_path_is_file shallow-local/local.t\n      +'\n      +\n     -+test_expect_success 'revert of shallow boundary commit is refused by default' '\n     -+\ttest_must_fail git -C shallow-repo revert --no-edit HEAD 2>err &&\n     -+\ttest_grep \"shallow boundary commit\" err\n     ++test_expect_success 'single-commit shallow clone: revert and cherry-pick are refused' '\n     ++\tgit init one-src &&\n     ++\ttest_commit -C one-src only &&\n     ++\tgit clone --no-local --depth=1 \"file://$(pwd)/one-src\" one-shallow &&\n     ++\tgit -C one-shallow rev-parse HEAD >expect &&\n     ++\ttest_must_fail git -C one-shallow revert --no-edit HEAD 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\ttest_must_fail git -C one-shallow cherry-pick -n HEAD 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\ttest_path_is_file one-shallow/only.t &&\n     ++\tgit -C one-shallow rev-parse HEAD >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'revert of shallow boundary succeeds with --allow-shallow-boundary' '\n     -+\tgit -C shallow-repo revert --no-edit --allow-shallow-boundary HEAD\n     ++test_expect_success 'revert and cherry-pick work once the clone is unshallowed' '\n     ++\tgit clone --no-local --depth=2 \"file://$(pwd)/shallow-src\" unshallowed &&\n     ++\ttest_must_fail git -C unshallowed revert --no-edit HEAD~1 2>err &&\n     ++\ttest_grep \"shallow boundary commit\" err &&\n     ++\tgit -C unshallowed fetch --unshallow &&\n     ++\tgit -C unshallowed revert --no-edit C &&\n     ++\ttest_path_is_missing unshallowed/C.t &&\n     ++\tgit -C unshallowed cherry-pick C &&\n     ++\ttest_path_is_file unshallowed/C.t\n      +'\n      +\n     -+test_expect_success 'revert in non-shallow repo is unaffected' '\n     -+\tgit init non-shallow &&\n     -+\ttest_commit -C non-shallow A &&\n     -+\ttest_commit -C non-shallow B &&\n     -+\tgit -C non-shallow revert --no-edit HEAD\n     ++test_expect_success '--allow-shallow-boundary cannot be combined with --abort' '\n     ++\ttest_must_fail git -C shallow-repo revert --abort \\\n     ++\t\t--allow-shallow-boundary 2>err &&\n     ++\ttest_grep \"cannot be used with\" err\n      +'\n      +\n       test_done\n\n\n Documentation/git-cherry-pick.adoc |   9 ++\n Documentation/git-rebase.adoc      |   7 +\n Documentation/git-revert.adoc      |  10 ++\n builtin/rebase.c                   |  60 +++++++++\n builtin/revert.c                   |   3 +\n sequencer.c                        |  52 +++++++-\n sequencer.h                        |   7 +\n shallow.c                          |   9 ++\n shallow.h                          |   1 +\n t/t3501-revert-cherry-pick.sh      | 208 +++++++++++++++++++++++++++++\n 10 files changed, 361 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-cherry-pick.adoc b/Documentation/git-cherry-pick.adoc\nindex f4cd8b9db7..ff6f1bf01d 100644\n--- a/Documentation/git-cherry-pick.adoc\n+++ b/Documentation/git-cherry-pick.adoc\n@@ -178,6 +178,15 @@ fail unless one of `--empty=keep` or `--allow-empty` are specified.\n \tPass the merge strategy-specific option through to the\n \tmerge strategy.  See linkgit:git-merge[1] for details.\n \n+--allow-shallow-boundary::\n+\tBy default, 'git cherry-pick' refuses to pick a commit that is a\n+\tshallow boundary (i.e. a commit listed in `.git/shallow`).\n+\tIts parents are missing from this repository, so it looks like\n+\ta root commit that added every file in its tree, and picking it\n+\twould add all of them.  This option overrides that safeguard.\n++\n+To get the missing history instead, use `git fetch --unshallow`.\n+\n include::rerere-options.adoc[]\n \n SEQUENCER SUBCOMMANDS\ndiff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc\nindex 6536ebdc90..ed2ce2d46a 100644\n--- a/Documentation/git-rebase.adoc\n+++ b/Documentation/git-rebase.adoc\n@@ -822,6 +822,13 @@ repository that you share.\n See also <<RECOVERING_FROM_UPSTREAM_REBASE,RECOVERING FROM UPSTREAM REBASE>>\n below.\n \n+If the commits to be rebased include a shallow boundary commit (a\n+commit listed in `.git/shallow`), the rebase refuses to start, because\n+the missing parents make that commit look like a root commit that added\n+every file in its tree.  This applies to both the `merge` and `apply`\n+backends, and nothing is changed when the rebase is refused.  Run\n+`git fetch --unshallow` to get the missing history first.\n+\n When the rebase is run, it will first execute a `pre-rebase` hook if one\n exists.  You can use this hook to do sanity checks and reject the rebase\n if it isn't appropriate.  Please see the template `pre-rebase` hook script\ndiff --git a/Documentation/git-revert.adoc b/Documentation/git-revert.adoc\nindex ffba365e63..288eddcf6e 100644\n--- a/Documentation/git-revert.adoc\n+++ b/Documentation/git-revert.adoc\n@@ -122,6 +122,16 @@ include::rerere-options.adoc[]\n \tconfiguration variable can be used to enable this option by\n \tdefault.\n \n+--allow-shallow-boundary::\n+\tBy default, 'git revert' refuses to revert a commit that is a\n+\tshallow boundary (i.e. a commit listed in `.git/shallow`).\n+\tIts parents are missing from this repository, so it looks like\n+\ta root commit that added every file in its tree, and reverting\n+\tit would remove all of them.  This option overrides that\n+\tsafeguard.\n++\n+To get the missing history instead, use `git fetch --unshallow`.\n+\n \n SEQUENCER SUBCOMMANDS\n ---------------------\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 535db60181..2305a3a495 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -34,6 +34,7 @@\n #include \"sequencer.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"shallow.h\"\n #include \"trace2.h\"\n #include \"hook.h\"\n #include \"trailer.h\"\n@@ -288,6 +289,8 @@ static int init_basic_state(struct replay_opts *opts, const char *head_name,\n \treturn write_basic_state(opts, head_name, onto, orig_head);\n }\n \n+static int cleanup_autostash(struct rebase_options *opts);\n+\n static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)\n {\n \tint ret = -1;\n@@ -309,6 +312,8 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)\n \t\t\t\t    &make_script_args, flags);\n \tif (ret) {\n \t\terror(_(\"could not generate todo list\"));\n+\t\t/* Nothing has been started yet; put back what we stashed */\n+\t\tcleanup_autostash(opts);\n \t\tgoto cleanup;\n \t}\n \n@@ -618,6 +623,55 @@ static int move_to_original_branch(struct rebase_options *opts)\n \treturn ret;\n }\n \n+/*\n+ * The \"apply\" backend does not go through the sequencer, so check here that\n+ * none of the commits \"git format-patch\" is about to turn into patches is a\n+ * shallow boundary commit.  This walks the same set of commits as run_am().\n+ */\n+static int check_no_shallow_boundary_to_apply(struct rebase_options *opts)\n+{\n+\tstruct rev_info revs;\n+\tstruct strvec args = STRVEC_INIT;\n+\tstruct commit *commit;\n+\tint ret = 0;\n+\n+\tif (!is_repository_shallow(the_repository))\n+\t\treturn 0;\n+\n+\trepo_init_revisions(the_repository, &revs, NULL);\n+\trevs.max_parents = 1;\n+\trevs.cherry_pick = 1;\n+\trevs.right_only = 1;\n+\trevs.limited = 1;\n+\n+\tstrvec_push(&args, \"\");\n+\tstrvec_pushf(&args, \"%s...%s\",\n+\t\t     oid_to_hex(opts->root ?\n+\t\t\t\t&opts->onto->object.oid :\n+\t\t\t\t&opts->upstream->object.oid),\n+\t\t     oid_to_hex(&opts->orig_head->object.oid));\n+\tif (opts->restrict_revision)\n+\t\tstrvec_pushf(&args, \"^%s\",\n+\t\t\t     oid_to_hex(&opts->restrict_revision->object.oid));\n+\n+\tsetup_revisions_from_strvec(&args, &revs, NULL);\n+\tif (prepare_revision_walk(&revs)) {\n+\t\tret = error(_(\"revision walk setup failed\"));\n+\t\tgoto out;\n+\t}\n+\twhile ((commit = get_revision(&revs))) {\n+\t\tif (sequencer_refuse_shallow_boundary(the_repository, commit, 0)) {\n+\t\t\tret = -1;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+out:\n+\trelease_revisions(&revs);\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n static int run_am(struct rebase_options *opts)\n {\n \tstruct child_process am = CHILD_PROCESS_INIT;\n@@ -1879,6 +1933,12 @@ int cmd_rebase(int argc,\n \tif (is_merge(&options))\n \t\tgoto run_rebase;\n \n+\t/* Fail before HEAD or the working tree has been touched */\n+\tif (check_no_shallow_boundary_to_apply(&options)) {\n+\t\tret = -1;\n+\t\tgoto cleanup_autostash;\n+\t}\n+\n \t/* Detach HEAD and reset the tree */\n \tif (options.flags & REBASE_NO_QUIET)\n \t\tprintf(_(\"First, rewinding head to replay your work on top of \"\ndiff --git a/builtin/revert.c b/builtin/revert.c\nindex 52100a20cb..a2526f3095 100644\n--- a/builtin/revert.c\n+++ b/builtin/revert.c\n@@ -128,6 +128,8 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,\n \t\tOPT_BOOL('e', \"edit\", &opts->edit, N_(\"edit the commit message\")),\n \t\tOPT_NOOP_NOARG('r', NULL),\n \t\tOPT_BOOL('s', \"signoff\", &opts->signoff, N_(\"add a Signed-off-by trailer\")),\n+\t\tOPT_BOOL(0, \"allow-shallow-boundary\", &opts->allow_shallow_boundary,\n+\t\t\t N_(\"allow picking or reverting a shallow boundary commit\")),\n \t\tOPT_CALLBACK('m', \"mainline\", opts, N_(\"parent-number\"),\n \t\t\t     N_(\"select mainline parent\"), option_parse_m),\n \t\tOPT_RERERE_AUTOUPDATE(&opts->allow_rerere_auto),\n@@ -213,6 +215,7 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,\n \t\t\t\t\"--strategy-option\", opts->xopts.nr ? 1 : 0,\n \t\t\t\t\"-x\", opts->record_origin,\n \t\t\t\t\"--ff\", opts->allow_ff,\n+\t\t\t\t\"--allow-shallow-boundary\", opts->allow_shallow_boundary,\n \t\t\t\t\"--rerere-autoupdate\", opts->allow_rerere_auto == RERERE_AUTOUPDATE,\n \t\t\t\t\"--no-rerere-autoupdate\", opts->allow_rerere_auto == RERERE_NOAUTOUPDATE,\n \t\t\t\t\"--keep-redundant-commits\", opts->keep_redundant_commits,\ndiff --git a/sequencer.c b/sequencer.c\nindex 508ec71792..8d38ac3878 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -49,6 +49,7 @@\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n #include \"branch.h\"\n+#include \"shallow.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -3201,6 +3202,9 @@ static int populate_opts_cb(const char *key, const char *value,\n \t\topts->record_origin = git_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n \telse if (!strcmp(key, \"options.allow-ff\"))\n \t\topts->allow_ff = git_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n+\telse if (!strcmp(key, \"options.allow-shallow-boundary\"))\n+\t\topts->allow_shallow_boundary =\n+\t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n \telse if (!strcmp(key, \"options.mainline\"))\n \t\topts->mainline = git_config_int(key, value, ctx->kvi);\n \telse if (!strcmp(key, \"options.strategy\"))\n@@ -3466,6 +3470,23 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,\n \treturn 0;\n }\n \n+/*\n+ * A shallow boundary commit has had its parents hidden, so it looks like\n+ * a root commit that added every file at once.  Picking or reverting it\n+ * would not do what the user expects, so refuse before anything is\n+ * started.\n+ */\n+int sequencer_refuse_shallow_boundary(struct repository *r,\n+\t\t\t\t      const struct commit *commit, int allow)\n+{\n+\tif (allow || !is_shallow_boundary_commit(r, &commit->object.oid))\n+\t\treturn 0;\n+\treturn error(_(\"commit %s is a shallow boundary commit; its parents \"\n+\t\t       \"are missing from this clone (use 'git fetch --unshallow' \"\n+\t\t       \"to get them)\"),\n+\t\t     oid_to_hex(&commit->object.oid));\n+}\n+\n static int walk_revs_populate_todo(struct todo_list *todo_list,\n \t\t\t\tstruct replay_opts *opts)\n {\n@@ -3481,13 +3502,18 @@ static int walk_revs_populate_todo(struct todo_list *todo_list,\n \tencoding = get_log_output_encoding();\n \n \twhile ((commit = get_revision(opts->revs))) {\n-\t\tstruct todo_item *item = append_new_todo(todo_list);\n-\t\tconst char *commit_buffer = repo_logmsg_reencode(the_repository,\n-\t\t\t\t\t\t\t\t commit, NULL,\n-\t\t\t\t\t\t\t\t encoding);\n+\t\tstruct todo_item *item;\n+\t\tconst char *commit_buffer;\n \t\tconst char *subject;\n \t\tint subject_len;\n \n+\t\tif (sequencer_refuse_shallow_boundary(the_repository, commit,\n+\t\t\t\t\t    opts->allow_shallow_boundary))\n+\t\t\treturn -1;\n+\n+\t\titem = append_new_todo(todo_list);\n+\t\tcommit_buffer = repo_logmsg_reencode(the_repository, commit,\n+\t\t\t\t\t\t     NULL, encoding);\n \t\titem->command = command;\n \t\titem->commit = commit;\n \t\titem->arg_offset = 0;\n@@ -3799,6 +3825,9 @@ static int save_opts(struct replay_opts *opts)\n \tif (opts->allow_ff)\n \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n \t\t\t\t\t\"options.allow-ff\", NULL, \"true\");\n+\tif (opts->allow_shallow_boundary)\n+\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n+\t\t\t\t\"options.allow-shallow-boundary\", NULL, \"true\");\n \tif (opts->mainline) {\n \t\tstruct strbuf buf = STRBUF_INIT;\n \t\tstrbuf_addf(&buf, \"%d\", opts->mainline);\n@@ -5692,6 +5721,9 @@ static int single_pick(struct repository *r,\n \tint check_todo;\n \tstruct todo_item item;\n \n+\tif (sequencer_refuse_shallow_boundary(r, cmit, opts->allow_shallow_boundary))\n+\t\treturn -1;\n+\n \titem.command = opts->action == REPLAY_PICK ?\n \t\t\tTODO_PICK : TODO_REVERT;\n \titem.commit = cmit;\n@@ -6078,6 +6110,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \tint rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;\n \tint root_with_onto = flags & TODO_LIST_ROOT_WITH_ONTO;\n \tint skipped_commit = 0;\n+\tint ret = 0;\n \tstruct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;\n \tstruct strbuf label_from_message = STRBUF_INIT;\n \tstruct commit_list *commits = NULL, **tail = &commits, *iter;\n@@ -6141,6 +6174,10 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \t\t}\n \t\tif (is_empty && !keep_empty)\n \t\t\tcontinue;\n+\t\tif (sequencer_refuse_shallow_boundary(the_repository, commit, 0)) {\n+\t\t\tret = -1;\n+\t\t\tgoto cleanup;\n+\t\t}\n \n \t\tstrbuf_reset(&oneline);\n \t\tpretty_print_commit(pp, commit, &oneline);\n@@ -6307,6 +6344,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \t\tcommit_list_free(list);\n \t}\n \n+cleanup:\n \tcommit_list_free(commits);\n \tcommit_list_free(tips);\n \n@@ -6322,7 +6360,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n \thashmap_clear_and_free(&state.labels, struct labels_entry, entry);\n \tstrbuf_release(&state.buf);\n \n-\treturn 0;\n+\treturn ret;\n }\n \n int sequencer_make_script(struct repository *r, struct strbuf *out,\n@@ -6394,6 +6432,10 @@ int sequencer_make_script(struct repository *r, struct strbuf *out,\n \t\t}\n \t\tif (is_empty && !keep_empty)\n \t\t\tcontinue;\n+\t\tif (sequencer_refuse_shallow_boundary(r, commit, 0)) {\n+\t\t\tret = -1;\n+\t\t\tgoto cleanup;\n+\t\t}\n \t\tstrbuf_addf(out, \"%s %s \", insn,\n \t\t\t    oid_to_hex(&commit->object.oid));\n \t\tpretty_print_commit(&pp, commit, out);\ndiff --git a/sequencer.h b/sequencer.h\nindex a5d12a1070..1035cc38d2 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -57,6 +57,7 @@ struct replay_opts {\n \tint committer_date_is_author_date;\n \tint ignore_date;\n \tint commit_use_reference;\n+\tint allow_shallow_boundary;\n \n \tstruct strvec trailer_args;\n \n@@ -197,6 +198,12 @@ int sequencer_remove_state(struct replay_opts *opts);\n #define TODO_LIST_REAPPLY_CHERRY_PICKS (1U << 7)\n #define TODO_LIST_WARN_SKIPPED_CHERRY_PICKS (1U << 8)\n \n+/*\n+ * Return -1 after reporting an error if \"commit\" is a shallow boundary\n+ * commit and \"allow\" is zero, 0 otherwise.\n+ */\n+int sequencer_refuse_shallow_boundary(struct repository *r,\n+\t\t\t\t      const struct commit *commit, int allow);\n int sequencer_make_script(struct repository *r, struct strbuf *out,\n \t\t\t  struct strvec *argv, unsigned flags);\n \ndiff --git a/shallow.c b/shallow.c\nindex 8e244a5669..bc31ff5232 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -92,6 +92,15 @@ int is_repository_shallow(struct repository *r)\n \treturn r->parsed_objects->is_shallow;\n }\n \n+int is_shallow_boundary_commit(struct repository *r, const struct object_id *oid)\n+{\n+\tstruct commit_graft *graft;\n+\tif (!is_repository_shallow(r))\n+\t\treturn 0;\n+\tgraft = lookup_commit_graft(r, oid);\n+\treturn graft && graft->nr_parent < 0;\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..c3fb6b929e 100644\n--- a/shallow.h\n+++ b/shallow.h\n@@ -14,6 +14,7 @@ void set_alternate_shallow_file(struct repository *r, const char *path);\n 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+int is_shallow_boundary_commit(struct repository *r, const struct object_id *oid);\n \n /*\n  * Lock for updating the $GIT_DIR/shallow file.\ndiff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh\nindex 939e7a16a6..49da8427cf 100755\n--- a/t/t3501-revert-cherry-pick.sh\n+++ b/t/t3501-revert-cherry-pick.sh\n@@ -256,4 +256,212 @@ test_expect_success 'cherry-pick is unaware of --reference (for now)' '\n \ttest_grep \"^usage: git cherry-pick\" actual\n '\n \n+# Tests for shallow boundary safeguard\n+\n+test_expect_success 'setup shallow boundary tests' '\n+\tgit init shallow-src &&\n+\ttest_commit -C shallow-src A &&\n+\ttest_commit -C shallow-src B &&\n+\ttest_commit -C shallow-src C &&\n+\tgit clone --no-local --depth=2 \"file://$(pwd)/shallow-src\" shallow-repo &&\n+\tgit -C shallow-repo rev-parse --verify B >boundary &&\n+\ttest_cmp shallow-repo/.git/shallow boundary\n+'\n+\n+test_expect_success 'revert of shallow boundary commit is refused' '\n+\tgit -C shallow-repo reset --hard C &&\n+\ttest_must_fail git -C shallow-repo revert --no-edit B 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\ttest_path_is_missing shallow-repo/.git/sequencer &&\n+\ttest_path_is_missing shallow-repo/.git/REVERT_HEAD &&\n+\tgit -C shallow-repo rev-parse C >expect &&\n+\tgit -C shallow-repo rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'cherry-pick of shallow boundary commit is refused' '\n+\tgit -C shallow-repo checkout -q --detach C &&\n+\tgit -C shallow-repo rm -rq . &&\n+\tgit -C shallow-repo commit -q -m empty &&\n+\tgit -C shallow-repo rev-parse HEAD >expect &&\n+\ttest_must_fail git -C shallow-repo cherry-pick B 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\tgit -C shallow-repo rev-parse HEAD >actual &&\n+\ttest_cmp expect actual &&\n+\tgit -C shallow-repo reset -q --hard C\n+'\n+\n+test_expect_success 'revert of non-boundary commit in shallow repo works' '\n+\tgit -C shallow-repo revert --no-edit C &&\n+\tgit -C shallow-repo reset -q --hard C\n+'\n+\n+test_expect_success 'multi-commit revert is refused before anything is reverted' '\n+\ttest_must_fail git -C shallow-repo revert --no-edit C B 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\ttest_path_is_missing shallow-repo/.git/sequencer &&\n+\tgit -C shallow-repo rev-parse C >expect &&\n+\tgit -C shallow-repo rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--allow-shallow-boundary overrides the revert check' '\n+\tgit -C shallow-repo revert --no-edit --allow-shallow-boundary B &&\n+\ttest_path_is_missing shallow-repo/B.t &&\n+\tgit -C shallow-repo reset -q --hard C\n+'\n+\n+test_expect_success '--allow-shallow-boundary overrides the cherry-pick check' '\n+\tgit -C shallow-repo checkout -q --detach C &&\n+\tgit -C shallow-repo rm -rq . &&\n+\tgit -C shallow-repo commit -q -m empty &&\n+\tgit -C shallow-repo cherry-pick --allow-shallow-boundary B &&\n+\ttest_path_is_file shallow-repo/B.t &&\n+\tgit -C shallow-repo reset -q --hard C\n+'\n+\n+test_expect_success '--allow-shallow-boundary survives --continue' '\n+\ttest_commit -C shallow-repo conflict-1 file one &&\n+\ttest_commit -C shallow-repo conflict-2 file two &&\n+\ttest_must_fail git -C shallow-repo revert --no-edit \\\n+\t\t--allow-shallow-boundary conflict-1 B &&\n+\techo resolved >shallow-repo/file &&\n+\tgit -C shallow-repo add file &&\n+\tgit -C shallow-repo revert --continue &&\n+\ttest_path_is_missing shallow-repo/B.t &&\n+\tgit -C shallow-repo reset -q --hard C\n+'\n+\n+test_expect_success 'revert and cherry-pick in a non-shallow repo are unaffected' '\n+\tgit init full-repo &&\n+\ttest_commit -C full-repo A &&\n+\ttest_commit -C full-repo B &&\n+\tgit -C full-repo revert --no-edit B &&\n+\tgit -C full-repo cherry-pick B &&\n+\ttest_path_is_file full-repo/B.t\n+'\n+\n+test_expect_success 'rebase of a range containing a shallow boundary is refused' '\n+\tgit -C shallow-repo checkout -q --detach C &&\n+\tgit -C shallow-repo rev-parse C >expect &&\n+\ttest_must_fail git -C shallow-repo rebase --root 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\ttest_path_is_missing shallow-repo/.git/rebase-merge &&\n+\tgit -C shallow-repo rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'rebase --rebase-merges is refused as well' '\n+\ttest_must_fail git -C shallow-repo rebase --rebase-merges --root 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\ttest_path_is_missing shallow-repo/.git/rebase-merge\n+'\n+\n+test_expect_success 'rebase --apply of a range containing a shallow boundary is refused' '\n+\tgit -C shallow-repo checkout -q --detach C &&\n+\tgit -C shallow-repo checkout -q --orphan unrelated &&\n+\tgit -C shallow-repo rm -rfq . &&\n+\ttest_commit -C shallow-repo unrelated &&\n+\tgit -C shallow-repo checkout -q --detach C &&\n+\tgit -C shallow-repo rev-parse C >expect &&\n+\ttest_must_fail git -C shallow-repo rebase --apply \\\n+\t\t--onto unrelated unrelated 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\ttest_path_is_missing shallow-repo/.git/rebase-apply &&\n+\tgit -C shallow-repo rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'rebase --apply restores an autostash when refusing' '\n+\techo dirty >shallow-repo/C.t &&\n+\ttest_must_fail git -C shallow-repo rebase --apply --autostash \\\n+\t\t--onto unrelated unrelated 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\techo dirty >expect &&\n+\ttest_cmp expect shallow-repo/C.t &&\n+\tgit -C shallow-repo stash list >stashes &&\n+\ttest_must_be_empty stashes &&\n+\tgit -C shallow-repo checkout -q -- C.t\n+'\n+\n+test_expect_success 'rebase --apply that does not replay the boundary is unaffected' '\n+\tgit -C shallow-repo checkout -q -B apply-topic C &&\n+\ttest_commit -C shallow-repo E &&\n+\tgit -C shallow-repo rebase --apply --onto unrelated C &&\n+\ttest_path_is_file shallow-repo/E.t\n+'\n+\n+test_expect_success 'rebase that does not replay the boundary is unaffected' '\n+\tgit -C shallow-repo checkout -q -B topic C &&\n+\ttest_commit -C shallow-repo D &&\n+\tgit -C shallow-repo rebase C\n+'\n+\n+test_expect_success 'rebase restores an autostash when refusing (merge backend)' '\n+\tgit -C shallow-repo checkout -q --detach C &&\n+\techo dirty >shallow-repo/C.t &&\n+\ttest_must_fail git -C shallow-repo rebase --autostash --root 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\ttest_path_is_missing shallow-repo/.git/rebase-merge &&\n+\techo dirty >expect &&\n+\ttest_cmp expect shallow-repo/C.t &&\n+\tgit -C shallow-repo stash list >stashes &&\n+\ttest_must_be_empty stashes &&\n+\tgit -C shallow-repo checkout -q -- C.t\n+'\n+\n+test_expect_success 'interactive rebase restores an autostash when refusing' '\n+\techo dirty >shallow-repo/C.t &&\n+\ttest_must_fail env GIT_SEQUENCE_EDITOR=: \\\n+\t\tgit -C shallow-repo rebase -i --autostash --root 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\ttest_path_is_missing shallow-repo/.git/rebase-merge &&\n+\techo dirty >expect &&\n+\ttest_cmp expect shallow-repo/C.t &&\n+\tgit -C shallow-repo stash list >stashes &&\n+\ttest_must_be_empty stashes &&\n+\tgit -C shallow-repo checkout -q -- C.t\n+'\n+\n+test_expect_success 'rebase onto new upstream work in a shallow clone still works' '\n+\tgit clone --no-local --depth=1 \"file://$(pwd)/shallow-src\" shallow-local &&\n+\ttest_commit -C shallow-local local &&\n+\ttest_commit -C shallow-src upstream &&\n+\tgit -C shallow-local fetch origin &&\n+\tgit -C shallow-local rebase origin/HEAD &&\n+\ttest_path_is_file shallow-local/upstream.t &&\n+\ttest_path_is_file shallow-local/local.t\n+'\n+\n+test_expect_success 'single-commit shallow clone: revert and cherry-pick are refused' '\n+\tgit init one-src &&\n+\ttest_commit -C one-src only &&\n+\tgit clone --no-local --depth=1 \"file://$(pwd)/one-src\" one-shallow &&\n+\tgit -C one-shallow rev-parse HEAD >expect &&\n+\ttest_must_fail git -C one-shallow revert --no-edit HEAD 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\ttest_must_fail git -C one-shallow cherry-pick -n HEAD 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\ttest_path_is_file one-shallow/only.t &&\n+\tgit -C one-shallow rev-parse HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'revert and cherry-pick work once the clone is unshallowed' '\n+\tgit clone --no-local --depth=2 \"file://$(pwd)/shallow-src\" unshallowed &&\n+\ttest_must_fail git -C unshallowed revert --no-edit HEAD~1 2>err &&\n+\ttest_grep \"shallow boundary commit\" err &&\n+\tgit -C unshallowed fetch --unshallow &&\n+\tgit -C unshallowed revert --no-edit C &&\n+\ttest_path_is_missing unshallowed/C.t &&\n+\tgit -C unshallowed cherry-pick C &&\n+\ttest_path_is_file unshallowed/C.t\n+'\n+\n+test_expect_success '--allow-shallow-boundary cannot be combined with --abort' '\n+\ttest_must_fail git -C shallow-repo revert --abort \\\n+\t\t--allow-shallow-boundary 2>err &&\n+\ttest_grep \"cannot be used with\" err\n+'\n+\n test_done\n\nbase-commit: 50fe5cc63f9cd6e89b87a2c9ccf96a444f4cfdb7\n-- \ngitgitgadget\n\n"},{"id":"557035","messageId":"20261031171834.64081-1-vsnakul@gmail.com","threadId":"66616","inReplyTo":"7179f371-a709-476f-8e94-807d7b62ce6e@gmail.com","subject":"Re: [PATCH] sequencer: refuse to revert shallow boundary commits by default","fromName":"Nakul V S","fromEmail":"vsnakul@gmail.com","sentAt":"2026-10-31T17:18:34Z","receivedAt":"2026-10-31T17:18:34Z","isPatch":true,"sender":{"key":"vsnakul@gmail.com","avatar":null},"body":"Hi Phillip,\n\nThanks for the review. I've sent a v2 that addresses it:\nhttps://lore.kernel.org/git/pull.2273.v2.git.1793455522168.gitgitgadget@gmail.com\n\n> I wonder if it would be better to refuse to cherry-pick and rebase a\n> shallow boundary commit as well.\n\nYes, I think so. v2 covers revert, cherry-pick and rebase, including\nthe \"apply\" backend of rebase.\n\n> Do you know of a sensible use for reverting a shallow boundary or is\n> the new option added out of caution in case someone wants it?\n\nHonestly it was out of caution.I can't think of a real use for it. v2 keeps --allow-shallow-boundary for revert and cherry-pick only, and rebase always refuses. If you'd\nrather not have an escape hatch at all, I'm happy to drop it.\n\n> For such a rarely used option I don't think we need to add it to the\n> synopsis\n\nFair enough, it's out of the synopsis now.\n\n> It would be good to extract this into a helper function so that we\n> don't have to copy the code to detect a shallow boundary each time we\n> need to detect one.\n\nDone. There is one helper now, so the error message lives in a single\nplace.\n\nThanks,\nNakul\n\n"},{"id":"557036","messageId":"20261031172238.66306-1-vsnakul@gmail.com","threadId":"66616","inReplyTo":"235e7dcd-2d29-47e4-b68f-9b02f80085dd@gmail.com","subject":"Re: [PATCH] sequencer: refuse to revert shallow boundary commits by default","fromName":"Nakul V S","fromEmail":"vsnakul@gmail.com","sentAt":"2026-10-31T17:22:38Z","receivedAt":"2026-10-31T17:22:38Z","isPatch":true,"sender":{"key":"vsnakul@gmail.com","avatar":null},"body":"Hi Phillip,\n\n> From the user's point of view, I think do_pick_commit() is a bit late\n> to error out. It would be nicer to refuse to add a shallow commit to\n> the todo list in the first place which means we'd need a helper\n> function that's called from sequencer_make_script(),\n> make_script_with_merges(), single_pick() and\n> sequencer_pick_revisions().\n\nYou're right, and I hadn't thought about it from the user's side. I\ntried \"git revert <tip> <boundary>\" with v1. It reverted the tip, made\na commit, then failed on the boundary and left .git/sequencer behind.\nThat's a messy state to be left in.\n\nSo v2 does what you describe. The check is made where each command\nbuilds its list of commits, and they all call the one helper. If any\nof the commits is a boundary, the command refuses up front: no commit\nis created, HEAD doesn't move, and there is nothing to abort.\n\n\"rebase --apply\" doesn't go through the sequencer, so it gets a small\ncheck of its own before it rewinds HEAD. That check does nothing in a\nrepository that isn't shallow.\n\nOne thing to be aware of: if someone types a \"pick\" or \"revert\" line\nfor a boundary commit into the rebase todo list by hand, it won't be\ncaught, because the check runs when the list is generated and not\nafter the editor. Git doesn't catch that today either, and the person\nwould have to type the hash on purpose, so I think it's an acceptable\nprice for failing early. If you'd prefer, I can add a second check in\ndo_pick_commit() as well.\n\n\nThanks,\nNakul\n\n"},{"id":"557038","messageId":"20261031172900.69612-1-vsnakul@gmail.com","threadId":"66616","inReplyTo":"xmqqqzhina26.fsf@gitster.g","subject":"Re: [PATCH] sequencer: refuse to revert shallow boundary commits by default","fromName":"Nakul V S","fromEmail":"vsnakul@gmail.com","sentAt":"2026-10-31T17:29:00Z","receivedAt":"2026-10-31T17:29:00Z","isPatch":true,"sender":{"key":"vsnakul@gmail.com","avatar":null},"body":"Hi Junio,\n\nThanks for the review, and for the question.\n\n> The next natural question is where the modification should go if we\n> want to make sure we protect these shallow boundaries not just from\n> revert but also from \"cherry-pick\", \"rebase\", and possibly \"commit\n> --amend\".  Is sequencer.c:do_pick_commit() the right place to cover\n> all of them?\n\nNo, it isn't. When I wrote v1 I was only thinking about \"revert\",\nsince that is what the original thread was about. do_pick_commit() is\nwhere every pick and revert ends up, so it looked like the easiest\nplace for a single check. I honestly didn't think about cherry-pick or\nrebase at that point, or about how late that is for the user.\n\nv2 moves it. The check now happens where each command builds its list\nof commits, and they all share one helper. \"rebase --apply\" doesn't go\nthrough the sequencer, so it gets a small check of its own before HEAD\nis touched.\n\nI've left \"git commit --amend\" alone for now. It is a different code\npath, so I'd rather do it as a separate patch than make this one\nbigger. Let me know if you'd prefer it in the same series.\n\nv2 is here:\nhttps://lore.kernel.org/git/pull.2273.v2.git.1793455522168.gitgitgadget@gmail.com\n\nThanks,\nNakul\n\n"},{"id":"557053","messageId":"xmqqwlqx1dx0.fsf@gitster.g","threadId":"66616","inReplyTo":"pull.2273.v2.git.1793455522168.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] sequencer: refuse to pick or revert shallow boundary commits","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-11-01T02:30:35Z","receivedAt":"2026-11-01T02:30:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Nakul V S via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>     Changes since v1:\n>     \n>      * Also refuse cherry-pick and rebase (merge, interactive and --apply\n>        backends), as suggested by Phillip.\n>      * Check when the list of commits is built rather than in\n>        do_pick_commit(), so a refused command leaves no state behind. Uses a\n>        single shared helper.\n>      * --allow-shallow-boundary is now accepted by cherry-pick as well, and\n>        is saved across --continue.\n>      * Dropped the option from the revert synopsis.\n>      * rebase --autostash restores the user's changes if the rebase is\n>        refused.\n\nAll of these sounds sensible.\n\n>      * Rebased onto current master.\n\nWas it necessary?  On which commit was the previous round based?\nHas there been any changes that made it necessary to rebase since\nthen?\n\n>     Not covered here: git commit --amend, and pick/revert lines typed into\n>     the rebase todo by hand.\n\nIt is OK to start small and extend coverage step by step, as we are\nmaking strict improvements.  Do we however have some vision on how\nthese would later be supported?\n\n> +\telse if (!strcmp(key, \"options.allow-shallow-boundary\"))\n> +\t\topts->allow_shallow_boundary =\n> +\t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n\nHmph, why not git_config_bool()?\n\nUnlike commit.verbose or status.submodulesummary, I didn't expect\nthis to have anything more than a simple \"yes we allow it\" or \"no\ndon't allow it\" answer.  \"all the other boolean neighbors somehow\nuse it\" is an understandable answer, but I expect people to do\nbetter than that.\n\nOverall things seem to be in a good shape.  Except for tiny details\nlike this one.\n\nThanks.\n\n"},{"id":"557085","messageId":"xmqqbj881l2j.fsf@gitster.g","threadId":"66616","inReplyTo":"pull.2273.v2.git.1793455522168.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] sequencer: refuse to pick or revert shallow boundary commits","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-11-01T18:08:20Z","receivedAt":"2026-11-01T18:08:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Nakul V S via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Nakul V S <vsnakul@gmail.com>\n>\n> In a shallow repository, the commits at the edge of the downloaded\n> history are recorded in $GIT_DIR/shallow and have their parents hidden\n> from the revision machinery.  Such a \"shallow boundary\" commit looks\n> like a root commit that added every file in its tree at once.\n>\n> Reverting it therefore removes every tracked file, and cherry-picking\n> or rebasing it adds all of them again.  In a \"git clone --depth=1\" this\n> is the only commit there is, so \"git revert HEAD\" empties the working\n> tree, which is almost never what the user meant.\n>\n> Refuse to do so by default for \"revert\", \"cherry-pick\" and \"rebase\".\n> For \"revert\" and \"cherry-pick\" only, add --allow-shallow-boundary to\n> override the check; it is remembered across --continue.  \"rebase\"\n> always refuses, and the error message suggests \"git fetch --unshallow\"\n> instead.\n>\n> The check is made when the list of commits to replay is built, not when\n> each commit is picked, so a command that is going to be refused does so\n> before it has done anything: no commit is created, HEAD and the working\n> tree are untouched, and no sequencer or rebase state is left behind for\n> the user to clean up.\n>\n> The \"apply\" backend of \"rebase\" does not go through the sequencer, so\n> it checks the commits \"git format-patch\" is about to turn into patches\n> before it rewinds HEAD.  This is skipped altogether in a repository\n> that is not shallow.\n>\n> A \"pick\" or \"revert\" line for a shallow boundary commit that the user\n> types into the todo list by hand is not checked, as the check is not\n> made again after the todo list has been edited.  \"git commit --amend\"\n> is also left alone for now.\n>\n> Signed-off-by: Nakul V S <vsnakul@gmail.com>\n> ---\n>     sequencer: refuse to pick or revert shallow boundary commits by default\n>     \n>     Changes since v1:\n>     \n>      * Also refuse cherry-pick and rebase (merge, interactive and --apply\n>        backends), as suggested by Phillip.\n>      * Check when the list of commits is built rather than in\n>        do_pick_commit(), so a refused command leaves no state behind. Uses a\n>        single shared helper.\n>      * --allow-shallow-boundary is now accepted by cherry-pick as well, and\n>        is saved across --continue.\n>      * Dropped the option from the revert synopsis.\n>      * rebase --autostash restores the user's changes if the rebase is\n>        refused.\n>      * Rebased onto current master.\n>     \n>     Not covered here: git commit --amend, and pick/revert lines typed into\n>     the rebase todo by hand.\n>\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2273%2Fvsnakul%2Fsequencer-shallow-boundary-check-v2\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2273/vsnakul/sequencer-shallow-boundary-check-v2\n> Pull-Request: https://github.com/gitgitgadget/git/pull/2273\n>\n> Range-diff vs v1:\n\nFor a single-patch series a large range-diff like this before the\npatch makes it very distracting.  Isn't there a way to tell\nGitGitGadget not to do this (or perhaps add it after the patch)?\n\n> diff --git a/shallow.c b/shallow.c\n> index 8e244a5669..bc31ff5232 100644\n> --- a/shallow.c\n> +++ b/shallow.c\n> @@ -92,6 +92,15 @@ int is_repository_shallow(struct repository *r)\n>  \treturn r->parsed_objects->is_shallow;\n>  }\n>  \n> +int is_shallow_boundary_commit(struct repository *r, const struct object_id *oid)\n> +{\n> +\tstruct commit_graft *graft;\n> +\tif (!is_repository_shallow(r))\n> +\t\treturn 0;\n> +\tgraft = lookup_commit_graft(r, oid);\n> +\treturn graft && graft->nr_parent < 0;\n> +}\n> +\n\nPlease check how badly your change conflicts with at least 'next'\nand 'seen' before sending any non-trivial patch.  Doing so would\nhave shown you that you do not need to write this function, and\nshould instead discuss the best course to resolve the conflict with\nHarald, who wrote f8bc48ea42 (object-name: explain why <rev>~N fails\nin a shallow clone, 2026-09-23).\n\nThat other topic is currently stagnant.  You can see how topics are\ndoing by visiting the mailing list archive.  For example, a trial\nmerge of your patch into 'seen' would have shown that Harald's\nf8bc48ea42 is highly relevant.  Using its Message-ID, you can find\nit on the lore archive:\n\n  https://lore.kernel.org/git/pull.2413.v3.git.git.1790189759802.gitgitgadget@gmail.com/\n\nThe archive shows that v3 was posted without replying to reviews on\nv2, and no new reviewers have shown interest since then, leaving the\ntopic stuck.\n\nThe best path forward is for someone with a stake in getting this\ntopic unstuck, such as yourself, to provide a fresh review.  This\ncan help push it to the finish line, or help us realize that it is\nnot viable so the topic can be dropped.  In the former case, you can\nreuse the function introduced by that topic in your patch.  In the\nlatter case, we no longer need to worry about the two topics\nconflicting.\n\nThanks.\n\n"},{"id":"557440","messageId":"5c68ca5e-bee7-42c7-9693-2e1e941abc81@gmail.com","threadId":"66616","inReplyTo":"pull.2273.v2.git.1793455522168.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] sequencer: refuse to pick or revert shallow boundary commits","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-11-04T14:22:21Z","receivedAt":"2026-11-04T14:22:21Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 31/10/2026 14:05, Nakul V S via GitGitGadget wrote:\n>\n>      Changes since v1:\n>      >       * Also refuse cherry-pick and rebase (merge, interactive and --apply\n>         backends), as suggested by Phillip.\n>       * Check when the list of commits is built rather than in\n>         do_pick_commit(), so a refused command leaves no state behind. Uses a\n>         single shared helper.\n>       * --allow-shallow-boundary is now accepted by cherry-pick as well, and\n>         is saved across --continue.\n>       * Dropped the option from the revert synopsis.\n>       * rebase --autostash restores the user's changes if the rebase is\n>         refused.\n\nSounds good\n\n>      Not covered here: git commit --amend, and pick/revert lines typed into\n>      the rebase todo by hand.\n\nAs Junio said, we can add those later.\n\n> diff --git a/Documentation/git-cherry-pick.adoc b/Documentation/git-cherry-pick.adoc\n> index f4cd8b9db7..ff6f1bf01d 100644\n> --- a/Documentation/git-cherry-pick.adoc\n> +++ b/Documentation/git-cherry-pick.adoc\n> @@ -178,6 +178,15 @@ fail unless one of `--empty=keep` or `--allow-empty` are specified.\n>   \tPass the merge strategy-specific option through to the\n>   \tmerge strategy.  See linkgit:git-merge[1] for details.\n>   > +--allow-shallow-boundary::\n> +\tBy default, 'git cherry-pick' refuses to pick a commit that is a\n> +\tshallow boundary (i.e. a commit listed in `.git/shallow`).\n> +\tIts parents are missing from this repository, so it looks like\n> +\ta root commit that added every file in its tree, and picking it\n> +\twould add all of them.  This option overrides that safeguard.\n\nUnless someone can come up with a realistic use for this option I'd be inclined to drop it.\n\n> diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc\n> index 6536ebdc90..ed2ce2d46a 100644\n> --- a/Documentation/git-rebase.adoc\n> +++ b/Documentation/git-rebase.adoc\n> @@ -822,6 +822,13 @@ repository that you share.\n>   See also <<RECOVERING_FROM_UPSTREAM_REBASE,RECOVERING FROM UPSTREAM REBASE>>\n>   below.\n>   > +If the commits to be rebased include a shallow boundary commit (a\n> +commit listed in `.git/shallow`), the rebase refuses to start, because\n> +the missing parents make that commit look like a root commit that added\n\nI think\n\tbecause it looks like a root commit. Run `git fetch --unshallow`\n\tto fetch the missing history first.\n\nwould be sufficient\n\n> +every file in its tree.  This applies to both the `merge` and `apply`\n> +backends, and nothing is changed when the rebase is refused.  Run\n> +`git fetch --unshallow` to get the missing history first.\n> +\n\n> diff --git a/Documentation/git-revert.adoc b/Documentation/git-revert.adoc\n> index ffba365e63..288eddcf6e 100644\n> --- a/Documentation/git-revert.adoc\n> +++ b/Documentation/git-revert.adoc\n> @@ -122,6 +122,16 @@ include::rerere-options.adoc[]\n>   \tconfiguration variable can be used to enable this option by\n>   \tdefault.\n>   > +--allow-shallow-boundary::\n> +\tBy default, 'git revert' refuses to revert a commit that is a\n> +\tshallow boundary (i.e. a commit listed in `.git/shallow`).\n> +\tIts parents are missing from this repository, so it looks like\n> +\ta root commit that added every file in its tree, and reverting\n> +\tit would remove all of them.  This option overrides that\n> +\tsafeguard.\n\nAs above, do we really need this option?\n\n> diff --git a/builtin/rebase.c b/builtin/rebase.c\n> index 535db60181..2305a3a495 100644\n> --- a/builtin/rebase.c\n> +++ b/builtin/rebase.c\n> @@ -34,6 +34,7 @@\n>   #include \"sequencer.h\"\n>   #include \"rebase-interactive.h\"\n>   #include \"reset.h\"\n> +#include \"shallow.h\"\n>   #include \"trace2.h\"\n>   #include \"hook.h\"\n>   #include \"trailer.h\"\n> @@ -288,6 +289,8 @@ static int init_basic_state(struct replay_opts *opts, const char *head_name,\n>   \treturn write_basic_state(opts, head_name, onto, orig_head);\n>   }\n>   > +static int cleanup_autostash(struct rebase_options *opts);\n> +\n\nLets move cleaunup_autostash() in a separate, preparatory, commit, rather than littering the code with unnecessary forward declarations.\n\n>   static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)\n>   {\n>   \tint ret = -1;\n> @@ -309,6 +312,8 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)\n>   \t\t\t\t    &make_script_args, flags);\n>   \tif (ret) {\n>   \t\terror(_(\"could not generate todo list\"));\n> +\t\t/* Nothing has been started yet; put back what we stashed */\n> +\t\tcleanup_autostash(opts);\n\nIs this related to the other changes in this patch?\n\n>   \t\tgoto cleanup;\n>   \t}\n>   > @@ -618,6 +623,55 @@ static int move_to_original_branch(struct rebase_options *opts)\n>   \treturn ret;\n>   }\n>   > +/*\n> + * The \"apply\" backend does not go through the sequencer, so check here that\n> + * none of the commits \"git format-patch\" is about to turn into patches is a\n> + * shallow boundary commit.  This walks the same set of commits as run_am().\n> + */\n> +static int check_no_shallow_boundary_to_apply(struct rebase_options *opts)\n> +{\n> +\tstruct rev_info revs;\n> +\tstruct strvec args = STRVEC_INIT;\n> +\tstruct commit *commit;\n> +\tint ret = 0;\n> +\n> +\tif (!is_repository_shallow(the_repository))\n> +\t\treturn 0;\n\nIt's good that this returns early in non-shallow repositories, but cherry-pick detection is expensive as we have to calculate the diff of each commit on both sides of the merge-base. Ideally we'd only walk the commits once and feed the commit object ids into\n\n\tgit format-patch --stdin --no-walk=unsorted ...\n\nto generate the patches. The way the code is currently structured makes that slightly tricky as it checks out \"onto\" before generating the patches. I think we could move the checkout of \"onto\" into run_am() so that it is separate to the fast-forward case in cmd_rebase(). Then we could generate the patches and check for shallow grafts with a single revision walk before checking out \"onto\".\n\n> diff --git a/sequencer.c b/sequencer.c\n> index 508ec71792..8d38ac3878 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n>   > +/*\n> + * A shallow boundary commit has had its parents hidden, so it looks like\n> + * a root commit that added every file at once.  Picking or reverting it\n> + * would not do what the user expects, so refuse before anything is\n> + * started.\n> + */\n> +int sequencer_refuse_shallow_boundary(struct repository *r,\n> +\t\t\t\t      const struct commit *commit, int allow)\n> +{\n> +\tif (allow || !is_shallow_boundary_commit(r, &commit->object.oid))\n> +\t\treturn 0;\n> +\treturn error(_(\"commit %s is a shallow boundary commit; its parents \"\n> +\t\t       \"are missing from this clone (use 'git fetch --unshallow' \"\n> +\t\t       \"to get them)\"),\n\nI wonder if it would be helpful to add a hint explaining why we refuse to cherry-pick, revert or rebase shallow boundary commits. There is nothing specific to the sequencer here, so if we want to add checks to other commands like \"git commit --amend\" in the future, maybe this should live in shallow.c? Alternatively we may want to use separate, more specific messages in each case.\n\n> +\t\t     oid_to_hex(&commit->object.oid));\n> +}\n> +\n>   static int walk_revs_populate_todo(struct todo_list *todo_list,\n>   \t\t\t\tstruct replay_opts *opts)\n>   {\n> @@ -3481,13 +3502,18 @@ static int walk_revs_populate_todo(struct todo_list *todo_list,\n>   \tencoding = get_log_output_encoding();\n>   >   \twhile ((commit = get_revision(opts->revs))) {\n> -\t\tstruct todo_item *item = append_new_todo(todo_list);\n> -\t\tconst char *commit_buffer = repo_logmsg_reencode(the_repository,\n> -\t\t\t\t\t\t\t\t commit, NULL,\n> -\t\t\t\t\t\t\t\t encoding);\n> +\t\tstruct todo_item *item;\n> +\t\tconst char *commit_buffer;\n>   \t\tconst char *subject;\n>   \t\tint subject_len;\n>   > +\t\tif (sequencer_refuse_shallow_boundary(the_repository, commit,\n> +\t\t\t\t\t    opts->allow_shallow_boundary))\n> +\t\t\treturn -1;\n> +\n> +\t\titem = append_new_todo(todo_list);\n> +\t\tcommit_buffer = repo_logmsg_reencode(the_repository, commit,\n> +\t\t\t\t\t\t     NULL, encoding);\n\nThis covers cherry-picking or reverting multiple commits\n\n> @@ -5692,6 +5721,9 @@ static int single_pick(struct repository *r,\n>   \tint check_todo;\n>   \tstruct todo_item item;\n>   > +\tif (sequencer_refuse_shallow_boundary(r, cmit, opts->allow_shallow_boundary))\n> +\t\treturn -1;\n\nThis covers cherry-picking or reverting a single commit.\n\n>   \titem.command = opts->action == REPLAY_PICK ?\n>   \t\t\tTODO_PICK : TODO_REVERT;\n>   \titem.commit = cmit;\n> @@ -6078,6 +6110,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n>   \tint rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;\n>   \tint root_with_onto = flags & TODO_LIST_ROOT_WITH_ONTO;\n>   \tint skipped_commit = 0;\n> +\tint ret = 0;\n>   \tstruct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;\n>   \tstruct strbuf label_from_message = STRBUF_INIT;\n>   \tstruct commit_list *commits = NULL, **tail = &commits, *iter;\n> @@ -6141,6 +6174,10 @@ static int make_script_with_merges(struct pretty_print_context *pp,\n>   \t\t}\n>   \t\tif (is_empty && !keep_empty)\n>   \t\t\tcontinue;\n> +\t\tif (sequencer_refuse_shallow_boundary(the_repository, commit, 0)) {\n> +\t\t\tret = -1;\n> +\t\t\tgoto cleanup;\n> +\t\t}\n\nThis covers \"git rebase -r\", and on error we cleanup properly - good\n\n> @@ -6394,6 +6432,10 @@ int sequencer_make_script(struct repository *r, struct strbuf *out,\n>   \t\t}\n>   \t\tif (is_empty && !keep_empty)\n>   \t\t\tcontinue;\n> +\t\tif (sequencer_refuse_shallow_boundary(r, commit, 0)) {\n> +\t\t\tret = -1;\n> +\t\t\tgoto cleanup;\n> +\t\t}\n\nThis covers a regular rebase.\n\n\n> diff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh\n> index 939e7a16a6..49da8427cf 100755\n> --- a/t/t3501-revert-cherry-pick.sh\n> +++ b/t/t3501-revert-cherry-pick.sh\n> @@ -256,4 +256,212 @@ test_expect_success 'cherry-pick is unaware of --reference (for now)' '\n>   \ttest_grep \"^usage: git cherry-pick\" actual\n>   '\n>   > +# Tests for shallow boundary safeguard\n\nThere are an awful lot of new tests here, many more than there are places where we check for a shallow boundary. It would be a good idea to check whether each test actually adds any new coverage. Also the rebase tests should live in one of the rebase test files (maybe t3400?)\n\nThanks for re-rolling, this is definitely moving in the right direction.\n\nThanks\n\nPhillip\n\n> +test_expect_success 'setup shallow boundary tests' '\n> +\tgit init shallow-src &&\n> +\ttest_commit -C shallow-src A &&\n> +\ttest_commit -C shallow-src B &&\n> +\ttest_commit -C shallow-src C &&\n> +\tgit clone --no-local --depth=2 \"file://$(pwd)/shallow-src\" shallow-repo &&\n> +\tgit -C shallow-repo rev-parse --verify B >boundary &&\n> +\ttest_cmp shallow-repo/.git/shallow boundary\n> +'\n> +\n> +test_expect_success 'revert of shallow boundary commit is refused' '\n> +\tgit -C shallow-repo reset --hard C &&\n> +\ttest_must_fail git -C shallow-repo revert --no-edit B 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\ttest_path_is_missing shallow-repo/.git/sequencer &&\n> +\ttest_path_is_missing shallow-repo/.git/REVERT_HEAD &&\n> +\tgit -C shallow-repo rev-parse C >expect &&\n> +\tgit -C shallow-repo rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'cherry-pick of shallow boundary commit is refused' '\n> +\tgit -C shallow-repo checkout -q --detach C &&\n> +\tgit -C shallow-repo rm -rq . &&\n> +\tgit -C shallow-repo commit -q -m empty &&\n> +\tgit -C shallow-repo rev-parse HEAD >expect &&\n> +\ttest_must_fail git -C shallow-repo cherry-pick B 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\tgit -C shallow-repo rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual &&\n> +\tgit -C shallow-repo reset -q --hard C\n> +'\n> +\n> +test_expect_success 'revert of non-boundary commit in shallow repo works' '\n> +\tgit -C shallow-repo revert --no-edit C &&\n> +\tgit -C shallow-repo reset -q --hard C\n> +'\n> +\n> +test_expect_success 'multi-commit revert is refused before anything is reverted' '\n> +\ttest_must_fail git -C shallow-repo revert --no-edit C B 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\ttest_path_is_missing shallow-repo/.git/sequencer &&\n> +\tgit -C shallow-repo rev-parse C >expect &&\n> +\tgit -C shallow-repo rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '--allow-shallow-boundary overrides the revert check' '\n> +\tgit -C shallow-repo revert --no-edit --allow-shallow-boundary B &&\n> +\ttest_path_is_missing shallow-repo/B.t &&\n> +\tgit -C shallow-repo reset -q --hard C\n> +'\n> +\n> +test_expect_success '--allow-shallow-boundary overrides the cherry-pick check' '\n> +\tgit -C shallow-repo checkout -q --detach C &&\n> +\tgit -C shallow-repo rm -rq . &&\n> +\tgit -C shallow-repo commit -q -m empty &&\n> +\tgit -C shallow-repo cherry-pick --allow-shallow-boundary B &&\n> +\ttest_path_is_file shallow-repo/B.t &&\n> +\tgit -C shallow-repo reset -q --hard C\n> +'\n> +\n> +test_expect_success '--allow-shallow-boundary survives --continue' '\n> +\ttest_commit -C shallow-repo conflict-1 file one &&\n> +\ttest_commit -C shallow-repo conflict-2 file two &&\n> +\ttest_must_fail git -C shallow-repo revert --no-edit \\\n> +\t\t--allow-shallow-boundary conflict-1 B &&\n> +\techo resolved >shallow-repo/file &&\n> +\tgit -C shallow-repo add file &&\n> +\tgit -C shallow-repo revert --continue &&\n> +\ttest_path_is_missing shallow-repo/B.t &&\n> +\tgit -C shallow-repo reset -q --hard C\n> +'\n> +\n> +test_expect_success 'revert and cherry-pick in a non-shallow repo are unaffected' '\n> +\tgit init full-repo &&\n> +\ttest_commit -C full-repo A &&\n> +\ttest_commit -C full-repo B &&\n> +\tgit -C full-repo revert --no-edit B &&\n> +\tgit -C full-repo cherry-pick B &&\n> +\ttest_path_is_file full-repo/B.t\n> +'\n> +\n> +test_expect_success 'rebase of a range containing a shallow boundary is refused' '\n> +\tgit -C shallow-repo checkout -q --detach C &&\n> +\tgit -C shallow-repo rev-parse C >expect &&\n> +\ttest_must_fail git -C shallow-repo rebase --root 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\ttest_path_is_missing shallow-repo/.git/rebase-merge &&\n> +\tgit -C shallow-repo rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'rebase --rebase-merges is refused as well' '\n> +\ttest_must_fail git -C shallow-repo rebase --rebase-merges --root 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\ttest_path_is_missing shallow-repo/.git/rebase-merge\n> +'\n> +\n> +test_expect_success 'rebase --apply of a range containing a shallow boundary is refused' '\n> +\tgit -C shallow-repo checkout -q --detach C &&\n> +\tgit -C shallow-repo checkout -q --orphan unrelated &&\n> +\tgit -C shallow-repo rm -rfq . &&\n> +\ttest_commit -C shallow-repo unrelated &&\n> +\tgit -C shallow-repo checkout -q --detach C &&\n> +\tgit -C shallow-repo rev-parse C >expect &&\n> +\ttest_must_fail git -C shallow-repo rebase --apply \\\n> +\t\t--onto unrelated unrelated 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\ttest_path_is_missing shallow-repo/.git/rebase-apply &&\n> +\tgit -C shallow-repo rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'rebase --apply restores an autostash when refusing' '\n> +\techo dirty >shallow-repo/C.t &&\n> +\ttest_must_fail git -C shallow-repo rebase --apply --autostash \\\n> +\t\t--onto unrelated unrelated 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\techo dirty >expect &&\n> +\ttest_cmp expect shallow-repo/C.t &&\n> +\tgit -C shallow-repo stash list >stashes &&\n> +\ttest_must_be_empty stashes &&\n> +\tgit -C shallow-repo checkout -q -- C.t\n> +'\n> +\n> +test_expect_success 'rebase --apply that does not replay the boundary is unaffected' '\n> +\tgit -C shallow-repo checkout -q -B apply-topic C &&\n> +\ttest_commit -C shallow-repo E &&\n> +\tgit -C shallow-repo rebase --apply --onto unrelated C &&\n> +\ttest_path_is_file shallow-repo/E.t\n> +'\n> +\n> +test_expect_success 'rebase that does not replay the boundary is unaffected' '\n> +\tgit -C shallow-repo checkout -q -B topic C &&\n> +\ttest_commit -C shallow-repo D &&\n> +\tgit -C shallow-repo rebase C\n> +'\n> +\n> +test_expect_success 'rebase restores an autostash when refusing (merge backend)' '\n> +\tgit -C shallow-repo checkout -q --detach C &&\n> +\techo dirty >shallow-repo/C.t &&\n> +\ttest_must_fail git -C shallow-repo rebase --autostash --root 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\ttest_path_is_missing shallow-repo/.git/rebase-merge &&\n> +\techo dirty >expect &&\n> +\ttest_cmp expect shallow-repo/C.t &&\n> +\tgit -C shallow-repo stash list >stashes &&\n> +\ttest_must_be_empty stashes &&\n> +\tgit -C shallow-repo checkout -q -- C.t\n> +'\n> +\n> +test_expect_success 'interactive rebase restores an autostash when refusing' '\n> +\techo dirty >shallow-repo/C.t &&\n> +\ttest_must_fail env GIT_SEQUENCE_EDITOR=: \\\n> +\t\tgit -C shallow-repo rebase -i --autostash --root 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\ttest_path_is_missing shallow-repo/.git/rebase-merge &&\n> +\techo dirty >expect &&\n> +\ttest_cmp expect shallow-repo/C.t &&\n> +\tgit -C shallow-repo stash list >stashes &&\n> +\ttest_must_be_empty stashes &&\n> +\tgit -C shallow-repo checkout -q -- C.t\n> +'\n> +\n> +test_expect_success 'rebase onto new upstream work in a shallow clone still works' '\n> +\tgit clone --no-local --depth=1 \"file://$(pwd)/shallow-src\" shallow-local &&\n> +\ttest_commit -C shallow-local local &&\n> +\ttest_commit -C shallow-src upstream &&\n> +\tgit -C shallow-local fetch origin &&\n> +\tgit -C shallow-local rebase origin/HEAD &&\n> +\ttest_path_is_file shallow-local/upstream.t &&\n> +\ttest_path_is_file shallow-local/local.t\n> +'\n> +\n> +test_expect_success 'single-commit shallow clone: revert and cherry-pick are refused' '\n> +\tgit init one-src &&\n> +\ttest_commit -C one-src only &&\n> +\tgit clone --no-local --depth=1 \"file://$(pwd)/one-src\" one-shallow &&\n> +\tgit -C one-shallow rev-parse HEAD >expect &&\n> +\ttest_must_fail git -C one-shallow revert --no-edit HEAD 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\ttest_must_fail git -C one-shallow cherry-pick -n HEAD 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\ttest_path_is_file one-shallow/only.t &&\n> +\tgit -C one-shallow rev-parse HEAD >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'revert and cherry-pick work once the clone is unshallowed' '\n> +\tgit clone --no-local --depth=2 \"file://$(pwd)/shallow-src\" unshallowed &&\n> +\ttest_must_fail git -C unshallowed revert --no-edit HEAD~1 2>err &&\n> +\ttest_grep \"shallow boundary commit\" err &&\n> +\tgit -C unshallowed fetch --unshallow &&\n> +\tgit -C unshallowed revert --no-edit C &&\n> +\ttest_path_is_missing unshallowed/C.t &&\n> +\tgit -C unshallowed cherry-pick C &&\n> +\ttest_path_is_file unshallowed/C.t\n> +'\n> +\n> +test_expect_success '--allow-shallow-boundary cannot be combined with --abort' '\n> +\ttest_must_fail git -C shallow-repo revert --abort \\\n> +\t\t--allow-shallow-boundary 2>err &&\n> +\ttest_grep \"cannot be used with\" err\n> +'\n> +\n>   test_done\n> > base-commit: 50fe5cc63f9cd6e89b87a2c9ccf96a444f4cfdb7\n\n\n"}]}