Subject: [PATCH v3 0/3] branch: operations on orphan branches

From: Rubén Justo <[email-masked]>
Date: Tue, 7 Feb 2023 00:01:27 +0100

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 <[email-masked]>
     
      ## 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 <[email-masked]>
    @@ 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 <[email-masked]>
     
      ## 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