Merge branch 'nd/per-worktree-ref-iteration'

The code to traverse objects for reachability, used to decide what
objects are unreferenced and expendable, have been taught to also
consider per-worktree refs of other worktrees as starting points to
prevent data loss.

* nd/per-worktree-ref-iteration:
  git-worktree.txt: correct linkgit command name
  reflog expire: cover reflog from all worktrees
  fsck: check HEAD and reflog from other worktrees
  fsck: move fsck_head_link() to get_default_heads() to avoid some globals
  revision.c: better error reporting on ref from different worktrees
  revision.c: correct a parameter name
  refs: new ref types to make per-worktree refs visible to all worktrees
  Add a place for (not) sharing stuff between worktrees
  refs.c: indent with tabs, not spaces
This commit is contained in:
Junio C Hamano 2018-11-13 22:37:26 +09:00
commit e146cc97be
16 changed files with 449 additions and 47 deletions

View File

@ -20,7 +20,7 @@ depending on the subcommand:
'git reflog' ['show'] [log-options] [<ref>]
'git reflog expire' [--expire=<time>] [--expire-unreachable=<time>]
[--rewrite] [--updateref] [--stale-fix]
[--dry-run | -n] [--verbose] [--all | <refs>...]
[--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]
'git reflog delete' [--rewrite] [--updateref]
[--dry-run | -n] [--verbose] ref@\{specifier\}...
'git reflog exists' <ref>
@ -72,6 +72,11 @@ Options for `expire`
--all::
Process the reflogs of all references.
--single-worktree::
By default when `--all` is specified, reflogs from all working
trees are processed. This option limits the processing to reflogs
from the current working tree only.
--expire=<time>::
Prune entries older than the specified time. If this option is
not specified, the expiration time is taken from the

View File

@ -204,6 +204,35 @@ working trees, it can be used to identify worktrees. For example if
you only have two working trees, at "/abc/def/ghi" and "/abc/def/ggg",
then "ghi" or "def/ghi" is enough to point to the former working tree.
REFS
----
In multiple working trees, some refs may be shared between all working
trees, some refs are local. One example is HEAD is different for all
working trees. This section is about the sharing rules and how to access
refs of one working tree from another.
In general, all pseudo refs are per working tree and all refs starting
with "refs/" are shared. Pseudo refs are ones like HEAD which are
directly under GIT_DIR instead of inside GIT_DIR/refs. There are one
exception to this: refs inside refs/bisect and refs/worktree is not
shared.
Refs that are per working tree can still be accessed from another
working tree via two special paths, main-worktree and worktrees. The
former gives access to per-worktree refs of the main working tree,
while the latter to all linked working trees.
For example, main-worktree/HEAD or main-worktree/refs/bisect/good
resolve to the same value as the main working tree's HEAD and
refs/bisect/good respectively. Similarly, worktrees/foo/HEAD or
worktrees/bar/refs/bisect/bad are the same as
GIT_COMMON_DIR/worktrees/foo/HEAD and
GIT_COMMON_DIR/worktrees/bar/refs/bisect/bad.
To access refs, it's best not to look inside GIT_DIR directly. Instead
use commands such as linkgit:git-rev-parse[1] or linkgit:git-update-ref[1]
which will handle refs correctly.
CONFIGURATION FILE
------------------
By default, the repository "config" file is shared across all working
@ -258,7 +287,8 @@ linked working tree `git rev-parse --git-path HEAD` returns
`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git
rev-parse --git-path refs/heads/master` uses
$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
since refs are shared across all working trees.
since refs are shared across all working trees, except refs/bisect and
refs/worktree.
See linkgit:gitrepository-layout[5] for more information. The rule of
thumb is do not make any assumption about whether a path belongs to

View File

