diff mbox series

[3/8] refs: new ref types to make per-worktree refs visible to all worktrees

Message ID 20180922180500.4689-4-pclouds@gmail.com (mailing list archive)
State New, archived
Headers show
Series fix per-worktree ref iteration in fsck/reflog expire | expand

Commit Message

Duy Nguyen Sept. 22, 2018, 6:04 p.m. UTC
One of the problems with multiple worktree is accessing per-worktree
refs of one worktree from another worktree. This was sort of solved by
multiple ref store, where the code can open the ref store of another
worktree and has access to the ref space of that worktree.

The problem with this is reporting. "HEAD" in another ref space is
also called "HEAD" like in the current ref space. In order to
differentiate them, all the code must somehow carry the ref store
around and print something like "HEAD from this ref store".

But that is not feasible (or possible with a _lot_ of work). With the
current design, we pass a reference around as a string (so called
"refname"). Extending this design to pass a string _and_ a ref store
is a nightmare, especially when handling extended SHA-1 syntax.

So we do it another way. Instead of entering a separate ref space, we
make refs from other worktrees available in the current ref space. So
"HEAD" is always HEAD of the current worktree, but then we can have
"worktrees/blah/HEAD" to denote HEAD from a worktree named
"blah". This syntax coincidentally matches the underlying directory
structure which makes implementation a bit easier.

The main worktree has to be treated specially because well.. it's
special from the beginning. So HEAD from the main worktree is
acccessible via the name "main/HEAD" (we can't use
"worktrees/main/HEAD" because "main" under "worktrees" is not
reserved).

This patch also makes it possible to specify refs from one worktree in
another one, e.g.

    git log worktrees/foo/HEAD

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 refs.c                   | 20 ++++++++++++++++++++
 refs.h                   |  8 +++++---
 refs/files-backend.c     | 28 ++++++++++++++++++++++++++++
 t/t1415-worktree-refs.sh | 30 ++++++++++++++++++++++++++++++
 4 files changed, 83 insertions(+), 3 deletions(-)

Comments

Eric Sunshine Sept. 23, 2018, 8:06 a.m. UTC | #1
On Sat, Sep 22, 2018 at 2:05 PM Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:
> [...]
> The main worktree has to be treated specially because well.. it's
> special from the beginning. So HEAD from the main worktree is
> acccessible via the name "main/HEAD" (we can't use
> "worktrees/main/HEAD" because "main" under "worktrees" is not
> reserved).

Bikeshedding: I wonder if this would be more intuitive if called
simply "/HEAD" rather than "main/HEAD".

> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
> ---
> diff --git a/refs.c b/refs.c
> @@ -641,12 +641,32 @@ static int is_pseudoref_syntax(const char *refname)
> +static int is_main_pseudoref_syntax(const char *refname)
> +{
> +       return skip_prefix(refname, "main/", &refname) &&
> +               is_pseudoref_syntax(refname);
> +}
> +
> +static int is_other_pseudoref_syntax(const char *refname)
> +{
> +       if (!skip_prefix(refname, "worktrees/", &refname))
> +               return 0;
> +       refname = strchr(refname, '/');
> +       if (!refname)
> +               return 0;
> +       return is_pseudoref_syntax(refname + 1);
> +}

If the input is "worktrees/refs/" (nothing following the trailing
'/'), then an empty string will be passed to is_pseudoref_syntax(),
which will return true. Does that result in correct behavior? (Same
question about "main/" being passed to is_main_pseudoref_syntax().)
Duy Nguyen Sept. 23, 2018, 1:10 p.m. UTC | #2
On Sun, Sep 23, 2018 at 10:06 AM Eric Sunshine <sunshine@sunshineco.com> wrote:
>
> On Sat, Sep 22, 2018 at 2:05 PM Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:
> > [...]
> > The main worktree has to be treated specially because well.. it's
> > special from the beginning. So HEAD from the main worktree is
> > acccessible via the name "main/HEAD" (we can't use
> > "worktrees/main/HEAD" because "main" under "worktrees" is not
> > reserved).
>
> Bikeshedding: I wonder if this would be more intuitive if called
> simply "/HEAD" rather than "main/HEAD".

