{"thread":{"id":"66220","subject":"[PATCH 0/2] branch: -d protects upstream branches","startedAt":"2026-08-25T21:25:19Z","lastAt":"2026-08-27T05:20:04Z","messageCount":9,"participants":["Harald Nordgren via GitGitGadget","Junio C Hamano","Harald Nordgren","Tuomas Ahola","Elijah Newren"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"551245","messageId":"pull.2365.git.git.1787693117.gitgitgadget@gmail.com","threadId":"66220","inReplyTo":null,"subject":"[PATCH 0/2] branch: -d protects upstream branches","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-25T21:25:15Z","receivedAt":"2026-08-25T21:25:19Z","isPatch":true,"body":"Protect local branches from git branch -d when a surviving branch depends on\nthem through a local upstream chain.\n\nHarald Nordgren (2):\n  branch: move stacked branch helpers\n  branch: protect local upstreams from -d\n\n Documentation/git-branch.adoc |   4 +-\n builtin/branch.c              | 142 +++++++++++++++++++++-------------\n t/t1507-rev-parse-upstream.sh |   4 +-\n t/t3200-branch.sh             |  43 ++++++++++\n t/t6040-tracking-info.sh      |   2 +-\n 5 files changed, 137 insertions(+), 58 deletions(-)\n\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2365%2FHaraldNordgren%2Fbranch-d-protect-stacked-upstreams-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2365/HaraldNordgren/branch-d-protect-stacked-upstreams-v1\nPull-Request: https://github.com/git/git/pull/2365\n-- \ngitgitgadget\n"},{"id":"551246","messageId":"f7856e7a5b7b996be9380eb78d24c92b682a1fae.1787693117.git.gitgitgadget@gmail.com","threadId":"66220","inReplyTo":"pull.2365.git.git.1787693117.gitgitgadget@gmail.com","subject":"[PATCH 1/2] branch: move stacked branch helpers","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-25T21:25:16Z","receivedAt":"2026-08-25T21:25:21Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nMove the stacked branch helpers earlier so delete_branches() can use\nthem without a forward declaration.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/branch.c | 104 +++++++++++++++++++++++------------------------\n 1 file changed, 52 insertions(+), 52 deletions(-)\n\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex a613148fc7..87f0aa4051 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -203,6 +203,58 @@ enum delete_branch_flags {\n \tDELETE_BRANCH_DRY_RUN = (1 << 4),\n };\n \n+struct stacked_branch_data {\n+\tstruct strset *deletable_branch_names;\n+\tstruct strset *protected_branch_names;\n+};\n+\n+static int collect_stacked_branch_base(const struct reference *ref,\n+\t\t\t\t       void *cb_data)\n+{\n+\tstruct stacked_branch_data *data = cb_data;\n+\tconst char *branch_name;\n+\tstruct branch *branch;\n+\tconst char *upstream_refname;\n+\tconst char *upstream_branch_name;\n+\n+\tif (!skip_prefix(ref->name, \"refs/heads/\", &branch_name))\n+\t\tBUG(\"expected local branch ref, got '%s'\", ref->name);\n+\tif (strset_contains(data->deletable_branch_names, branch_name))\n+\t\treturn 0;\n+\n+\tbranch = branch_get(branch_name);\n+\tupstream_refname = branch_get_upstream(branch, NULL);\n+\tif (!upstream_refname ||\n+\t    !skip_prefix(upstream_refname, \"refs/heads/\",\n+\t\t\t &upstream_branch_name) ||\n+\t    !strset_contains(data->deletable_branch_names,\n+\t\t\t    upstream_branch_name))\n+\t\treturn 0;\n+\n+\tstrset_add(data->protected_branch_names, upstream_branch_name);\n+\treturn 0;\n+}\n+\n+static void protect_stacked_branch_bases(struct ref_store *refs,\n+\t\t\t\t\t struct strset *deletable_branch_names,\n+\t\t\t\t\t struct strset *protected_branch_names)\n+{\n+\tstruct stacked_branch_data data = {\n+\t\t.deletable_branch_names = deletable_branch_names,\n+\t\t.protected_branch_names = protected_branch_names,\n+\t};\n+\tstruct refs_for_each_ref_options opts = {\n+\t\t.prefix = \"refs/heads/\",\n+\t};\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *entry;\n+\n+\trefs_for_each_ref_ext(refs, collect_stacked_branch_base, &data, &opts);\n+\n+\tstrset_for_each_entry(protected_branch_names, &iter, entry)\n+\t\tstrset_remove(deletable_branch_names, entry->key);\n+}\n+\n static int check_branch_commit(const char *branchname, const char *refname,\n \t\t\t       const struct object_id *oid, struct commit *head_rev,\n \t\t\t       int kinds, unsigned int flags)\n@@ -718,58 +770,6 @@ static int parse_opt_forked(const struct option *opt, const char *arg, int unset\n \treturn 0;\n }\n \n-struct stacked_branch_data {\n-\tstruct strset *deletable_branch_names;\n-\tstruct strset *protected_branch_names;\n-};\n-\n-static int collect_stacked_branch_base(const struct reference *ref,\n-\t\t\t\t       void *cb_data)\n-{\n-\tstruct stacked_branch_data *data = cb_data;\n-\tconst char *branch_name;\n-\tstruct branch *branch;\n-\tconst char *upstream_refname;\n-\tconst char *upstream_branch_name;\n-\n-\tif (!skip_prefix(ref->name, \"refs/heads/\", &branch_name))\n-\t\tBUG(\"expected local branch ref, got '%s'\", ref->name);\n-\tif (strset_contains(data->deletable_branch_names, branch_name))\n-\t\treturn 0;\n-\n-\tbranch = branch_get(branch_name);\n-\tupstream_refname = branch_get_upstream(branch, NULL);\n-\tif (!upstream_refname ||\n-\t    !skip_prefix(upstream_refname, \"refs/heads/\",\n-\t\t\t &upstream_branch_name) ||\n-\t    !strset_contains(data->deletable_branch_names,\n-\t\t\t    upstream_branch_name))\n-\t\treturn 0;\n-\n-\tstrset_add(data->protected_branch_names, upstream_branch_name);\n-\treturn 0;\n-}\n-\n-static void protect_stacked_branch_bases(struct ref_store *refs,\n-\t\t\t\t\t struct strset *deletable_branch_names,\n-\t\t\t\t\t struct strset *protected_branch_names)\n-{\n-\tstruct stacked_branch_data data = {\n-\t\t.deletable_branch_names = deletable_branch_names,\n-\t\t.protected_branch_names = protected_branch_names,\n-\t};\n-\tstruct refs_for_each_ref_options opts = {\n-\t\t.prefix = \"refs/heads/\",\n-\t};\n-\tstruct hashmap_iter iter;\n-\tstruct strmap_entry *entry;\n-\n-\trefs_for_each_ref_ext(refs, collect_stacked_branch_base, &data, &opts);\n-\n-\tstrset_for_each_entry(protected_branch_names, &iter, entry)\n-\t\tstrset_remove(deletable_branch_names, entry->key);\n-}\n-\n static void clear_deleted_upstreams(struct strset *protected_branch_names,\n \t\t\t\t    struct strset *deletable_branch_names)\n {\n-- \ngitgitgadget\n\n"},{"id":"551247","messageId":"d3d7a06e3d6f0c7adf9739ca496ed4012e261ac1.1787693117.git.gitgitgadget@gmail.com","threadId":"66220","inReplyTo":"pull.2365.git.git.1787693117.gitgitgadget@gmail.com","subject":"[PATCH 2/2] branch: protect local upstreams from -d","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-25T21:25:17Z","receivedAt":"2026-08-25T21:25:22Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nA local branch may be fully merged into its own upstream while still\nserving as the base of a surviving stacked branch. Deleting it with\n\"git branch -d\" then leaves the surviving branch with a missing\nupstream.\n\nUse the existing stacked-branch protection after checking every\nrequested deletion. This makes multi-branch deletion independent of\nargument order: a branch that fails its safety check remains available\nto protect its upstream. Allow -D to override the protection, and allow\na complete stack to be deleted together.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-branch.adoc |  4 +++-\n builtin/branch.c              | 38 +++++++++++++++++++++++++++++--\n t/t1507-rev-parse-upstream.sh |  4 ++--\n t/t3200-branch.sh             | 43 +++++++++++++++++++++++++++++++++++\n t/t6040-tracking-info.sh      |  2 +-\n 5 files changed, 85 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc\nindex bfdf459329..5c2a3339b2 100644\n--- a/Documentation/git-branch.adoc\n+++ b/Documentation/git-branch.adoc\n@@ -102,7 +102,9 @@ OPTIONS\n `--delete`::\n \tDelete a branch. The branch must be fully merged in its\n \tupstream branch, or in `HEAD` if no upstream was set with\n-\t`--track` or `--set-upstream-to`.\n+\t`--track` or `--set-upstream-to`, and must not be an upstream,\n+\tdirectly or indirectly, of another local branch that will remain\n+\tafter the operation.\n \n `-D`::\n \tShortcut for `--delete --force`.\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex 87f0aa4051..7f76789027 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -294,12 +294,13 @@ static int delete_branches(int argc, const char **argv, int kinds,\n \tstruct object_id oid;\n \tchar *name = NULL;\n \tconst char *fmt;\n-\tint i;\n \tint ret = 0;\n \tint remote_branch = 0;\n \tstruct strbuf bname = STRBUF_INIT;\n \tenum interpret_branch_kind allowed_interpret;\n \tstruct string_list refs_to_delete = STRING_LIST_INIT_DUP;\n+\tstruct strset deletable_branch_names = STRSET_INIT;\n+\tstruct strset protected_branch_names = STRSET_INIT;\n \tstruct string_list_item *item;\n \tint branch_name_pos;\n \tconst char *fmt_remotes = \"refs/remotes/%s\";\n@@ -326,7 +327,7 @@ static int delete_branches(int argc, const char **argv, int kinds,\n \t    !(flags & DELETE_BRANCH_NO_HEAD_FALLBACK))\n \t\thead_rev = lookup_commit_reference(the_repository, &head_oid);\n \n-\tfor (i = 0; i < argc; i++, strbuf_reset(&bname)) {\n+\tfor (int i = 0; i < argc; i++, strbuf_reset(&bname)) {\n \t\tchar *target = NULL;\n \t\tint ref_flags = 0;\n \n@@ -397,11 +398,42 @@ static int delete_branches(int argc, const char **argv, int kinds,\n \t\titem->util = xstrdup((ref_flags & REF_ISBROKEN) ? \"broken\"\n \t\t\t\t    : (ref_flags & REF_ISSYMREF) ? target\n \t\t\t\t    : repo_find_unique_abbrev(the_repository, &oid, DEFAULT_ABBREV));\n+\t\tif (!remote_branch && !(flags & (DELETE_BRANCH_FORCE |\n+\t\t\t\t\t\t DELETE_BRANCH_SKIP_UNMERGED)))\n+\t\t\tstrset_add(&deletable_branch_names, bname.buf);\n \n \tnext:\n \t\tfree(target);\n \t}\n \n+\tif (!remote_branch &&\n+\t    !(flags & (DELETE_BRANCH_FORCE | DELETE_BRANCH_SKIP_UNMERGED)) &&\n+\t    refs_to_delete.nr) {\n+\t\tprotect_stacked_branch_bases(get_main_ref_store(the_repository),\n+\t\t\t\t\t     &deletable_branch_names, &protected_branch_names);\n+\t\tfor (size_t i = refs_to_delete.nr; i; i--) {\n+\t\t\tconst char *branch_name;\n+\n+\t\t\titem = &refs_to_delete.items[i - 1];\n+\t\t\tif (!skip_prefix(item->string, \"refs/heads/\",\n+\t\t\t\t\t &branch_name))\n+\t\t\t\tBUG(\"expected local branch ref, got '%s'\",\n+\t\t\t\t    item->string);\n+\t\t\tif (strset_contains(&deletable_branch_names, branch_name))\n+\t\t\t\tcontinue;\n+\n+\t\t\terror(_(\"the branch '%s' is an upstream of another branch\"),\n+\t\t\t      branch_name);\n+\t\t\tadvise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,\n+\t\t\t\t\t  _(\"If you are sure you want to delete it, \"\n+\t\t\t\t\t    \"run 'git branch -D %s'\"),\n+\t\t\t\t\t  branch_name);\n+\t\t\tret = 1;\n+\t\t\tunsorted_string_list_delete_item(&refs_to_delete, i - 1,\n+\t\t\t\t\t\t\t 1);\n+\t\t}\n+\t}\n+\n \tif (!(flags & DELETE_BRANCH_DRY_RUN) &&\n \t    refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))\n \t\tret = 1;\n@@ -428,6 +460,8 @@ static int delete_branches(int argc, const char **argv, int kinds,\n \t\tfree(describe_ref);\n \t}\n \tstring_list_clear(&refs_to_delete, 0);\n+\tstrset_clear(&deletable_branch_names);\n+\tstrset_clear(&protected_branch_names);\n \n \tfree(name);\n \tstrbuf_release(&bname);\ndiff --git a/t/t1507-rev-parse-upstream.sh b/t/t1507-rev-parse-upstream.sh\nindex cb9ef7e329..04abfb6f94 100755\n--- a/t/t1507-rev-parse-upstream.sh\n+++ b/t/t1507-rev-parse-upstream.sh\n@@ -146,9 +146,9 @@ test_expect_success 'merge my-side@{u} records the correct name' '\n )\n '\n \n-test_expect_success 'branch -d other@{u}' '\n+test_expect_success 'branch -D other@{u}' '\n \tgit checkout -t -b other main &&\n-\tgit branch -d @{u} &&\n+\tgit branch -D @{u} &&\n \tgit for-each-ref refs/heads/main >actual &&\n \ttest_must_be_empty actual\n '\ndiff --git a/t/t3200-branch.sh b/t/t3200-branch.sh\nindex cdb6c6a634..a3d492ffcd 100755\n--- a/t/t3200-branch.sh\n+++ b/t/t3200-branch.sh\n@@ -2173,6 +2173,49 @@ test_expect_success \"branch -d still deletes a deleteMerged=false branch\" '\n \t)\n '\n \n+test_expect_success 'branch -d keeps the upstream of a surviving branch' '\n+\tsetup_repo_for_delete_merged &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit branch foundation origin/next --track &&\n+\t\tgit checkout -b topic foundation --track &&\n+\t\tgit commit --allow-empty -m \"topic work\" &&\n+\t\tgit checkout --detach &&\n+\n+\t\ttest_must_fail git branch -d foundation 2>err &&\n+\t\ttest_grep \"branch .foundation. is an upstream of another branch\" err &&\n+\t\ttest_ref_exists refs/heads/foundation &&\n+\t\ttest_ref_exists refs/heads/topic &&\n+\n+\t\tgit branch -D foundation &&\n+\t\ttest_ref_missing refs/heads/foundation &&\n+\t\ttest_ref_exists refs/heads/topic\n+\t)\n+'\n+\n+test_expect_success 'branch -d protects a base when another deletion fails' '\n+\tsetup_repo_for_delete_merged &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit branch foundation origin/next --track &&\n+\t\tgit checkout -b topic foundation --track &&\n+\t\tgit commit --allow-empty -m \"topic work\" &&\n+\t\tgit checkout --detach &&\n+\n+\t\ttest_must_fail git branch -d foundation topic 2>err &&\n+\t\ttest_grep \"branch .foundation. is an upstream of another branch\" err &&\n+\t\ttest_grep \"branch .topic. is not fully merged\" err &&\n+\t\ttest_ref_exists refs/heads/foundation &&\n+\t\ttest_ref_exists refs/heads/topic &&\n+\n+\t\ttest_must_fail git branch -d topic foundation 2>err &&\n+\t\ttest_grep \"branch .foundation. is an upstream of another branch\" err &&\n+\t\ttest_grep \"branch .topic. is not fully merged\" err &&\n+\t\ttest_ref_exists refs/heads/foundation &&\n+\t\ttest_ref_exists refs/heads/topic\n+\t)\n+'\n+\n test_expect_success '--dry-run without --delete-merged is rejected' '\n \ttest_must_fail git -C forked branch --dry-run 2>err &&\n \ttest_grep \"requires --delete-merged\" err\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex e95d420972..01145f6681 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -34,7 +34,7 @@ test_expect_success setup '\n \t\tgit checkout -b brokenbase origin &&\n \t\tgit checkout -b b5 --track brokenbase &&\n \t\tadvance g &&\n-\t\tgit branch -d brokenbase &&\n+\t\tgit branch -D brokenbase &&\n \t\tgit checkout -b b6 origin\n \t) &&\n \tgit checkout -b follower --track main &&\n-- \ngitgitgadget\n"},{"id":"551253","messageId":"xmqq33w1lv4j.fsf@gitster.g","threadId":"66220","inReplyTo":"d3d7a06e3d6f0c7adf9739ca496ed4012e261ac1.1787693117.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/2] branch: protect local upstreams from -d","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-25T21:59:08Z","receivedAt":"2026-08-25T21:59:10Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> A local branch may be fully merged into its own upstream while still\n> serving as the base of a surviving stacked branch. Deleting it with\n> \"git branch -d\" then leaves the surviving branch with a missing\n> upstream.\n>\n> Use the existing stacked-branch protection after checking every\n> requested deletion. This makes multi-branch deletion independent of\n> argument order: a branch that fails its safety check remains available\n> to protect its upstream. Allow -D to override the protection, and allow\n> a complete stack to be deleted together.\n\nIt may be a good thing to optionally be able to do this, but\nchanging the long-established semantics of what the '-d' option\nmeans would lead to serious breakage to the end-user workflows\npeople depend on, I am afraid, and...\n\n> -test_expect_success 'branch -d other@{u}' '\n> +test_expect_success 'branch -D other@{u}' '\n>  \tgit checkout -t -b other main &&\n> -\tgit branch -d @{u} &&\n> +\tgit branch -D @{u} &&\n>  \tgit for-each-ref refs/heads/main >actual &&\n>  \ttest_must_be_empty actual\n>  '\n\n... having to adjust the test command sequence like this is a very\nclear illustration of why it is not a safe thing to do.  Our change\njust broke what the user wanted to do, i.e., removing the branch\n@{u}, which they have happily been doing with '-d' while guarded by\nthe original safety feature '-d' already had.  Now they have to use\n'-D' to remove it unconditionally without safety -- that is not\nexactly progress.  In addition, depending on the version of Git, our\nchange makes 'git branch -d' behave differently, making it less\npredictable.\n\nAnd no, a configuration variable to tweak the behaviour of '-d' is\nunwelcome here; it would make the behavior of the command and the\noption even less predictable.\n\n"},{"id":"551262","messageId":"CAHwyqnXjO6Cv50BVjJjW939A06-bQtcA2uf0Cwk+xuoc2Gh_LA@mail.gmail.com","threadId":"66220","inReplyTo":"xmqq33w1lv4j.fsf@gitster.g","subject":"Re: [PATCH 2/2] branch: protect local upstreams from -d","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-08-26T06:56:51Z","receivedAt":"2026-08-26T06:57:31Z","isPatch":true,"body":"Hmm, it makes sense what you are saying, but I could have sworn that\nyou asked for this (likely some very different version from this) when\nI was working on delete-merged, i.e. to extend branch protections to\n'-d' as well. Phillip brought it up recently as well. Maybe I\nmisunderstood.\n\n\nHarald\n"},{"id":"551267","messageId":"20260826084641.tb2NX%taahol@utu.fi","threadId":"66220","inReplyTo":"CAHwyqnXjO6Cv50BVjJjW939A06-bQtcA2uf0Cwk+xuoc2Gh_LA@mail.gmail.com","subject":"Re: [PATCH 2/2] branch: protect local upstreams from -d","fromName":"Tuomas Ahola","fromEmail":"taahol@utu.fi","sentAt":"2026-08-26T08:46:41Z","receivedAt":"2026-08-26T08:46:55Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> wrote:\n\n> Hmm, it makes sense what you are saying, but I could have sworn that\n> you asked for this (likely some very different version from this) when\n> I was working on delete-merged, i.e. to extend branch protections to\n> '-d' as well. Phillip brought it up recently as well. Maybe I\n> misunderstood.\n> \n> \n> Harald\n\nIn <xmqq33yimsdp.fsf@gitster.g> Junio wrote:\n\n} [...]\n} \n} Do we also need the same safety around \"git branch -d feature1\" by\n} the way?  The \"-d\" option with safety checks the same \"is feature1\n} already merged (to its upstream)?\" condition, so it can protect the\n} feature2 branch the same way, by saying either \"oops, you cannot\n} delete feature1 because you still have other branches like feature2\n} that depend on it\", or \"ok, featur2 used to depend on feature1, but\n} because we are deleting feature1 based on it being in origin/master,\n} we will make feature2 depend on origin/master from now on\".\n} \n\nHarald, perhaps that's the passage you thought of?\n\n-- \nTuomas\n"},{"id":"551268","messageId":"CAHwyqnXbzWCfVR0k=UskRDNNdPQnBEZRA+4r+kYbjT9J5K9OeA@mail.gmail.com","threadId":"66220","inReplyTo":"20260826084641.tb2NX%taahol@utu.fi","subject":"Re: [PATCH 2/2] branch: protect local upstreams from -d","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-08-26T09:20:46Z","receivedAt":"2026-08-26T09:21:25Z","isPatch":true,"body":"> } Do we also need the same safety around \"git branch -d feature1\" by\n> } the way?  The \"-d\" option with safety checks the same \"is feature1\n> } already merged (to its upstream)?\" condition, so it can protect the\n> } feature2 branch the same way, by saying either \"oops, you cannot\n> } delete feature1 because you still have other branches like feature2\n> } that depend on it\", or \"ok, featur2 used to depend on feature1, but\n> } because we are deleting feature1 based on it being in origin/master,\n> } we will make feature2 depend on origin/master from now on\".\n> }\n>\n> Harald, perhaps that's the passage you thought of?\n\nYes, exactly. Thanks for finding it!\n\n\nHarald\n"},{"id":"551287","messageId":"xmqqfr01j6bc.fsf@gitster.g","threadId":"66220","inReplyTo":"CAHwyqnXjO6Cv50BVjJjW939A06-bQtcA2uf0Cwk+xuoc2Gh_LA@mail.gmail.com","subject":"Re: [PATCH 2/2] branch: protect local upstreams from -d","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-26T14:37:59Z","receivedAt":"2026-08-26T14:38:02Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> Hmm, it makes sense what you are saying, but I could have sworn that\n> you asked for this (likely some very different version from this) when\n> I was working on delete-merged, i.e. to extend branch protections to\n> '-d' as well. Phillip brought it up recently as well. Maybe I\n> misunderstood.\n\nIt is more likely that I misstated.  I do appreciate that we now\nhave a machinery that allows us to offer an \"improved\" protection\nfeature that may be \"better\" than 'branch -d' to users.  It is a\ndifferent matter in what shape we offer the feature while balancing\nthe need to avoid breaking established end-user workflow.\n\nThanks for working on this topic.\n\n"},{"id":"551338","messageId":"CABPp-BHQvUwwA6v+5rq9=8iUWavDO1ScMVr-3ok4Zm5r_Bp2hQ@mail.gmail.com","threadId":"66220","inReplyTo":"d3d7a06e3d6f0c7adf9739ca496ed4012e261ac1.1787693117.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/2] branch: protect local upstreams from -d","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-08-27T05:19:51Z","receivedAt":"2026-08-27T05:20:04Z","isPatch":true,"body":"On Tue, Aug 25, 2026 at 2:30 PM Harald Nordgren via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> A local branch may be fully merged into its own upstream while still\n> serving as the base of a surviving stacked branch. Deleting it with\n> \"git branch -d\" then leaves the surviving branch with a missing\n> upstream.\n>\n> Use the existing stacked-branch protection after checking every\n> requested deletion. This makes multi-branch deletion independent of\n> argument order: a branch that fails its safety check remains available\n> to protect its upstream.\n[...]\n> diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc\n> index bfdf459329..5c2a3339b2 100644\n> --- a/Documentation/git-branch.adoc\n> +++ b/Documentation/git-branch.adoc\n> @@ -102,7 +102,9 @@ OPTIONS\n>  `--delete`::\n>         Delete a branch. The branch must be fully merged in its\n>         upstream branch, or in `HEAD` if no upstream was set with\n> -       `--track` or `--set-upstream-to`.\n> +       `--track` or `--set-upstream-to`, and must not be an upstream,\n> +       directly or indirectly, of another local branch that will remain\n> +       after the operation.\n\nWith this patch applied:\n\n$ git init -q repo && cd repo\n$ git commit --allow-empty -m base\n[master (root-commit) b9a0882] base\n$ git branch A\n$ git branch B\n$ git branch C\n$ git branch --set-upstream-to=A B\nbranch 'B' set up to track 'A'.\n$ git branch --set-upstream-to=B C\nbranch 'C' set up to track 'B'.\n$ ~/floss/git-review/bin-wrappers/git branch -d A B\nerror: the branch 'B' is an upstream of another branch\nhint: If you are sure you want to delete it, run 'git branch -D B'\nhint: Disable this message with \"git config set advice.forceDeleteBranch false\"\nDeleted branch A (was b9a0882).\n\nSo, C had B as an upstream and git did protect B from being deleted.\nThat matches the claims above.\nHowever, B had A as an upstream and git didn't protect A; it deleted\nit.  That doesn't match the claims above.\n\nVerifying:\n\n$ git config branch.B.merge\nrefs/heads/A\n$ git rev-parse -q --verify refs/heads/A ; echo $?\n1\n$ git rev-parse -q --verify refs/heads/B ; echo $?\nb9a088270710b2494f5fa0668fc7e81a40aebd35\n0\n"}]}