git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
From: Jeff King <peff@peff.net>
To: Anders Kaseorg <andersk@mit.edu>
Cc: Junio C Hamano <gitster@pobox.com>,
	git@vger.kernel.org, Andreas Heiduk <andreas.heiduk@mathema.de>
Subject: Re: [PATCH v2] fetch: Protect branches checked out in all worktrees
Date: Fri, 5 Nov 2021 07:53:38 -0400	[thread overview]
Message-ID: <YYUbQqyYQDD5QEAz@coredump.intra.peff.net> (raw)
In-Reply-To: <991240f7-2a3e-8ae7-ae25-a1d9d96d55d4@mit.edu>

On Fri, Nov 05, 2021 at 03:01:39AM -0700, Anders Kaseorg wrote:

> On 11/5/21 01:38, Jeff King wrote:
> > If we get the non-default worktree here, it might be nice to tell the
> > user which worktree has it checked out. Otherwise it may lead to
> > head-scratching as they peek at their current HEAD.
> 
> I’d agree if this case were reachable, but as per below, it seems not to be.
> So I didn’t bother figuring out how to shoehorn such a message into the
> arguments expected by format_display().

I was thinking of just:

diff --git a/builtin/fetch.c b/builtin/fetch.c
index a6549c2ab6..61c8fc9983 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -855,6 +855,7 @@ static int update_local_ref(struct ref *ref,
 			    int summary_width)
 {
 	struct commit *current = NULL, *updated;
+	const struct worktree *wt;
 	const char *pretty_ref = prettify_refname(ref->name);
 	int fast_forward = 0;
 
@@ -869,14 +870,16 @@ static int update_local_ref(struct ref *ref,
 	}
 
 	if (!update_head_ok &&
-	    find_shared_symref("HEAD", ref->name) &&
+	    (wt = find_shared_symref("HEAD", ref->name)) &&
 	    !is_null_oid(&ref->old_oid)) {
 		/*
 		 * If this is the head, and it's not okay to update
 		 * the head, and the old value of the head isn't empty...
 		 */
 		format_display(display, '!', _("[rejected]"),
-			       _("can't fetch in current branch"),
+			       wt->is_current ?
+			       _("can't fetch in current branch") :
+			       _("branch checked out in worktree"),
 			       remote, pretty_ref, summary_width);
 		return 1;
 	}

though of course that does not mention the working tree we found. We'd
probably have to format the string into an extra strbuf for that. But I
agree, that if we don't think this is reachable, it may not be worth
worrying about. But see below, where I get off on a bit of tangent.

> > So if the earlier hunk covered this case, then what is this hunk for? :)
> 
> Yeah, that duplication confused me too. The relevant call paths seem to be:
> 
> 1. do_fetch calls check_not_current_branch (later hunk);
> 2. do_fetch calls fetch_and_consume_refs → store_updated_refs →
> update_local_ref (earlier hunk);
> 3. do_fetch calls backfill_tags → fetch_and_consume_refs →
> store_updated_refs → update_local_ref (earlier hunk);
> 
> in that order. That suggests there should be no way to trip the earlier
> hunk’s check without first tripping the latter hunk’s check.

That matches what I found. I think that the check in update_local_ref()
became obsolete when we added check_not_current_branch() in 8ee5d73137
(Fix fetch/pull when run without --update-head-ok, 2008-10-13). That
commit confused me at first, because there's no mention of the existing
mechanism at all in the commit message nor the mailing list thread.

But it was indeed broken back then, and I think is even broken now! The
problem is that it was comparing against current_branch->name, which is
not the full refname, and thus would never match. The check added in
8ee5d73137 gets this right.

Your patch also ends up fixing this as a side effect, because it
switches to using find_shared_symref(), which eliminates the buggy code.

But all of that means we could actually drop check_not_current_branch()
in favor of the update_local_ref() check. Doing this:

