From 0701530c2663f82ff4b678de27db4c61f8b3092f Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 31 Aug 2015 22:13:09 -0400 Subject: refs: clean up common_list Instead of common_list having formatting like ! and /, use a struct to hold common_list data in a structured form. We don't use 'exclude' yet; instead, we keep the old codepath that handles info/sparse-checkout and logs/HEAD. Later, we will use exclude. [jc: with "make common_list[] static" clean-up from Ramsay squashed in] Signed-off-by: David Turner Signed-off-by: Junio C Hamano --- path.c | 58 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/path.c b/path.c index 95acbafa68..2215ab5bf7 100644 --- a/path.c +++ b/path.c @@ -91,35 +91,51 @@ 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, 0, "lost-found" }, + { 0, 1, 0, "objects" }, + { 0, 1, 0, "refs" }, + { 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 } }; static void update_common_dir(struct strbuf *buf, int git_dir_len) { char *base = buf->buf + git_dir_len; - const char **p; + const struct common_dir *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)) { + for (p = common_list; p->dirname; p++) { + const char *path = p->dirname; + if (p->is_dir && dir_prefix(base, path)) { replace_dir(buf, git_dir_len, get_git_common_dir()); return; } - if (!is_dir && !strcmp(base, path)) { + if (!p->is_dir && !strcmp(base, path)) { replace_dir(buf, git_dir_len, get_git_common_dir()); return; } @@ -129,16 +145,16 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len) 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); -- cgit v1.2.3 From 4e09cf2acf00c63848c365479c61d80af62eceba Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 31 Aug 2015 22:13:10 -0400 Subject: path: optimize common dir checking Instead of a linear search over common_list to check whether a path is common, use a trie. The trie search operates on path prefixes, and handles excludes. Signed-off-by: David Turner Signed-off-by: Junio C Hamano --- path.c | 227 ++++++++++++++++++++++++++++++++++++++++++++++---- t/t0060-path-utils.sh | 1 + 2 files changed, 214 insertions(+), 14 deletions(-) diff --git a/path.c b/path.c index 2215ab5bf7..7e959c9cdd 100644 --- a/path.c +++ b/path.c @@ -121,25 +121,224 @@ static struct common_dir common_list[] = { { 0, 0, 0, NULL } }; -static void update_common_dir(struct strbuf *buf, int git_dir_len) +/* + * 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) { - char *base = buf->buf + git_dir_len; - const struct common_dir *p; + 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; +} - 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->dirname; p++) { - const char *path = p->dirname; - if (p->is_dir && dir_prefix(base, path)) { - replace_dir(buf, git_dir_len, get_git_common_dir()); - return; +/* + * 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); } - if (!p->is_dir && !strcmp(base, path)) { - replace_dir(buf, git_dir_len, get_git_common_dir()); - return; + 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; + 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) diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index 93605f42f2..1ca49e102d 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -271,6 +271,7 @@ 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 -- cgit v1.2.3 From ce414b33ec0385a2aa701b6eaa712695ac13717a Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 31 Aug 2015 22:13:11 -0400 Subject: refs: make refs/bisect/* per-worktree We need the place we stick refs for bisects in progress to not be shared between worktrees. So we make the refs/bisect/ hierarchy per-worktree. The is_per_worktree_ref function and associated docs learn that refs/bisect/ is per-worktree, as does the git_path code in path.c The ref-packing functions learn that per-worktree refs should not be packed (since packed-refs is common rather than per-worktree). Since refs/bisect is per-worktree, logs/refs/bisect should be too. Signed-off-by: David Turner Signed-off-by: Junio C Hamano --- Documentation/glossary-content.txt | 5 +++-- path.c | 2 ++ refs.c | 32 +++++++++++++++++++++++++++++++- t/t0060-path-utils.sh | 5 +++++ t/t1400-update-ref.sh | 19 +++++++++++++++++++ t/t3210-pack-refs.sh | 7 +++++++ 6 files changed, 67 insertions(+), 3 deletions(-) diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt index 8c6478b2f2..e225974253 100644 --- a/Documentation/glossary-content.txt +++ b/Documentation/glossary-content.txt @@ -413,8 +413,9 @@ exclude;; [[def_per_worktree_ref]]per-worktree ref:: Refs that are per-<>, rather than - global. This is presently only <>, but might - later include other unusual refs. + global. This is presently only <> 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 diff --git a/path.c b/path.c index 7e959c9cdd..65beb2dca3 100644 --- a/path.c +++ b/path.c @@ -107,9 +107,11 @@ static struct common_dir common_list[] = { { 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" }, diff --git a/refs.c b/refs.c index 4e15f60d98..24401f71f8 100644 --- a/refs.c +++ b/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) diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index 1ca49e102d..627ef854d5 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -266,6 +266,10 @@ 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 @@ -276,6 +280,7 @@ 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 diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index 97406fa4b1..af1b20dd5c 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -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 diff --git a/t/t3210-pack-refs.sh b/t/t3210-pack-refs.sh index 7b5b6d452e..db244d2f88 100755 --- a/t/t3210-pack-refs.sh +++ b/t/t3210-pack-refs.sh @@ -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 -- cgit v1.2.3