Merge branch 'dt/refs-bisection'
Move the refs used during a "git bisect" session to per-worktree hierarchy refs/worktree/* so that independent bisect sessions can be done in different worktrees. * dt/refs-bisection: refs: make refs/bisect/* per-worktree path: optimize common dir checking refs: clean up common_list
This commit is contained in:
commit
297ae7151f
@ -413,8 +413,9 @@ exclude;;
|
||||
|
||||
[[def_per_worktree_ref]]per-worktree ref::
|
||||
Refs that are per-<<def_working_tree,worktree>>, rather than
|
||||
global. This is presently only <<def_HEAD,HEAD>>, but might
|
||||
later include other unusual refs.
|
||||
global. This is presently only <<def_HEAD,HEAD>> and any refs
|
||||
that start with `refs/bisect/`, but might later include other
|
||||
unusual refs.
|
||||
|
||||
[[def_pseudoref]]pseudoref::
|
||||
Pseudorefs are a class of files under `$GIT_DIR` which behave
|
||||
|
281
path.c
281
path.c
@ -91,54 +91,271 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
|
||||
buf->buf[newlen] = '/';
|
||||
}
|
||||
|
||||
static const char *common_list[] = {
|
||||
"/branches", "/hooks", "/info", "!/logs", "/lost-found",
|
||||
"/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn",
|
||||
"config", "!gc.pid", "packed-refs", "shallow",
|
||||
NULL
|
||||
struct common_dir {
|
||||
/* Not considered garbage for report_linked_checkout_garbage */
|
||||
unsigned ignore_garbage:1;
|
||||
unsigned is_dir:1;
|
||||
/* Not common even though its parent is */
|
||||
unsigned exclude:1;
|
||||
const char *dirname;
|
||||
};
|
||||
|
||||
static struct common_dir common_list[] = {
|
||||
{ 0, 1, 0, "branches" },
|
||||
{ 0, 1, 0, "hooks" },
|
||||
{ 0, 1, 0, "info" },
|
||||
{ 0, 0, 1, "info/sparse-checkout" },
|
||||
{ 1, 1, 0, "logs" },
|
||||
{ 1, 1, 1, "logs/HEAD" },
|
||||
{ 0, 1, 1, "logs/refs/bisect" },
|
||||
{ 0, 1, 0, "lost-found" },
|
||||
{ 0, 1, 0, "objects" },
|
||||
{ 0, 1, 0, "refs" },
|
||||
{ 0, 1, 1, "refs/bisect" },
|
||||
{ 0, 1, 0, "remotes" },
|
||||
{ 0, 1, 0, "worktrees" },
|
||||
{ 0, 1, 0, "rr-cache" },
|
||||
{ 0, 1, 0, "svn" },
|
||||
{ 0, 0, 0, "config" },
|
||||
{ 1, 0, 0, "gc.pid" },
|
||||
{ 0, 0, 0, "packed-refs" },
|
||||
{ 0, 0, 0, "shallow" },
|
||||
{ 0, 0, 0, NULL }
|
||||
};
|
||||
|
||||
/*
|
||||
* A compressed trie. A trie node consists of zero or more characters that
|
||||
* are common to all elements with this prefix, optionally followed by some
|
||||
* children. If value is not NULL, the trie node is a terminal node.
|
||||
*
|
||||
* For example, consider the following set of strings:
|
||||
* abc
|
||||
* def
|
||||
* definite
|
||||
* definition
|
||||
*
|
||||
* The trie would look look like:
|
||||
* root: len = 0, children a and d non-NULL, value = NULL.
|
||||
* a: len = 2, contents = bc, value = (data for "abc")
|
||||
* d: len = 2, contents = ef, children i non-NULL, value = (data for "def")
|
||||
* i: len = 3, contents = nit, children e and i non-NULL, value = NULL
|
||||
* e: len = 0, children all NULL, value = (data for "definite")
|
||||
* i: len = 2, contents = on, children all NULL,
|
||||
* value = (data for "definition")
|
||||
*/
|
||||
struct trie {
|
||||
struct trie *children[256];
|
||||
int len;
|
||||
char *contents;
|
||||
void *value;
|
||||
};
|
||||
|
||||
static struct trie *make_trie_node(const char *key, void *value)
|
||||
{
|
||||
struct trie *new_node = xcalloc(1, sizeof(*new_node));
|
||||
new_node->len = strlen(key);
|
||||
if (new_node->len) {
|
||||
new_node->contents = xmalloc(new_node->len);
|
||||
memcpy(new_node->contents, key, new_node->len);
|
||||
}
|
||||
new_node->value = value;
|
||||
return new_node;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add a key/value pair to a trie. The key is assumed to be \0-terminated.
|
||||
* If there was an existing value for this key, return it.
|
||||
*/
|
||||
static void *add_to_trie(struct trie *root, const char *key, void *value)
|
||||
{
|
||||
struct trie *child;
|
||||
void *old;
|
||||
int i;
|
||||
|
||||
if (!*key) {
|
||||
/* we have reached the end of the key */
|
||||
old = root->value;
|
||||
root->value = value;
|
||||
return old;
|
||||
}
|
||||
|
||||
for (i = 0; i < root->len; i++) {
|
||||
if (root->contents[i] == key[i])
|
||||
continue;
|
||||
|
||||
/*
|
||||
* Split this node: child will contain this node's
|
||||
* existing children.
|
||||
*/
|
||||
child = malloc(sizeof(*child));
|
||||
memcpy(child->children, root->children, sizeof(root->children));
|
||||
|
||||
child->len = root->len - i - 1;
|
||||
if (child->len) {
|
||||
child->contents = xstrndup(root->contents + i + 1,
|
||||
child->len);
|
||||
}
|
||||
child->value = root->value;
|
||||
root->value = NULL;
|
||||
root->len = i;
|
||||
|
||||
memset(root->children, 0, sizeof(root->children));
|
||||
root->children[(unsigned char)root->contents[i]] = child;
|
||||
|
||||
/* This is the newly-added child. */
|
||||
root->children[(unsigned char)key[i]] =
|
||||
make_trie_node(key + i + 1, value);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* We have matched the entire compressed section */
|
||||
if (key[i]) {
|
||||
child = root->children[(unsigned char)key[root->len]];
|
||||
if (child) {
|
||||
return add_to_trie(child, key + root->len + 1, value);
|
||||
} else {
|
||||
child = make_trie_node(key + root->len + 1, value);
|
||||
root->children[(unsigned char)key[root->len]] = child;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
old = root->value;
|
||||
root->value = value;
|
||||
return old;
|
||||
}
|
||||
|
||||
typedef int (*match_fn)(const char *unmatched, void *data, void *baton);
|
||||
|
||||
/*
|
||||
* Search a trie for some key. Find the longest /-or-\0-terminated
|
||||
* prefix of the key for which the trie contains a value. Call fn
|
||||
* with the unmatched portion of the key and the found value, and
|
||||
* return its return value. If there is no such prefix, return -1.
|
||||
*
|
||||
* The key is partially normalized: consecutive slashes are skipped.
|
||||
*
|
||||
* For example, consider the trie containing only [refs,
|
||||
* refs/worktree] (both with values).
|
||||
*
|
||||
* | key | unmatched | val from node | return value |
|
||||
* |-----------------|------------|---------------|--------------|
|
||||
* | a | not called | n/a | -1 |
|
||||
* | refs | \0 | refs | as per fn |
|
||||
* | refs/ | / | refs | as per fn |
|
||||
* | refs/w | /w | refs | as per fn |
|
||||
* | refs/worktree | \0 | refs/worktree | as per fn |
|
||||
* | refs/worktree/ | / | refs/worktree | as per fn |
|
||||
* | refs/worktree/a | /a | refs/worktree | as per fn |
|
||||
* |-----------------|------------|---------------|--------------|
|
||||
*
|
||||
*/
|
||||
static int trie_find(struct trie *root, const char *key, match_fn fn,
|
||||
void *baton)
|
||||
{
|
||||
int i;
|
||||
int result;
|
||||
struct trie *child;
|
||||
|
||||
if (!*key) {
|
||||
/* we have reached the end of the key */
|
||||
if (root->value && !root->len)
|
||||
return fn(key, root->value, baton);
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < root->len; i++) {
|
||||
/* Partial path normalization: skip consecutive slashes. */
|
||||
if (key[i] == '/' && key[i+1] == '/') {
|
||||
key++;
|
||||
continue;
|
||||
}
|
||||
if (root->contents[i] != key[i])
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Matched the entire compressed section */
|
||||
key += i;
|
||||
if (!*key)
|
||||
/* End of key */
|
||||
return fn(key, root->value, baton);
|
||||
|
||||
/* Partial path normalization: skip consecutive slashes */
|
||||
while (key[0] == '/' && key[1] == '/')
|
||||
key++;
|
||||
|
||||
child = root->children[(unsigned char)*key];
|
||||
if (child)
|
||||
result = trie_find(child, key + 1, fn, baton);
|
||||
else
|
||||
result = -1;
|
||||
|
||||
if (result >= 0 || (*key != '/' && *key != 0))
|
||||
return result;
|
||||
if (root->value)
|
||||
return fn(key, root->value, baton);
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
|
||||
static struct trie common_trie;
|
||||
static int common_trie_done_setup;
|
||||
|
||||
static void init_common_trie(void)
|
||||
{
|
||||
struct common_dir *p;
|
||||
|
||||
if (common_trie_done_setup)
|
||||
return;
|
||||
|
||||
for (p = common_list; p->dirname; p++)
|
||||
add_to_trie(&common_trie, p->dirname, p);
|
||||
|
||||
common_trie_done_setup = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function for update_common_dir: returns 1 if the dir
|
||||
* prefix is common.
|
||||
*/
|
||||
static int check_common(const char *unmatched, void *value, void *baton)
|
||||
{
|
||||
struct common_dir *dir = value;
|
||||
|
||||
if (!dir)
|
||||
return 0;
|
||||
|
||||
if (dir->is_dir && (unmatched[0] == 0 || unmatched[0] == '/'))
|
||||
return !dir->exclude;
|
||||
|
||||
if (!dir->is_dir && unmatched[0] == 0)
|
||||
return !dir->exclude;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void update_common_dir(struct strbuf *buf, int git_dir_len)
|
||||
{
|
||||
char *base = buf->buf + git_dir_len;
|
||||
const char **p;
|
||||
|
||||
if (is_dir_file(base, "logs", "HEAD") ||
|
||||
is_dir_file(base, "info", "sparse-checkout"))
|
||||
return; /* keep this in $GIT_DIR */
|
||||
for (p = common_list; *p; p++) {
|
||||
const char *path = *p;
|
||||
int is_dir = 0;
|
||||
if (*path == '!')
|
||||
path++;
|
||||
if (*path == '/') {
|
||||
path++;
|
||||
is_dir = 1;
|
||||
}
|
||||
if (is_dir && dir_prefix(base, path)) {
|
||||
replace_dir(buf, git_dir_len, get_git_common_dir());
|
||||
return;
|
||||
}
|
||||
if (!is_dir && !strcmp(base, path)) {
|
||||
replace_dir(buf, git_dir_len, get_git_common_dir());
|
||||
return;
|
||||
}
|
||||
}
|
||||
init_common_trie();
|
||||
if (trie_find(&common_trie, base, check_common, NULL) > 0)
|
||||
replace_dir(buf, git_dir_len, get_git_common_dir());
|
||||
}
|
||||
|
||||
void report_linked_checkout_garbage(void)
|
||||
{
|
||||
struct strbuf sb = STRBUF_INIT;
|
||||
const char **p;
|
||||
const struct common_dir *p;
|
||||
int len;
|
||||
|
||||
if (!git_common_dir_env)
|
||||
return;
|
||||
strbuf_addf(&sb, "%s/", get_git_dir());
|
||||
len = sb.len;
|
||||
for (p = common_list; *p; p++) {
|
||||
const char *path = *p;
|
||||
if (*path == '!')
|
||||
for (p = common_list; p->dirname; p++) {
|
||||
const char *path = p->dirname;
|
||||
if (p->ignore_garbage)
|
||||
continue;
|
||||
strbuf_setlen(&sb, len);
|
||||
strbuf_addstr(&sb, path);
|
||||
|
32
refs.c
32
refs.c
@ -304,6 +304,11 @@ struct ref_entry {
|
||||
};
|
||||
|
||||
static void read_loose_refs(const char *dirname, struct ref_dir *dir);
|
||||
static int search_ref_dir(struct ref_dir *dir, const char *refname, size_t len);
|
||||
static struct ref_entry *create_dir_entry(struct ref_cache *ref_cache,
|
||||
const char *dirname, size_t len,
|
||||
int incomplete);
|
||||
static void add_entry_to_dir(struct ref_dir *dir, struct ref_entry *entry);
|
||||
|
||||
static struct ref_dir *get_ref_dir(struct ref_entry *entry)
|
||||
{
|
||||
@ -312,6 +317,24 @@ static struct ref_dir *get_ref_dir(struct ref_entry *entry)
|
||||
dir = &entry->u.subdir;
|
||||
if (entry->flag & REF_INCOMPLETE) {
|
||||
read_loose_refs(entry->name, dir);
|
||||
|
||||
/*
|
||||
* Manually add refs/bisect, which, being
|
||||
* per-worktree, might not appear in the directory
|
||||
* listing for refs/ in the main repo.
|
||||
*/
|
||||
if (!strcmp(entry->name, "refs/")) {
|
||||
int pos = search_ref_dir(dir, "refs/bisect/", 12);
|
||||
if (pos < 0) {
|
||||
struct ref_entry *child_entry;
|
||||
child_entry = create_dir_entry(dir->ref_cache,
|
||||
"refs/bisect/",
|
||||
12, 1);
|
||||
add_entry_to_dir(dir, child_entry);
|
||||
read_loose_refs("refs/bisect",
|
||||
&child_entry->u.subdir);
|
||||
}
|
||||
}
|
||||
entry->flag &= ~REF_INCOMPLETE;
|
||||
}
|
||||
return dir;
|
||||
@ -2649,6 +2672,8 @@ struct pack_refs_cb_data {
|
||||
struct ref_to_prune *ref_to_prune;
|
||||
};
|
||||
|
||||
static int is_per_worktree_ref(const char *refname);
|
||||
|
||||
/*
|
||||
* An each_ref_entry_fn that is run over loose references only. If
|
||||
* the loose reference can be packed, add an entry in the packed ref
|
||||
@ -2662,6 +2687,10 @@ static int pack_if_possible_fn(struct ref_entry *entry, void *cb_data)
|
||||
struct ref_entry *packed_entry;
|
||||
int is_tag_ref = starts_with(entry->name, "refs/tags/");
|
||||
|
||||
/* Do not pack per-worktree refs: */
|
||||
if (is_per_worktree_ref(entry->name))
|
||||
return 0;
|
||||
|
||||
/* ALWAYS pack tags */
|
||||
if (!(cb->flags & PACK_REFS_ALL) && !is_tag_ref)
|
||||
return 0;
|
||||
@ -2856,7 +2885,8 @@ static int delete_ref_loose(struct ref_lock *lock, int flag, struct strbuf *err)
|
||||
|
||||
static int is_per_worktree_ref(const char *refname)
|
||||
{
|
||||
return !strcmp(refname, "HEAD");
|
||||
return !strcmp(refname, "HEAD") ||
|
||||
starts_with(refname, "refs/bisect/");
|
||||
}
|
||||
|
||||
static int is_pseudoref_syntax(const char *refname)
|
||||
|
@ -266,15 +266,21 @@ test_expect_success 'setup common repository' 'git --git-dir=bar init'
|
||||
test_git_path GIT_COMMON_DIR=bar index .git/index
|
||||
test_git_path GIT_COMMON_DIR=bar HEAD .git/HEAD
|
||||
test_git_path GIT_COMMON_DIR=bar logs/HEAD .git/logs/HEAD
|
||||
test_git_path GIT_COMMON_DIR=bar logs/refs/bisect/foo .git/logs/refs/bisect/foo
|
||||
test_git_path GIT_COMMON_DIR=bar logs/refs/bisec/foo bar/logs/refs/bisec/foo
|
||||
test_git_path GIT_COMMON_DIR=bar logs/refs/bisec bar/logs/refs/bisec
|
||||
test_git_path GIT_COMMON_DIR=bar logs/refs/bisectfoo bar/logs/refs/bisectfoo
|
||||
test_git_path GIT_COMMON_DIR=bar objects bar/objects
|
||||
test_git_path GIT_COMMON_DIR=bar objects/bar bar/objects/bar
|
||||
test_git_path GIT_COMMON_DIR=bar info/exclude bar/info/exclude
|
||||
test_git_path GIT_COMMON_DIR=bar info/grafts bar/info/grafts
|
||||
test_git_path GIT_COMMON_DIR=bar info/sparse-checkout .git/info/sparse-checkout
|
||||
test_git_path GIT_COMMON_DIR=bar info//sparse-checkout .git/info//sparse-checkout
|
||||
test_git_path GIT_COMMON_DIR=bar remotes/bar bar/remotes/bar
|
||||
test_git_path GIT_COMMON_DIR=bar branches/bar bar/branches/bar
|
||||
test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master bar/logs/refs/heads/master
|
||||
test_git_path GIT_COMMON_DIR=bar refs/heads/master bar/refs/heads/master
|
||||
test_git_path GIT_COMMON_DIR=bar refs/bisect/foo .git/refs/bisect/foo
|
||||
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
|
||||
|
@ -1130,4 +1130,23 @@ test_expect_success ULIMIT_FILE_DESCRIPTORS 'large transaction deleting branches
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'handle per-worktree refs in refs/bisect' '
|
||||
git commit --allow-empty -m "initial commit" &&
|
||||
git worktree add -b branch worktree &&
|
||||
(
|
||||
cd worktree &&
|
||||
git commit --allow-empty -m "test commit" &&
|
||||
git for-each-ref >for-each-ref.out &&
|
||||
! grep refs/bisect for-each-ref.out &&
|
||||
git update-ref refs/bisect/something HEAD &&
|
||||
git rev-parse refs/bisect/something >../worktree-head &&
|
||||
git for-each-ref | grep refs/bisect/something
|
||||
) &&
|
||||
test_path_is_missing .git/refs/bisect &&
|
||||
test_must_fail git rev-parse refs/bisect/something &&
|
||||
git update-ref refs/bisect/something HEAD &&
|
||||
git rev-parse refs/bisect/something >main-head &&
|
||||
! test_cmp main-head worktree-head
|
||||
'
|
||||
|
||||
test_done
|
||||
|
@ -160,6 +160,13 @@ test_expect_success 'pack ref directly below refs/' '
|
||||
test_path_is_missing .git/refs/top
|
||||
'
|
||||
|
||||
test_expect_success 'do not pack ref in refs/bisect' '
|
||||
git update-ref refs/bisect/local HEAD &&
|
||||
git pack-refs --all --prune &&
|
||||
! grep refs/bisect/local .git/packed-refs >/dev/null &&
|
||||
test_path_is_file .git/refs/bisect/local
|
||||
'
|
||||
|
||||
test_expect_success 'disable reflogs' '
|
||||
git config core.logallrefupdates false &&
|
||||
rm -rf .git/logs
|
||||
|
Loading…
Reference in New Issue
Block a user