diff --git a/builtin/fetch.c b/builtin/fetch.c
index f7abbc31ff..c52c44684a 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -869,7 +869,7 @@ static int update_local_ref(struct ref *ref,
 	}
 
 	if (current_branch &&
-	    !strcmp(ref->name, current_branch->name) &&
+	    !strcmp(ref->name, current_branch->refname) &&
 	    !(update_head_ok || is_bare_repository()) &&
 	    !is_null_oid(&ref->old_oid)) {
 		/*
@@ -1385,20 +1385,6 @@ static int prune_refs(struct refspec *rs, struct ref *ref_map,
 	return result;
 }
 
-static void check_not_current_branch(struct ref *ref_map)
-{
-	struct branch *current_branch = branch_get(NULL);
-
-	if (is_bare_repository() || !current_branch)
-		return;
-
-	for (; ref_map; ref_map = ref_map->next)
-		if (ref_map->peer_ref && !strcmp(current_branch->refname,
-					ref_map->peer_ref->name))
-			die(_("Refusing to fetch into current branch %s "
-			    "of non-bare repository"), current_branch->refname);
-}
-
 static int truncate_fetch_head(void)
 {
 	const char *filename = git_path_fetch_head(the_repository);
@@ -1587,8 +1573,6 @@ static int do_fetch(struct transport *transport,
 
 	ref_map = get_ref_map(transport->remote, remote_refs, rs,
 			      tags, &autotags);
-	if (!update_head_ok)
-		check_not_current_branch(ref_map);
 
 	if (tags == TAGS_DEFAULT && autotags)
 		transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, "1");

passes the entire test suite except for one test which expects:

  git fetch . side:main

to fail. But that is only because "side" and "main" point to the same
commit, and thus the fetch is a noop. The code in update_local_ref()
covers that case before checking the HEAD case (which I would argue is
a completely reasonable outcome).

The reason I bring this up is that I think doing the check in
update_local_ref() makes much more sense. We don't abort the whole
fetch, but just treat it as a normal per-ref failure. That gives us the
usual status-table output (I thought it might also avoid wasting some
work of actually fetching objects, but I think the current check kicks
in before we actually fetch anything).

On the other hand, this is a rare-ish error, and nobody has cared much
about the distinction in the last 13 years. So it probably doesn't
really matter.

Now getting back to your patch and the message produced in the
update_local_ref() check. I am content to say: this code path is not
really reachable, but cleaning it up is out of scope, so let's leave it
for another day. But if we do eventually want to keep just the check in
update_local_ref(), then we might want to improve the message now while
we're thinking about it.

> > It's technically correct, but perhaps it would be nicer to keep the old
> > message if the worktree we found is the current one.
> 
> I note that this new message is more consistent with
> 
> $ git branch -d main
> error: Cannot delete branch 'main' checked out at '/home/anders/tmp'

Yeah, that may be a reason to keep things consistent. My main worry is
just that somebody who is not using worktrees but has a long repo
pathname may find the message a bit more overwhelming. But if we want to
deal with that, we should probably do so everywhere.

> While looking for other related messages, I noticed another bug:
> receive-pack doesn’t stop you from updating or deleting a branch checked out
> in a worktree for a bare repository.
> 
> $ git init
> $ git commit --allow-empty -qm foo
> $ git clone --bare . bare.git
> $ git -C bare.git worktree add wt
> $ git -C bare.git/wt push .. :wt
> 
> To ..
> 
>  - [deleted]         wt
> 
> 
> This is_bare_repository() check in update() in builtin/receive-pack.c seems
> wrong:
> 
> const struct worktree *worktree = is_bare_repository() ? NULL :
> find_shared_symref("HEAD", name);

Hmm. That is the same check that is on the fetch side, isn't it? In a
bare repository, we will not do any of these current-branch checks. What
is weird in your example is that you are adding a worktree to a bare
repository. Is that a supported workflow? Should that make it non-bare?

I notice that there is a worktree->is_bare flag, and that
find_shared_symref() would not report such a bare entry to us. But I'm
really unclear how any of that is supposed to work (how do you have a
bare worktree, and what does it mean?).

I think that's all orthogonal to the main purpose of your patch here.
You may want to post about it separately with a different subject to get
the attention of folks who work on worktrees.

-Peff

  reply	other threads:[~2021-11-05 11:53 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-04 21:05 [PATCH] fetch: Protect branches checked out in all worktrees Anders Kaseorg
2021-11-04 21:10 ` [PATCH v2] " Anders Kaseorg
2021-11-05  8:38   ` Jeff King
2021-11-05 10:01     ` Anders Kaseorg
2021-11-05 11:53       ` Jeff King [this message]
2021-11-05 18:39         ` Junio C Hamano
2021-11-08 20:12         ` Anders Kaseorg

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://vger.kernel.org/majordomo-info.html

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=YYUbQqyYQDD5QEAz@coredump.intra.peff.net \
    --to=peff@peff.net \
    --cc=andersk@mit.edu \
    --cc=andreas.heiduk@mathema.de \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).