Avoid some confusing errors operating with orphan branches when working with worktrees. Changes from v2: - Renamed "ishead_and_reject_rebase_or_bisect_branch()" to "die_if_branch_is_being_rebased_or_bisected()" A proposed name "die_if_branch_is_is_use()" has not been used because it could lead to confusion. We don't yet support copying or renaming a branch being rebased or bisected, but we do under other uses. - Comments added and variables renamed to make more clear the intention and the functioning. Rubén Justo (3): branch: avoid unnecessary worktrees traversals branch: description for orphan branch errors branch: rename orphan branches in any worktree builtin/branch.c | 47 ++++++++++++++++++++++++------------------ t/t3200-branch.sh | 14 +++++++++++++ t/t3202-show-branch.sh | 18 ++++++++++++++++ 3 files changed, 59 insertions(+), 20 deletions(-) Range-diff against v2: 1: d16819bc61 ! 1: 50fa7ed076 avoid unnecessary worktrees traversing @@ Metadata Author: Rubén Justo <rjusto@xxxxxxxxx> ## Commit message ## - avoid unnecessary worktrees traversing + branch: avoid unnecessary worktrees traversals "reject_rebase_or_bisect_branch()" was introduced [1] to prevent a branch under bisect or rebase from being renamed or copied. It traverses all worktrees in the repository and dies if the specified - branch is being rebased or bisected in any them. + branch is being rebased or bisected in any of them. "replace_each_worktree_head_symref()" was introduced [2] to adjust the HEAD in all worktrees after a branch rename succeeded. It traverses all @@ Commit message to the renamed ref, it adjusts it. If we could know in advance if the renamed branch is not HEAD in any - worktree we could avoid calling "replace_each_worktree_head_symref()". + worktree we could avoid calling "replace_each_worktree_head_symref()", + and so avoid that unnecessary second traversing. - Let's have "reject_rebase_or_bisect_branch()" check and return whether - the specified branch is HEAD in any worktree, and use this information - to avoid unnecessary calls to "replace_each_worktree_head_symref()". + Let's rename "reject_rebase_or_bisect_branch()" to a more meaningful + name "die_if_branch_is_being_rebased_or_bisected()" and make it return, + if it does not die(), if the specified branch is HEAD in any worktree. + Use this new information to avoid unnecessary calls to + "replace_each_worktree_head_symref()". 1.- 14ace5b (branch: do not rename a branch under bisect or rebase, 2016-04-22) @@ builtin/branch.c: static void print_current_branch_name(void) } -static void reject_rebase_or_bisect_branch(const char *target) -+static int ishead_and_reject_rebase_or_bisect_branch(const char *target) ++/* ++ * Dies if the specified branch is being rebased or bisected. Otherwise returns ++ * 0 or, if the branch is HEAD in any worktree, returns 1. ++ */ ++static int die_if_branch_is_being_rebased_or_bisected(const char *target) { struct worktree **worktrees = get_worktrees(); - int i; @@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const c const char *interpreted_oldname = NULL; const char *interpreted_newname = NULL; - int recovery = 0; -+ int recovery = 0, ishead; ++ int recovery = 0, oldref_is_head; if (strbuf_check_branch_ref(&oldref, oldname)) { /* @@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const c validate_new_branchname(newname, &newref, force); - reject_rebase_or_bisect_branch(oldref.buf); -+ ishead = ishead_and_reject_rebase_or_bisect_branch(oldref.buf); ++ oldref_is_head = die_if_branch_is_being_rebased_or_bisected(oldref.buf); if (!skip_prefix(oldref.buf, "refs/heads/", &interpreted_oldname) || !skip_prefix(newref.buf, "refs/heads/", &interpreted_newname)) { @@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const c } - if (!copy && -+ if (!copy && ishead && ++ if (!copy && oldref_is_head && replace_each_worktree_head_symref(oldref.buf, newref.buf, logmsg.buf)) die(_("Branch renamed to %s, but HEAD is not updated!"), newname); 2: bc0ac43932 ! 2: ab277e5fcb branch: description for orphan branch errors @@ Commit message branch: description for orphan branch errors In bcfc82bd48 (branch: description for non-existent branch errors, - 2022-10-08) we check the current HEAD to detect if the branch to operate - with is an orphan branch, to avoid the confusing error: "No branch - named...". + 2022-10-08) we checked the current HEAD to detect if the branch to + operate with is an orphan branch, so as to avoid the confusing error: + "No branch named...". If we are asked to operate with an orphan branch in a different working tree than the current one, we need to check the HEAD in that different @@ Commit message repository, using the helper introduced in 31ad6b61bd (branch: add branch_checked_out() helper, 2022-06-15) - "ishead_reject_rebase_or_bised_branch()" already returns whether the - branch to operate with is HEAD in any working tree in the repository. + "die_if_branch_is_being_rebased_or_bisected()" already returns whether + the branch to operate with is HEAD in any worktree in the repository. Let's use this information in "copy_or_rename_branch()", instead of the helper. - Note that we now call reject_rebase_or_bisect_branch() earlier, which - introduces a change in the error displayed when a combination of - unsupported conditions occur simultaneously: the desired destination - name is invalid, and the branch to operate on is being overrun or - bisected. With "foo" being rebased or bisected, this: + Note that we now call "die_if_branch_is_being_rebased_or_bisected()" + earlier, which introduces a change in the error displayed when a + combination of unsupported conditions occur simultaneously: the desired + destination name is invalid, and the branch to operate with is being + rebased or bisected. i.e. with "foo" being rebased or bisected, this: - $ git branch -m foo HEAD - fatal: 'HEAD' is not a valid branch name. + $ git branch -m foo / + fatal: '/' is not a valid branch name. ... becomes: - $ git branch -m foo HEAD + $ git branch -m foo / fatal: Branch refs/heads/foo is being ... Signed-off-by: Rubén Justo <rjusto@xxxxxxxxx> @@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const c - else - die(_("No branch named '%s'."), oldname); - } -+ ishead = ishead_and_reject_rebase_or_bisect_branch(oldref.buf); ++ oldref_is_head = die_if_branch_is_being_rebased_or_bisected(oldref.buf); + -+ if ((copy || !ishead) && !ref_exists(oldref.buf)) -+ die(ishead ++ if ((copy || !oldref_is_head) && !ref_exists(oldref.buf)) ++ die(oldref_is_head + ? _("No commit on branch '%s' yet.") + : _("No branch named '%s'."), oldname); @@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const c else validate_new_branchname(newname, &newref, force); -- ishead = ishead_and_reject_rebase_or_bisect_branch(oldref.buf); +- oldref_is_head = die_if_branch_is_being_rebased_or_bisected(oldref.buf); - if (!skip_prefix(oldref.buf, "refs/heads/", &interpreted_oldname) || !skip_prefix(newref.buf, "refs/heads/", &interpreted_newname)) { 3: 29f8b6044d ! 3: 9742b4ff1f branch: rename orphan branches in any worktree @@ Commit message In cfaff3aac (branch -m: allow renaming a yet-unborn branch, 2020-12-13) we added support for renaming an orphan branch, skipping rename_ref() if - the branch to rename is an orphan branch, checking the current HEAD. + the branch being renamed is an orphan branch; checking the current HEAD. But the orphan branch to be renamed can be an orphan branch in a - different working tree than the current one, i.e. not the current HEAD. + different worktree than the current one, i.e. not the current HEAD. - Let's make "ishead_reject_rebase_or_bisect_branch()" return a flag - indicating if the returned value refers to an orphan branch, and use it - to extend what we did in cfaff3aac, to all HEADs in the repository. + Let's make "die_if_branch_is_being_rebased_or_bisected()" return if the + specified branch is HEAD and orphan, and use it to extend what we did in + cfaff3aac, to check all HEADs in the repository. Signed-off-by: Rubén Justo <rjusto@xxxxxxxxx> ## builtin/branch.c ## -@@ builtin/branch.c: static int ishead_and_reject_rebase_or_bisect_branch(const char *target) +@@ builtin/branch.c: static void print_current_branch_name(void) + + /* + * Dies if the specified branch is being rebased or bisected. Otherwise returns +- * 0 or, if the branch is HEAD in any worktree, returns 1. ++ * 0 or, if the branch is HEAD in any worktree, returns 1. If the branch is HEAD ++ * and also orphan, returns 2. + */ + static int die_if_branch_is_being_rebased_or_bisected(const char *target) + { +@@ builtin/branch.c: static int die_if_branch_is_being_rebased_or_bisected(const char *target) struct worktree *wt = worktrees[i]; if (wt->head_ref && !strcmp(target, wt->head_ref)) - ret = 1; -+ ret = 1 + (is_null_oid(&wt->head_oid) ? 1 : 0); ++ ret = is_null_oid(&wt->head_oid) ? 2 : 1; if (!wt->is_detached) continue; +@@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const char *newname, int + struct strbuf oldsection = STRBUF_INIT, newsection = STRBUF_INIT; + const char *interpreted_oldname = NULL; + const char *interpreted_newname = NULL; +- int recovery = 0, oldref_is_head; ++ int recovery = 0, oldref_is_head, oldref_is_orphan; + + if (strbuf_check_branch_ref(&oldref, oldname)) { + /* +@@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const char *newname, int + } + + oldref_is_head = die_if_branch_is_being_rebased_or_bisected(oldref.buf); ++ oldref_is_orphan = (oldref_is_head > 1); + +- if ((copy || !oldref_is_head) && !ref_exists(oldref.buf)) +- die(oldref_is_head ++ if ((copy || !oldref_is_head) && ++ (oldref_is_orphan || !ref_exists(oldref.buf))) ++ die(oldref_is_orphan + ? _("No commit on branch '%s' yet.") + : _("No branch named '%s'."), oldname); + @@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const char *newname, int strbuf_addf(&logmsg, "Branch: renamed %s to %s", oldref.buf, newref.buf); - if (!copy && - (!head || strcmp(oldname, head) || !is_null_oid(&head_oid)) && -+ if (!copy && !(ishead > 1) && ++ if (!copy && !oldref_is_orphan && rename_ref(oldref.buf, newref.buf, logmsg.buf)) die(_("Branch rename failed")); if (copy && copy_existing_ref(oldref.buf, newref.buf, logmsg.buf)) @@ t/t3200-branch.sh: test_expect_success 'git branch -M and -C fail on detached HE + test_when_finished git checkout - && + test_when_finished git worktree remove -f wt && + git worktree add wt --detach && -+ + # rename orphan in another worktreee + git -C wt checkout --orphan orphan-foo-wt && + git branch -m orphan-foo-wt orphan-bar-wt && + test orphan-bar-wt=$(git -C orphan-worktree branch --show-current) && -+ + # rename orphan in the current worktree + git checkout --orphan orphan-foo && + git branch -m orphan-foo orphan-bar && -- 2.39.0