A ref name cannot start with '/'. I'm open to a different name than
"main" though, it just felt a better name than "main-worktree".
Stefan Beller Sept. 25, 2018, 2:48 a.m. UTC | #3
On Sat, Sep 22, 2018 at 11:05 AM Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:
>
> One of the problems with multiple worktree is accessing per-worktree
> refs of one worktree from another worktree. This was sort of solved by
> multiple ref store, where the code can open the ref store of another
> worktree and has access to the ref space of that worktree.
>
> The problem with this is reporting. "HEAD" in another ref space is
> also called "HEAD" like in the current ref space. In order to
> differentiate them, all the code must somehow carry the ref store
> around and print something like "HEAD from this ref store".
>
> But that is not feasible (or possible with a _lot_ of work). With the
> current design, we pass a reference around as a string (so called
> "refname"). Extending this design to pass a string _and_ a ref store
> is a nightmare, especially when handling extended SHA-1 syntax.
>
> So we do it another way. Instead of entering a separate ref space, we
> make refs from other worktrees available in the current ref space. So
> "HEAD" is always HEAD of the current worktree, but then we can have
> "worktrees/blah/HEAD" to denote HEAD from a worktree named
> "blah". This syntax coincidentally matches the underlying directory
> structure which makes implementation a bit easier.
>
> The main worktree has to be treated specially because well.. it's
> special from the beginning. So HEAD from the main worktree is
> acccessible via the name "main/HEAD" (we can't use
> "worktrees/main/HEAD" because "main" under "worktrees" is not
> reserved).
>
> This patch also makes it possible to specify refs from one worktree in
> another one, e.g.
>
>     git log worktrees/foo/HEAD

This has strong similarities to remote refs:
Locally I may have a branch master, whose (stale local copy of its
distributed) counterpart is named origin/master.

It is also possible to have a working tree named origin
(just I like to name my worktree "git", when working on git.git),
how do we differentiate between the neighbor-worktree
"origin/master" and the remote-tracking branch "origin/master" ?

As the remote tracking branches are shared between all
worktree there is no need to differentiate between a
local-worktree remote tracking branch and a
neighbor-worktree remote tracking branch.

Now that you introduce the magic main working tree,
we also need to disallow working trees to be named "main",
i.e.
    $ git worktree add main HEAD

produces

  $ ls .git/worktrees/
  main

How do we deal with that?
Duy Nguyen Sept. 25, 2018, 3:49 p.m. UTC | #4
On Tue, Sep 25, 2018 at 4:48 AM Stefan Beller <sbeller@google.com> wrote:
> > This patch also makes it possible to specify refs from one worktree in
> > another one, e.g.
> >
> >     git log worktrees/foo/HEAD
>
> This has strong similarities to remote refs:
> Locally I may have a branch master, whose (stale local copy of its
> distributed) counterpart is named origin/master.

If you think of each worktree as independent clones (which is more or
less true, the fact that they share ODB is more like an implementation
detail) then yes it's almost like remotes.

> It is also possible to have a working tree named origin
> (just I like to name my worktree "git", when working on git.git),
> how do we differentiate between the neighbor-worktree
> "origin/master" and the remote-tracking branch "origin/master" ?

Hmm.. I think you're thinking that origin/master could either mean
refs/worktrees/origin/master or refs/remotes/origin/master. I do not
think we're going to support expanding origin/master to
refs/worktrees/origin/master. This part about ref resolution did cross
my mind but I didn't see a good reason to support it.

Even if we do support it, this is not even a new problem. If you have
refs/heads/origin/master and refs/remotes/origin/master now, we have
ref ambiguity anyway and a solution for this should handle
refs/worktrees/origin/master well if it comes into the picture.

> As the remote tracking branches are shared between all
> worktree there is no need to differentiate between a
> local-worktree remote tracking branch and a
> neighbor-worktree remote tracking branch.
>
> Now that you introduce the magic main working tree,
> we also need to disallow working trees to be named "main",
> i.e.
>     $ git worktree add main HEAD
>
> produces
>
>   $ ls .git/worktrees/
>   main
>
> How do we deal with that?

