Skip to content

Commit

Permalink
Merge branch 'nd/per-worktree-ref-iteration'
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gitster committed Nov 13, 2018
2 parents 11aa560 + 14f74d5 commit e146cc9
Show file tree
Hide file tree
Showing 16 changed files with 449 additions and 47 deletions.
7 changes: 6 additions & 1 deletion Documentation/git-reflog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion Documentation/git-worktree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions Documentation/gitrepository-layout.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
68 changes: 47 additions & 21 deletions builtin/fsck.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "packfile.h"
#include "object-store.h"
#include "run-command.h"
#include "worktree.h"

#define REACHABLE 0x0001
#define SEEN 0x0002
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
46 changes: 42 additions & 4 deletions builtin/reflog.c
Original file line number Diff line number Diff line change
Expand Up @@ -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[] =
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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, "--")) {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions path.c
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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" },
Expand Down
24 changes: 23 additions & 1 deletion refs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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/");
}
Expand All @@ -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)
Expand Down
8 changes: 5 additions & 3 deletions refs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit e146cc9

Please sign in to comment.