Subject: Re: "git branch -f" corrupt other worktree

From: Duy Nguyen <[email-masked]>
Date: Fri, 3 May 2019 00:16:13 +0700

On Thu, May 02, 2019 at 08:05:57PM +0700, Duy Nguyen wrote:
> The difficulty will be coming up with some sane UI that can
> handle that and not leave too many traps behind. I can't see that UI.

Well, I'm still thinking about it. And perhaps a good UI is not that
far away. The following hacky patch does three things:

1. it resolves "foo" to "refs/worktree/foo" if neither refs/heads/foo
   or refs/tags/foo exists.

2. it pretty prints refs/worktree/foo as worktree/foo

3. "git branch" has a new option --per-worktree that creates
   refs/worktree/foo instead of refs/heads/foo.

The idea here is we manage/decide per-worktree or shared refs at
branch/tag creation time. After that we use it like a normal
branch/tag. Point #1 helps the "normal" part.

Point #2 could even pretty print it to "foo" to make it even more
normal, but I feel that the distinction between per-worktree and
shared should still be more visible, hence "worktree/foo".

--per-worktree is pretty much a placeholder name. If we could find a
good name (and --worktre is already taken), then it could be added in
more places where a branch may be created.

It's far from complete. For example, refs/worktree/foo is not
considered a branch by any command, Only those inside refs/heads are
at the moment.

When I added refs/worktree/ namespace I didn't think that far ahead,
what kind of refs should be in there. But maybe we can still have
branches in refs/worktree/heads/ and tags in refs/worktree/tags/. Or
perhaps all refs/worktree/ should only contain branches, tags are
always shared (it's shared with remotes now even, at least in common
case)...

With more details worked out, perhaps we can make it work.

-- 8< --
diff --git a/branch.c b/branch.c
index 28b81a7e02..e5d738efa7 100644
--- a/branch.c
+++ b/branch.c
@@ -242,6 +242,8 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
+int create_branch_per_worktree;
+
 void create_branch(struct repository *r,
 		   const char *name, const char *start_name,
 		   int force, int clobber_head_ok, int reflog,
@@ -308,6 +310,9 @@ void create_branch(struct repository *r,
 	if (reflog)
 		log_all_ref_updates = LOG_REFS_NORMAL;
 
+	if (create_branch_per_worktree)
+		strbuf_splice(&ref, 0, 11, "refs/worktree/", 14);
+
 	if (!dont_change_ref) {
 		struct ref_transaction *transaction;
 		struct strbuf err = STRBUF_INIT;
diff --git a/builtin/branch.c b/builtin/branch.c
index d4359b33ac..8839c6f33f 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -605,6 +605,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	int icase = 0;
 	static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
 	struct ref_format format = REF_FORMAT_INIT;
+	int per_worktree = 0;
 
 	struct option options[] = {
 		OPT_GROUP(N_("Generic options")),
@@ -613,6 +614,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		OPT__QUIET(&quiet, N_("suppress informational messages")),
 		OPT_SET_INT('t', "track",  &track, N_("set up tracking mode (see git-pull(1))"),
 			BRANCH_TRACK_EXPLICIT),
+		OPT_BOOL(0, "per-worktree",  &per_worktree, N_("create per-worktree branch")),
 		OPT_SET_INT_F(0, "set-upstream", &track, N_("do not use"),
 			BRANCH_TRACK_OVERRIDE, PARSE_OPT_HIDDEN),
 		OPT_STRING('u', "set-upstream-to", &new_upstream, N_("upstream"), N_("change the upstream info")),
@@ -829,12 +831,15 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		git_config_set_multivar(buf.buf, NULL, NULL, 1);
 		strbuf_release(&buf);
 	} else if (argc > 0 && argc <= 2) {
+		extern int create_branch_per_worktree;
+
 		if (filter.kind != FILTER_REFS_BRANCHES)
 			die(_("-a and -r options to 'git branch' do not make sense with a branch name"));
 
 		if (track == BRANCH_TRACK_OVERRIDE)
 			die(_("the '--set-upstream' option is no longer supported. Please use '--track' or '--set-upstream-to' instead."));
 
+		create_branch_per_worktree = per_worktree;
 		create_branch(the_repository,
 			      argv[0], (argc == 2) ? argv[1] : head,
 			      force, 0, reflog, quiet, track);
diff --git a/refs.c b/refs.c
index 142888a40a..8d01a80e07 100644
--- a/refs.c
+++ b/refs.c
@@ -479,7 +479,9 @@ const char *prettify_refname(const char *name)
 {
 	if (skip_prefix(name, "refs/heads/", &name) ||
 	    skip_prefix(name, "refs/tags/", &name) ||
-	    skip_prefix(name, "refs/remotes/", &name))
+	    skip_prefix(name, "refs/remotes/", &name) ||
+	    (starts_with(name, "refs/worktree/") &&
+	     skip_prefix(name, "refs/", &name)))
 		; /* nothing */
 	return name;
 }
@@ -491,6 +493,7 @@ static const char *ref_rev_parse_rules[] = {
 	"refs/heads/%.*s",
 	"refs/remotes/%.*s",
 	"refs/remotes/%.*s/HEAD",
+	"refs/worktree/%.*s",
 	NULL
 };
 
-- 8< --