main is accessed via worktrees/main/HEAD while the main worktree's
HEAD is accessed via main/HEAD (which is _not_ automatically expanded
to refs/worktrees/main/HEAD). But if it is, yes we need to detect
ambiguity and tell the user to specify full ref name, either
refs/main/HEAD or refs/worktrees/main/HEAD.
Stefan Beller Sept. 25, 2018, 4:53 p.m. UTC | #5
On Tue, Sep 25, 2018 at 8:49 AM Duy Nguyen <pclouds@gmail.com> wrote:
>
> On Tue, Sep 25, 2018 at 4:48 AM Stefan Beller <sbeller@google.com> wrote:
> > > This patch also makes it possible to specify refs from one worktree in
> > > another one, e.g.
> > >
> > >     git log worktrees/foo/HEAD
> >
> > This has strong similarities to remote refs:
> > Locally I may have a branch master, whose (stale local copy of its
> > distributed) counterpart is named origin/master.
>
> If you think of each worktree as independent clones (which is more or
> less true, the fact that they share ODB is more like an implementation
> detail) then yes it's almost like remotes.

Apart from the ODB and the refs subsystem, there is also the config
space, which is shared (but you have sent out patches to have local
config as well).

So I would think worktrees are better than having two clones not just
due to the shared ODB, but also due to the common config as then I
have to setup my repo only once and can add/remove worktrees
cheaply (in terms of "how much time do I need to spend to configure
it as I need").

> > It is also possible to have a working tree named origin
> > (just I like to name my worktree "git", when working on git.git),
> > how do we differentiate between the neighbor-worktree
> > "origin/master" and the remote-tracking branch "origin/master" ?
>
> Hmm.. I think you're thinking that origin/master could either mean
> refs/worktrees/origin/master or refs/remotes/origin/master. I do not
> think we're going to support expanding origin/master to
> refs/worktrees/origin/master. This part about ref resolution did cross
> my mind but I didn't see a good reason to support it.
>
> Even if we do support it, this is not even a new problem. If you have
> refs/heads/origin/master and refs/remotes/origin/master now, we have
> ref ambiguity anyway and a solution for this should handle
> refs/worktrees/origin/master well if it comes into the picture.

So once origin/master is overloaded, I would have to spell out
refs/worktrees/origin/master and refs/remotes/origin/master to
avoid confusing the DWIM machinery. Makes sense.

> > How do we deal with that?
>
> main is accessed via worktrees/main/HEAD while the main worktree's
> HEAD is accessed via main/HEAD (which is _not_ automatically expanded
> to refs/worktrees/main/HEAD). But if it is, yes we need to detect
> ambiguity and tell the user to specify full ref name, either
> refs/main/HEAD or refs/worktrees/main/HEAD.

Ah, I see. Now I actually understand the last paragraph of the
commit message. Thanks for explaining!

Stefan
Junio C Hamano Sept. 25, 2018, 9:16 p.m. UTC | #6
Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:

> The main worktree has to be treated specially because well.. it's
> special from the beginning. So HEAD from the main worktree is
> acccessible via the name "main/HEAD" (we can't use
> "worktrees/main/HEAD" because "main" under "worktrees" is not
> reserved).

I do not quite follow.  So with this, both refs/heads/master and
main/refs/heads/master are good names for the master branch (even
though the local branch names are not per worktree), because
in the main worktree, refs/bisect/bad and main/refs/bisect/bad ought
to mean the same thing.

	side note: Or is this only for pseudo-refs
	(i.e. $GIT_DIR/$name where $name consists of all caps or
	underscore and typically ends with HEAD)?  Even if that were
	the case, I do not think it essentially changes the issue
	around disambiguation that much.

The disambiguation rule has always been: if you have a confusingly
named ref, you can spell it out fully to avoid any ambiguity, e.g.
refs/heads/refs/heads/foo can be given to "git rev-parse" and will
mean the tip of the branch whose name is "refs/heads/foo", even when
another branch whose name is "foo" exists.

Would we have a reasonable disambiguation rules that work well with
the main/ and worktrees/* prefixes?  When somebody has main/HEAD branch
and writes "git rev-parse main/HEAD", does it find refs/heads/main/HEAD
or $GIT_DIR/HEAD, if the user is in the main worktree?

It could be simply that the design is underdocumented in this patch
set (in which case I would have appreciated 'RFC' near 'PATCH'), but
I have a feeling that the code came way too early before such design
issues are fleshed out.

> diff --git a/refs.h b/refs.h
> index bd52c1bbae..9b53dbeae8 100644
> --- a/refs.h
> +++ b/refs.h
> @@ -704,9 +704,11 @@ int parse_hide_refs_config(const char *var, const char *value, const char *);
>  int ref_is_hidden(const char *, const char *);
>  
>  enum ref_type {
> -	REF_TYPE_PER_WORKTREE,
> -	REF_TYPE_PSEUDOREF,
> -	REF_TYPE_NORMAL,
> +	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
> +	REF_TYPE_PSEUDOREF,	  /* refs outside refs/ in current worktree */
> +	REF_TYPE_MAIN_PSEUDOREF,  /* pseudo refs from the main worktree     */
> +	REF_TYPE_OTHER_PSEUDOREF, /* pseudo refs from other worktrees       */
> +	REF_TYPE_NORMAL,	  /* normal/shared refs inside refs/        */
>  };
>  
>  enum ref_type ref_type(const char *refname);
> diff --git a/refs/files-backend.c b/refs/files-backend.c
> index 416eafa453..bf9ed633b1 100644
> --- a/refs/files-backend.c
> +++ b/refs/files-backend.c
> @@ -149,6 +149,23 @@ static struct files_ref_store *files_downcast(struct ref_store *ref_store,
>  	return refs;
>  }
>  
> +static void files_reflog_path_other_worktrees(struct files_ref_store *refs,
> +					      struct strbuf *sb,
> +					      const char *refname)
> +{
> +	const char *real_ref;
> +
> +	if (!skip_prefix(refname, "worktrees/", &real_ref))
> +		BUG("refname %s is not a other-worktree ref", refname);
> +	real_ref = strchr(real_ref, '/');
> +	if (!real_ref)
> +		BUG("refname %s is not a other-worktree ref", refname);
> +	real_ref++;
> +
> +	strbuf_addf(sb, "%s/%.*slogs/%s", refs->gitcommondir,
> +		    (int)(real_ref - refname), refname, real_ref);
> +}
> +
>  static void files_reflog_path(struct files_ref_store *refs,
>  			      struct strbuf *sb,
>  			      const char *refname)
> @@ -158,6 +175,12 @@ static void files_reflog_path(struct files_ref_store *refs,
>  	case REF_TYPE_PSEUDOREF:
>  		strbuf_addf(sb, "%s/logs/%s", refs->gitdir, refname);
>  		break;
> +	case REF_TYPE_OTHER_PSEUDOREF:
> +		return files_reflog_path_other_worktrees(refs, sb, refname);
> +	case REF_TYPE_MAIN_PSEUDOREF:
> +		if (!skip_prefix(refname, "main/", &refname))
> +			BUG("ref %s is not a main pseudoref", refname);
> +		/* passthru */
>  	case REF_TYPE_NORMAL:
>  		strbuf_addf(sb, "%s/logs/%s", refs->gitcommondir, refname);
>  		break;
> @@ -176,6 +199,11 @@ static void files_ref_path(struct files_ref_store *refs,
>  	case REF_TYPE_PSEUDOREF:
>  		strbuf_addf(sb, "%s/%s", refs->gitdir, refname);
>  		break;
> +	case REF_TYPE_MAIN_PSEUDOREF:
> +		if (!skip_prefix(refname, "main/", &refname))
> +			BUG("ref %s is not a main pseudoref", refname);
> +		/* passthru */
> +	case REF_TYPE_OTHER_PSEUDOREF:
>  	case REF_TYPE_NORMAL:
>  		strbuf_addf(sb, "%s/%s", refs->gitcommondir, refname);
>  		break;
> diff --git a/t/t1415-worktree-refs.sh b/t/t1415-worktree-refs.sh
> index 0c2d5f89a9..46ca7bfc19 100755
> --- a/t/t1415-worktree-refs.sh
> +++ b/t/t1415-worktree-refs.sh
> @@ -33,4 +33,34 @@ test_expect_success 'refs/local are per-worktree' '
>  	( cd wt2 && test_cmp_rev local/foo wt2 )
>  '
>  
> +test_expect_success 'resolve main/HEAD' '
> +	test_cmp_rev main/HEAD initial &&
> +	( cd wt1 && test_cmp_rev main/HEAD initial ) &&
> +	( cd wt2 && test_cmp_rev main/HEAD initial )
> +'
> +
> +test_expect_success 'resolve worktrees/xx/HEAD' '
> +	test_cmp_rev worktrees/wt1/HEAD wt1 &&
> +	( cd wt1 && test_cmp_rev worktrees/wt1/HEAD wt1 ) &&
> +	( cd wt2 && test_cmp_rev worktrees/wt1/HEAD wt1 )
> +'
> +
> +test_expect_success 'reflog of main/HEAD' '
> +	git reflog HEAD | sed "s/HEAD/main\/HEAD/" >expected &&
> +	git reflog main/HEAD >actual &&
> +	test_cmp expected actual &&
> +	git -C wt1 reflog main/HEAD >actual.wt1 &&
> +	test_cmp expected actual.wt1
> +'
> +
> +test_expect_success 'reflog of worktrees/xx/HEAD' '
> +	git -C wt2 reflog HEAD | sed "s/HEAD/worktrees\/wt2\/HEAD/" >expected &&
> +	git reflog worktrees/wt2/HEAD >actual &&
> +	test_cmp expected actual &&
> +	git -C wt1 reflog worktrees/wt2/HEAD >actual.wt1 &&
> +	test_cmp expected actual.wt1 &&
> +	git -C wt2 reflog worktrees/wt2/HEAD >actual.wt2 &&
> +	test_cmp expected actual.wt2
> +'
> +
>  test_done
Duy Nguyen Sept. 29, 2018, 6:26 p.m. UTC | #7
On Tue, Sep 25, 2018 at 11:16 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:
>
> > The main worktree has to be treated specially because well.. it's
> > special from the beginning. So HEAD from the main worktree is
> > acccessible via the name "main/HEAD" (we can't use
> > "worktrees/main/HEAD" because "main" under "worktrees" is not
> > reserved).
>
> I do not quite follow.  So with this, both refs/heads/master and
> main/refs/heads/master are good names for the master branch (even
> though the local branch names are not per worktree), because
> in the main worktree, refs/bisect/bad and main/refs/bisect/bad ought
> to mean the same thing.

True. I think the ambiguation here is about the main worktree versus a
secondary worktree that is accidentally named "main". Then suddenly we
have to worktrees of the same name, and accessing them both via
worktrees/<id>/HEAD will not work, and there is no other way to
disambiguate them.

>         side note: Or is this only for pseudo-refs
>         (i.e. $GIT_DIR/$name where $name consists of all caps or
>         underscore and typically ends with HEAD)?

Right now, due to implementation limitations, only pseudo refs (or
loose refs in the case of refs/bisect) are accessible. But I don't see
why main/refs/heads/master should not work.

> The disambiguation rule has always been: if you have a confusingly
> named ref, you can spell it out fully to avoid any ambiguity, e.g.
> refs/heads/refs/heads/foo can be given to "git rev-parse" and will
> mean the tip of the branch whose name is "refs/heads/foo", even when
> another branch whose name is "foo" exists.
>
> Would we have a reasonable disambiguation rules that work well with
> the main/ and worktrees/* prefixes?  When somebody has main/HEAD branch
> and writes "git rev-parse main/HEAD", does it find refs/heads/main/HEAD
> or $GIT_DIR/HEAD, if the user is in the main worktree?

The rules are not touched. But it looks like everything still works as
expected (I'm adding tests to verify this)
Junio C Hamano Oct. 6, 2018, 11:20 p.m. UTC | #8
Duy Nguyen <pclouds@gmail.com> writes:

>> > The main worktree has to be treated specially because well.. it's
>> > special from the beginning. So HEAD from the main worktree is
>> > acccessible via the name "main/HEAD" (we can't use
>> > "worktrees/main/HEAD" because "main" under "worktrees" is not
>> > reserved).
>>
>> I do not quite follow.  So with this, both refs/heads/master and
>> main/refs/heads/master are good names for the master branch (even
>> though the local branch names are not per worktree), because
>> in the main worktree, refs/bisect/bad and main/refs/bisect/bad ought
>> to mean the same thing.
>
> True. I think the ambiguation here is about the main worktree versus a
> secondary worktree that is accidentally named "main". Then suddenly we
> have to worktrees of the same name, and accessing them both via
> worktrees/<id>/HEAD will not work, and there is no other way to
> disambiguate them.

So those who have happily been referring 'refs/heads/main/foo' as
'main/foo' now suddenly have to say 'refs/heads/main/foo' instead?

> The rules are not touched. But it looks like everything still works as
> expected (I'm adding tests to verify this)

What I am worried about is that _your_ expectation may not coincide
with the expectations of users, especially with ones with existing
refs that overlap with the namespaces this series suddenly starts
carving out and squatting on.  As long as that won't be a problem, I
think it is OK, even with 'main' not renamed to 'worktree-main' or
somesuch.
diff mbox series

Patch

diff --git a/refs.c b/refs.c
index a851ef085b..90b73c7334 100644
--- a/refs.c
+++ b/refs.c
@@ -641,12 +641,32 @@  static int is_pseudoref_syntax(const char *refname)
 	return 1;
 }
 
+static int is_main_pseudoref_syntax(const char *refname)
+{
+	return skip_prefix(refname, "main/", &refname) &&
+		is_pseudoref_syntax(refname);
+}
+
+static int is_other_pseudoref_syntax(const char *refname)
+{
+	if (!skip_prefix(refname, "worktrees/", &refname))
+		return 0;
+	refname = strchr(refname, '/');
+	if (!refname)
+		return 0;
+	return is_pseudoref_syntax(refname + 1);
+}
+
 enum ref_type ref_type(const char *refname)
 {
 	if (is_per_worktree_ref(refname))
 		return REF_TYPE_PER_WORKTREE;
 	if (is_pseudoref_syntax(refname))
 		return REF_TYPE_PSEUDOREF;
+	if (is_main_pseudoref_syntax(refname))
+		return REF_TYPE_MAIN_PSEUDOREF;
+	if (is_other_pseudoref_syntax(refname))
+		return REF_TYPE_OTHER_PSEUDOREF;
 	return REF_TYPE_NORMAL;
 }
 
diff --git a/refs.h b/refs.h
index bd52c1bbae..9b53dbeae8 100644
--- a/refs.h
+++ b/refs.h
@@ -704,9 +704,11 @@  int parse_hide_refs_config(const char *var, const char *value, const char *);
 int ref_is_hidden(const char *, const char *);
 
 enum ref_type {
-	REF_TYPE_PER_WORKTREE,
-	REF_TYPE_PSEUDOREF,
-	REF_TYPE_NORMAL,
+	REF_TYPE_PER_WORKTREE,	  /* refs inside refs/ but not shared       */
+	REF_TYPE_PSEUDOREF,	  /* refs outside refs/ in current worktree */
+	REF_TYPE_MAIN_PSEUDOREF,  /* pseudo refs from the main worktree     */
+	REF_TYPE_OTHER_PSEUDOREF, /* pseudo refs from other worktrees       */
+	REF_TYPE_NORMAL,	  /* normal/shared refs inside refs/        */
 };
 
 enum ref_type ref_type(const char *refname);
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 416eafa453..bf9ed633b1 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -149,6 +149,23 @@  static struct files_ref_store *files_downcast(struct ref_store *ref_store,
 	return refs;
 }
 
+static void files_reflog_path_other_worktrees(struct files_ref_store *refs,
+					      struct strbuf *sb,
+					      const char *refname)
+{
+	const char *real_ref;
+
+	if (!skip_prefix(refname, "worktrees/", &real_ref))
+		BUG("refname %s is not a other-worktree ref", refname);
+	real_ref = strchr(real_ref, '/');
+	if (!real_ref)
+		BUG("refname %s is not a other-worktree ref", refname);
+	real_ref++;
+
+	strbuf_addf(sb, "%s/%.*slogs/%s", refs->gitcommondir,
+		    (int)(real_ref - refname), refname, real_ref);
+}
+
 static void files_reflog_path(struct files_ref_store *refs,
 			      struct strbuf *sb,
 			      const char *refname)
@@ -158,6 +175,12 @@  static void files_reflog_path(struct files_ref_store *refs,
 	case REF_TYPE_PSEUDOREF:
 		strbuf_addf(sb, "%s/logs/%s", refs->gitdir, refname);
 		break;
+	case REF_TYPE_OTHER_PSEUDOREF:
+		return files_reflog_path_other_worktrees(refs, sb, refname);
+	case REF_TYPE_MAIN_PSEUDOREF:
+		if (!skip_prefix(refname, "main/", &refname))
+			BUG("ref %s is not a main pseudoref", refname);
+		/* passthru */
 	case REF_TYPE_NORMAL:
 		strbuf_addf(sb, "%s/logs/%s", refs->gitcommondir, refname);
 		break;
@@ -176,6 +199,11 @@  static void files_ref_path(struct files_ref_store *refs,
 	case REF_TYPE_PSEUDOREF:
 		strbuf_addf(sb, "%s/%s", refs->gitdir, refname);
 		break;
+	case REF_TYPE_MAIN_PSEUDOREF:
+		if (!skip_prefix(refname, "main/", &refname))
+			BUG("ref %s is not a main pseudoref", refname);
+		/* passthru */
+	case REF_TYPE_OTHER_PSEUDOREF:
 	case REF_TYPE_NORMAL:
 		strbuf_addf(sb, "%s/%s", refs->gitcommondir, refname);
 		break;
diff --git a/t/t1415-worktree-refs.sh b/t/t1415-worktree-refs.sh
index 0c2d5f89a9..46ca7bfc19 100755
--- a/t/t1415-worktree-refs.sh
+++ b/t/t1415-worktree-refs.sh
@@ -33,4 +33,34 @@  test_expect_success 'refs/local are per-worktree' '
 	( cd wt2 && test_cmp_rev local/foo wt2 )
 '
 
+test_expect_success 'resolve main/HEAD' '
+	test_cmp_rev main/HEAD initial &&
+	( cd wt1 && test_cmp_rev main/HEAD initial ) &&
+	( cd wt2 && test_cmp_rev main/HEAD initial )
+'
+
+test_expect_success 'resolve worktrees/xx/HEAD' '
+	test_cmp_rev worktrees/wt1/HEAD wt1 &&
+	( cd wt1 && test_cmp_rev worktrees/wt1/HEAD wt1 ) &&
+	( cd wt2 && test_cmp_rev worktrees/wt1/HEAD wt1 )
+'
+
+test_expect_success 'reflog of main/HEAD' '
+	git reflog HEAD | sed "s/HEAD/main\/HEAD/" >expected &&
+	git reflog main/HEAD >actual &&
+	test_cmp expected actual &&
+	git -C wt1 reflog main/HEAD >actual.wt1 &&
+	test_cmp expected actual.wt1
+'
+
+test_expect_success 'reflog of worktrees/xx/HEAD' '
+	git -C wt2 reflog HEAD | sed "s/HEAD/worktrees\/wt2\/HEAD/" >expected &&
+	git reflog worktrees/wt2/HEAD >actual &&
+	test_cmp expected actual &&
+	git -C wt1 reflog worktrees/wt2/HEAD >actual.wt1 &&
+	test_cmp expected actual.wt1 &&
+	git -C wt2 reflog worktrees/wt2/HEAD >actual.wt2 &&
+	test_cmp expected actual.wt2
+'
+
 test_done