@ -95,8 +95,10 @@ refs::
References are stored in subdirectories of this
directory. The 'git prune' command knows to preserve
objects reachable from refs found in this directory and
its subdirectories. This directory is ignored if $GIT_COMMON_DIR
is set and "$GIT_COMMON_DIR/refs" will be used instead.
its subdirectories.
This directory is ignored (except refs/bisect and
refs/worktree) if $GIT_COMMON_DIR is set and
"$GIT_COMMON_DIR/refs" will be used instead.
refs/heads/`name`::
records tip-of-the-tree commit objects of branch `name`
@ -170,6 +172,11 @@ hooks::
each hook. This directory is ignored if $GIT_COMMON_DIR is set
and "$GIT_COMMON_DIR/hooks" will be used instead.
common::
When multiple working trees are used, most of files in
$GIT_DIR are per-worktree with a few known exceptions. All
files under 'common' however will be shared between all
working trees.
index::
The current index file for the repository. It is

View File

@ -19,6 +19,7 @@
#include "packfile.h"
#include "object-store.h"
#include "run-command.h"
#include "worktree.h"
#define REACHABLE 0x0001
#define SEEN 0x0002
@ -36,8 +37,6 @@ static int check_strict;
static int keep_cache_objects;
static struct fsck_options fsck_walk_options = FSCK_OPTIONS_DEFAULT;
static struct fsck_options fsck_obj_options = FSCK_OPTIONS_DEFAULT;
static struct object_id head_oid;
static const char *head_points_at;
static int errors_found;
static int write_lost_and_found;
static int verbose;
@ -446,7 +445,11 @@ static int fsck_handle_reflog_ent(struct object_id *ooid, struct object_id *noid
static int fsck_handle_reflog(const char *logname, const struct object_id *oid,
int flag, void *cb_data)
{
for_each_reflog_ent(logname, fsck_handle_reflog_ent, (void *)logname);
struct strbuf refname = STRBUF_INIT;
strbuf_worktree_ref(cb_data, &refname, logname);
for_each_reflog_ent(refname.buf, fsck_handle_reflog_ent, refname.buf);
strbuf_release(&refname);
return 0;
}
@ -484,13 +487,34 @@ static int fsck_handle_ref(const char *refname, const struct object_id *oid,
return 0;
}
static int fsck_head_link(const char *head_ref_name,
const char **head_points_at,
struct object_id *head_oid);
static void get_default_heads(void)
{
if (head_points_at && !is_null_oid(&head_oid))
fsck_handle_ref("HEAD", &head_oid, 0, NULL);
struct worktree **worktrees, **p;
const char *head_points_at;
struct object_id head_oid;
for_each_rawref(fsck_handle_ref, NULL);
if (include_reflogs)
for_each_reflog(fsck_handle_reflog, NULL);
worktrees = get_worktrees(0);
for (p = worktrees; *p; p++) {
struct worktree *wt = *p;
struct strbuf ref = STRBUF_INIT;
strbuf_worktree_ref(wt, &ref, "HEAD");
fsck_head_link(ref.buf, &head_points_at, &head_oid);
if (head_points_at && !is_null_oid(&head_oid))
fsck_handle_ref(ref.buf, &head_oid, 0, NULL);
strbuf_release(&ref);
if (include_reflogs)
refs_for_each_reflog(get_worktree_ref_store(wt),
fsck_handle_reflog, wt);
}
free_worktrees(worktrees);
/*
* Not having any default heads isn't really fatal, but
@ -579,33 +603,36 @@ static void fsck_object_dir(const char *path)
stop_progress(&progress);
}
static int fsck_head_link(void)
static int fsck_head_link(const char *head_ref_name,
const char **head_points_at,
struct object_id *head_oid)
{
int null_is_error = 0;
if (verbose)
fprintf(stderr, "Checking HEAD link\n");
fprintf(stderr, "Checking %s link\n", head_ref_name);
head_points_at = resolve_ref_unsafe("HEAD", 0, &head_oid, NULL);
if (!head_points_at) {
*head_points_at = resolve_ref_unsafe(head_ref_name, 0, head_oid, NULL);
if (!*head_points_at) {
errors_found |= ERROR_REFS;
return error("Invalid HEAD");
return error("Invalid %s", head_ref_name);
}
if (!strcmp(head_points_at, "HEAD"))
if (!strcmp(*head_points_at, head_ref_name))
/* detached HEAD */
null_is_error = 1;
else if (!starts_with(head_points_at, "refs/heads/")) {
else if (!starts_with(*head_points_at, "refs/heads/")) {
errors_found |= ERROR_REFS;
return error("HEAD points to something strange (%s)",
head_points_at);
return error("%s points to something strange (%s)",
head_ref_name, *head_points_at);
}
if (is_null_oid(&head_oid)) {
if (is_null_oid(head_oid)) {
if (null_is_error) {
errors_found |= ERROR_REFS;
return error("HEAD: detached HEAD points at nothing");
return error("%s: detached HEAD points at nothing",
head_ref_name);
}
fprintf(stderr, "notice: HEAD points to an unborn branch (%s)\n",
head_points_at + 11);
fprintf(stderr, "notice: %s points to an unborn branch (%s)\n",
head_ref_name, *head_points_at + 11);
}
return 0;
}
@ -720,7 +747,6 @@ int cmd_fsck(int argc, const char **argv, const char *prefix)
git_config(fsck_config, NULL);
fsck_head_link();
if (connectivity_only) {
for_each_loose_object(mark_loose_for_connectivity, NULL, 0);
for_each_packed_object(mark_packed_for_connectivity, NULL, 0);

View File

@ -10,6 +10,7 @@
#include "diff.h"
#include "revision.h"
#include "reachable.h"
#include "worktree.h"
/* NEEDSWORK: switch to using parse_options */
static const char reflog_expire_usage[] =
@ -52,6 +53,7 @@ struct collect_reflog_cb {
struct collected_reflog **e;
int alloc;
int nr;
struct worktree *wt;
};
/* Remember to update object flag allocation in object.h */
@ -330,13 +332,27 @@ static int push_tip_to_list(const char *refname, const struct object_id *oid,
return 0;
}
static int is_head(const char *refname)
{
switch (ref_type(refname)) {
case REF_TYPE_OTHER_PSEUDOREF:
case REF_TYPE_MAIN_PSEUDOREF:
if (parse_worktree_ref(refname, NULL, NULL, &refname))
BUG("not a worktree ref: %s", refname);
break;
default:
break;
}
return !strcmp(refname, "HEAD");
}
static void reflog_expiry_prepare(const char *refname,
const struct object_id *oid,
void *cb_data)
{
struct expire_reflog_policy_cb *cb = cb_data;
if (!cb->cmd.expire_unreachable || !strcmp(refname, "HEAD")) {
if (!cb->cmd.expire_unreachable || is_head(refname)) {
cb->tip_commit = NULL;
cb->unreachable_expire_kind = UE_HEAD;
} else {
@ -388,8 +404,19 @@ static int collect_reflog(const char *ref, const struct object_id *oid, int unus
{
struct collected_reflog *e;
struct collect_reflog_cb *cb = cb_data;
struct strbuf newref = STRBUF_INIT;
/*
* Avoid collecting the same shared ref multiple times because
* they are available via all worktrees.
*/
if (!cb->wt->is_current && ref_type(ref) == REF_TYPE_NORMAL)
return 0;
strbuf_worktree_ref(cb->wt, &newref, ref);
FLEX_ALLOC_STR(e, reflog, newref.buf);
strbuf_release(&newref);
FLEX_ALLOC_STR(e, reflog, ref);
oidcpy(&e->oid, oid);
ALLOC_GROW(cb->e, cb->nr + 1, cb->alloc);
cb->e[cb->nr++] = e;
@ -512,7 +539,7 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
{
struct expire_reflog_policy_cb cb;
timestamp_t now = time(NULL);
int i, status, do_all;
int i, status, do_all, all_worktrees = 1;
int explicit_expiry = 0;
unsigned int flags = 0;
@ -549,6 +576,8 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
flags |= EXPIRE_REFLOGS_UPDATE_REF;
else if (!strcmp(arg, "--all"))
do_all = 1;
else if (!strcmp(arg, "--single-worktree"))
all_worktrees = 0;
else if (!strcmp(arg, "--verbose"))
flags |= EXPIRE_REFLOGS_VERBOSE;
else if (!strcmp(arg, "--")) {
@ -577,10 +606,19 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
if (do_all) {
struct collect_reflog_cb collected;
struct worktree **worktrees, **p;
int i;
memset(&collected, 0, sizeof(collected));
for_each_reflog(collect_reflog, &collected);
worktrees = get_worktrees(0);
for (p = worktrees; *p; p++) {
if (!all_worktrees && !(*p)->is_current)
continue;
collected.wt = *p;
refs_for_each_reflog(get_worktree_ref_store(*p),
collect_reflog, &collected);
}
free_worktrees(worktrees);
for (i = 0; i < collected.nr; i++) {
struct collected_reflog *e = collected.e[i];
set_reflog_expiry_param(&cb.cmd, explicit_expiry, e->reflog);

2
path.c
View File

@ -108,6 +108,7 @@ struct common_dir {
static struct common_dir common_list[] = {
{ 0, 1, 0, "branches" },
{ 0, 1, 0, "common" },
{ 0, 1, 0, "hooks" },
{ 0, 1, 0, "info" },
{ 0, 0, 1, "info/sparse-checkout" },
@ -118,6 +119,7 @@ static struct common_dir common_list[] = {
{ 0, 1, 0, "objects" },
{ 0, 1, 0, "refs" },
{ 0, 1, 1, "refs/bisect" },
{ 0, 1, 1, "refs/worktree" },
{ 0, 1, 0, "remotes" },
{ 0, 1, 0, "worktrees" },
{ 0, 1, 0, "rr-cache" },

24
refs.c
View File

@ -624,6 +624,7 @@ int dwim_log(const char *str, int len, struct object_id *oid, char **log)
static int is_per_worktree_ref(const char *refname)
{
return !strcmp(refname, "HEAD") ||
starts_with(refname, "refs/worktree/") ||
starts_with(refname, "refs/bisect/") ||
starts_with(refname, "refs/rewritten/");
}
@ -640,13 +641,34 @@ static int is_pseudoref_syntax(const char *refname)
return 1;
}
static int is_main_pseudoref_syntax(const char *refname)
{
return skip_prefix(refname, "main-worktree/", &refname) &&
*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 || !refname[1])
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;
return REF_TYPE_NORMAL;
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;
}
long get_files_ref_lock_timeout_ms(void)

8
refs.h
View File

@ -714,9 +714,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);

View File

@ -10,6 +10,7 @@
#include "../object.h"
#include "../dir.h"
#include "../chdir-notify.h"
#include "worktree.h"
/*
* This backend uses the following flags in `ref_update::flags` for
@ -149,6 +150,25 @@ 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;
const char *worktree_name;
int length;
if (parse_worktree_ref(refname, &worktree_name, &length, &real_ref))
BUG("refname %s is not a other-worktree ref", refname);
if (worktree_name)
strbuf_addf(sb, "%s/worktrees/%.*s/logs/%s", refs->gitcommondir,
length, worktree_name, real_ref);
else
strbuf_addf(sb, "%s/logs/%s", refs->gitcommondir,
real_ref);
}
static void files_reflog_path(struct files_ref_store *refs,
struct strbuf *sb,
const char *refname)
@ -158,6 +178,9 @@ 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:
case REF_TYPE_MAIN_PSEUDOREF:
return files_reflog_path_other_worktrees(refs, sb, refname);
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-worktree/", &refname))
BUG("ref %s is not a main pseudoref", refname);
/* fallthrough */
case REF_TYPE_OTHER_PSEUDOREF:
case REF_TYPE_NORMAL:
strbuf_addf(sb, "%s/%s", refs->gitcommondir, refname);
break;
@ -269,9 +297,9 @@ static void loose_fill_ref_dir(struct ref_store *ref_store,
closedir(d);
/*
* Manually add refs/bisect, which, being per-worktree, might
* not appear in the directory listing for refs/ in the main
* repo.
* Manually add refs/bisect and refs/worktree, which, being
* per-worktree, might not appear in the directory listing for
* refs/ in the main repo.
*/
if (!strcmp(dirname, "refs/")) {
int pos = search_ref_dir(dir, "refs/bisect/", 12);
@ -281,6 +309,14 @@ static void loose_fill_ref_dir(struct ref_store *ref_store,
dir->cache, "refs/bisect/", 12, 1);
add_entry_to_dir(dir, child_entry);
}
pos = search_ref_dir(dir, "refs/worktree/", 11);
if (pos < 0) {
struct ref_entry *child_entry = create_dir_entry(
dir->cache, "refs/worktree/", 11, 1);
add_entry_to_dir(dir, child_entry);
}
}
}

View File

@ -1177,7 +1177,7 @@ struct all_refs_cb {
int warned_bad_reflog;
struct rev_info *all_revs;
const char *name_for_errormsg;
struct ref_store *refs;
struct worktree *wt;
};
int ref_excluded(struct string_list *ref_excludes, const char *path)
@ -1214,7 +1214,7 @@ static void init_all_refs_cb(struct all_refs_cb *cb, struct rev_info *revs,
cb->all_revs = revs;
cb->all_flags = flags;
revs->rev_input_given = 1;
cb->refs = NULL;
cb->wt = NULL;
}
void clear_ref_exclusion(struct string_list **ref_excludes_p)
@ -1277,14 +1277,20 @@ static int handle_one_reflog_ent(struct object_id *ooid, struct object_id *noid,
return 0;
}
static int handle_one_reflog(const char *path, const struct object_id *oid,
static int handle_one_reflog(const char *refname_in_wt,
const struct object_id *oid,
int flag, void *cb_data)
{
struct all_refs_cb *cb = cb_data;
struct strbuf refname = STRBUF_INIT;
cb->warned_bad_reflog = 0;
cb->name_for_errormsg = path;
refs_for_each_reflog_ent(cb->refs, path,
strbuf_worktree_ref(cb->wt, &refname, refname_in_wt);
cb->name_for_errormsg = refname.buf;
refs_for_each_reflog_ent(get_main_ref_store(the_repository),
refname.buf,
handle_one_reflog_ent, cb_data);
strbuf_release(&refname);
return 0;
}
@ -1299,8 +1305,8 @@ static void add_other_reflogs_to_pending(struct all_refs_cb *cb)
if (wt->is_current)
continue;
cb->refs = get_worktree_ref_store(wt);
refs_for_each_reflog(cb->refs,
cb->wt = wt;
refs_for_each_reflog(get_worktree_ref_store(wt),
handle_one_reflog,
cb);
}
@ -1313,7 +1319,7 @@ void add_reflogs_to_pending(struct rev_info *revs, unsigned flags)
cb.all_revs = revs;
cb.all_flags = flags;
cb.refs = get_main_ref_store(revs->repo);
cb.wt = NULL;
for_each_reflog(handle_one_reflog, &cb);
if (!revs->single_worktree)

View File

@ -306,6 +306,8 @@ test_git_path GIT_COMMON_DIR=bar hooks/me bar/hooks/me
test_git_path GIT_COMMON_DIR=bar config bar/config
test_git_path GIT_COMMON_DIR=bar packed-refs bar/packed-refs
test_git_path GIT_COMMON_DIR=bar shallow bar/shallow
test_git_path GIT_COMMON_DIR=bar common bar/common
test_git_path GIT_COMMON_DIR=bar common/file bar/common/file
# In the tests below, $(pwd) must be used because it is a native path on
# Windows and avoids MSYS's path mangling (which simplifies "foo/../bar" and

View File

@ -368,4 +368,19 @@ test_expect_success 'continue walking past root commits' '
)
'
test_expect_success 'expire with multiple worktrees' '
git init main-wt &&
(
cd main-wt &&
test_tick &&
test_commit foo &&
git worktree add link-wt &&
test_tick &&
test_commit -C link-wt foobar &&
test_tick &&
git reflog expire --verbose --all --expire=$test_tick &&
test_must_be_empty .git/worktrees/link-wt/logs/HEAD
)
'
test_done

79
t/t1415-worktree-refs.sh Executable file
View File

@ -0,0 +1,79 @@
#!/bin/sh
test_description='per-worktree refs'
. ./test-lib.sh
test_expect_success 'setup' '
test_commit initial &&
test_commit wt1 &&
test_commit wt2 &&
git worktree add wt1 wt1 &&
git worktree add wt2 wt2 &&
git checkout initial &&
git update-ref refs/worktree/foo HEAD &&
git -C wt1 update-ref refs/worktree/foo HEAD &&
git -C wt2 update-ref refs/worktree/foo HEAD
'
test_expect_success 'refs/worktree must not be packed' '
git pack-refs --all &&
test_path_is_missing .git/refs/tags/wt1 &&
test_path_is_file .git/refs/worktree/foo &&
test_path_is_file .git/worktrees/wt1/refs/worktree/foo &&
test_path_is_file .git/worktrees/wt2/refs/worktree/foo
'
test_expect_success 'refs/worktree are per-worktree' '
test_cmp_rev worktree/foo initial &&
( cd wt1 && test_cmp_rev worktree/foo wt1 ) &&
( cd wt2 && test_cmp_rev worktree/foo wt2 )
'
test_expect_success 'resolve main-worktree/HEAD' '
test_cmp_rev main-worktree/HEAD initial &&
( cd wt1 && test_cmp_rev main-worktree/HEAD initial ) &&
( cd wt2 && test_cmp_rev main-worktree/HEAD initial )
'
test_expect_success 'ambiguous main-worktree/HEAD' '
mkdir -p .git/refs/heads/main-worktree &&
test_when_finished rm -f .git/refs/heads/main-worktree/HEAD &&
cp .git/HEAD .git/refs/heads/main-worktree/HEAD &&
git rev-parse main-worktree/HEAD 2>warn &&
grep "main-worktree/HEAD.*ambiguous" warn
'
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 'ambiguous worktrees/xx/HEAD' '
mkdir -p .git/refs/heads/worktrees/wt1 &&
test_when_finished rm -f .git/refs/heads/worktrees/wt1/HEAD &&
cp .git/HEAD .git/refs/heads/worktrees/wt1/HEAD &&
git rev-parse worktrees/wt1/HEAD 2>warn &&
grep "worktrees/wt1/HEAD.*ambiguous" warn
'
test_expect_success 'reflog of main-worktree/HEAD' '
git reflog HEAD | sed "s/HEAD/main-worktree\/HEAD/" >expected &&
git reflog main-worktree/HEAD >actual &&
test_cmp expected actual &&
git -C wt1 reflog main-worktree/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

View File

@ -101,6 +101,41 @@ test_expect_success 'HEAD link pointing at a funny place' '
grep "HEAD points to something strange" out
'
test_expect_success 'HEAD link pointing at a funny object (from different wt)' '
test_when_finished "mv .git/SAVED_HEAD .git/HEAD" &&
test_when_finished "rm -rf .git/worktrees wt" &&
git worktree add wt &&
mv .git/HEAD .git/SAVED_HEAD &&
echo $ZERO_OID >.git/HEAD &&
# avoid corrupt/broken HEAD from interfering with repo discovery
test_must_fail git -C wt fsck 2>out &&
grep "main-worktree/HEAD: detached HEAD points" out
'
test_expect_success 'other worktree HEAD link pointing at a funny object' '
test_when_finished "rm -rf .git/worktrees other" &&
git worktree add other &&
echo $ZERO_OID >.git/worktrees/other/HEAD &&
test_must_fail git fsck 2>out &&
grep "worktrees/other/HEAD: detached HEAD points" out
'
test_expect_success 'other worktree HEAD link pointing at missing object' '
test_when_finished "rm -rf .git/worktrees other" &&
git worktree add other &&
echo "Contents missing from repo" | git hash-object --stdin >.git/worktrees/other/HEAD &&
test_must_fail git fsck 2>out &&
grep "worktrees/other/HEAD: invalid sha1 pointer" out
'
test_expect_success 'other worktree HEAD link pointing at a funny place' '
test_when_finished "rm -rf .git/worktrees other" &&
git worktree add other &&
echo "ref: refs/funny/place" >.git/worktrees/other/HEAD &&
test_must_fail git fsck 2>out &&
grep "worktrees/other/HEAD points to something strange" out
'
test_expect_success 'email without @ is okay' '
git cat-file commit HEAD >basis &&
sed "s/@/AT/" basis >okay &&

View File

@ -487,6 +487,75 @@ int submodule_uses_worktrees(const char *path)
return ret;
}
int parse_worktree_ref(const char *worktree_ref, const char **name,
int *name_length, const char **ref)
{
if (skip_prefix(worktree_ref, "main-worktree/", &worktree_ref)) {
if (!*worktree_ref)
return -1;
if (name)
*name = NULL;
if (name_length)
*name_length = 0;
if (ref)
*ref = worktree_ref;
return 0;
}
if (skip_prefix(worktree_ref, "worktrees/", &worktree_ref)) {
const char *slash = strchr(worktree_ref, '/');
if (!slash || slash == worktree_ref || !slash[1])
return -1;
if (name)
*name = worktree_ref;
if (name_length)
*name_length = slash - worktree_ref;
if (ref)
*ref = slash + 1;
return 0;
}
return -1;
}
void strbuf_worktree_ref(const struct worktree *wt,
struct strbuf *sb,
const char *refname)
{
switch (ref_type(refname)) {
case REF_TYPE_PSEUDOREF:
case REF_TYPE_PER_WORKTREE:
if (wt && !wt->is_current) {
if (is_main_worktree(wt))
strbuf_addstr(sb, "main-worktree/");
else
strbuf_addf(sb, "worktrees/%s/", wt->id);
}
break;
case REF_TYPE_MAIN_PSEUDOREF:
case REF_TYPE_OTHER_PSEUDOREF:
break;
case REF_TYPE_NORMAL:
/*
* For shared refs, don't prefix worktrees/ or
* main-worktree/. It's not necessary and
* files-backend.c can't handle it anyway.
*/
break;
}
strbuf_addstr(sb, refname);
}
const char *worktree_ref(const struct worktree *wt, const char *refname)
{
static struct strbuf sb = STRBUF_INIT;
strbuf_reset(&sb);
strbuf_worktree_ref(wt, &sb, refname);
return sb.buf;
}
int other_head_refs(each_ref_fn fn, void *cb_data)
{
struct worktree **worktrees, **p;
@ -495,13 +564,17 @@ int other_head_refs(each_ref_fn fn, void *cb_data)
worktrees = get_worktrees(0);
for (p = worktrees; *p; p++) {
struct worktree *wt = *p;
struct ref_store *refs;
struct object_id oid;
int flag;
if (wt->is_current)
continue;
refs = get_worktree_ref_store(wt);
ret = refs_head_ref(refs, fn, cb_data);
if (!refs_read_ref_full(get_main_ref_store(the_repository),
worktree_ref(wt, "HEAD"),
RESOLVE_REF_READING,
&oid, &flag))
ret = fn(worktree_ref(wt, "HEAD"), &oid, flag, cb_data);
if (ret)
break;
}

View File

@ -108,4 +108,28 @@ extern const char *worktree_git_path(const struct worktree *wt,
const char *fmt, ...)
__attribute__((format (printf, 2, 3)));
/*
* Parse a worktree ref (i.e. with prefix main-worktree/ or
* worktrees/) and return the position of the worktree's name and
* length (or NULL and zero if it's main worktree), and ref.
*
* All name, name_length and ref arguments could be NULL.
*/
int parse_worktree_ref(const char *worktree_ref, const char **name,
int *name_length, const char **ref);
/*
* Return a refname suitable for access from the current ref store.
*/
void strbuf_worktree_ref(const struct worktree *wt,
struct strbuf *sb,
const char *refname);
/*
* Return a refname suitable for access from the current ref
* store. The result will be destroyed at the next call.
*/
const char *worktree_ref(const struct worktree *wt,
const char *refname);
#endif