refs: make refs/bisect/* per-worktree
[gitweb.git] / path.c
diff --git a/path.c b/path.c
index 6b537ccfff452b7fbbe41446c001c8fda0679bea..65beb2dca3335912b81b19564ae1db3ca51067be 100644 (file)
--- a/path.c
+++ b/path.c
@@ -4,6 +4,7 @@
 #include "cache.h"
 #include "strbuf.h"
 #include "string-list.h"
+#include "dir.h"
 
 static int get_st_mode_bits(const char *path, int *mode)
 {
@@ -16,11 +17,15 @@ static int get_st_mode_bits(const char *path, int *mode)
 
 static char bad_path[] = "/bad-path/";
 
-static char *get_pathname(void)
+static struct strbuf *get_pathname(void)
 {
-       static char pathname_array[4][PATH_MAX];
+       static struct strbuf pathname_array[4] = {
+               STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+       };
        static int index;
-       return pathname_array[3 & ++index];
+       struct strbuf *sb = &pathname_array[3 & ++index];
+       strbuf_reset(sb);
+       return sb;
 }
 
 static char *cleanup_path(char *path)
@@ -34,6 +39,13 @@ static char *cleanup_path(char *path)
        return path;
 }
 
+static void strbuf_cleanup_path(struct strbuf *sb)
+{
+       char *path = cleanup_path(sb->buf);
+       if (path > sb->buf)
+               strbuf_remove(sb, 0, path - sb->buf);
+}
+
 char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 {
        va_list args;
@@ -49,124 +61,424 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
        return cleanup_path(buf);
 }
 
-static char *vsnpath(char *buf, size_t n, const char *fmt, va_list args)
+static int dir_prefix(const char *buf, const char *dir)
 {
-       const char *git_dir = get_git_dir();
-       size_t len;
+       int len = strlen(dir);
+       return !strncmp(buf, dir, len) &&
+               (is_dir_sep(buf[len]) || buf[len] == '\0');
+}
 
-       len = strlen(git_dir);
-       if (n < len + 1)
-               goto bad;
-       memcpy(buf, git_dir, len);
-       if (len && !is_dir_sep(git_dir[len-1]))
-               buf[len++] = '/';
-       len += vsnprintf(buf + len, n - len, fmt, args);
-       if (len >= n)
-               goto bad;
-       return cleanup_path(buf);
-bad:
-       strlcpy(buf, bad_path, n);
-       return buf;
+/* $buf =~ m|$dir/+$file| but without regex */
+static int is_dir_file(const char *buf, const char *dir, const char *file)
+{
+       int len = strlen(dir);
+       if (strncmp(buf, dir, len) || !is_dir_sep(buf[len]))
+               return 0;
+       while (is_dir_sep(buf[len]))
+               len++;
+       return !strcmp(buf + len, file);
+}
+
+static void replace_dir(struct strbuf *buf, int len, const char *newdir)
+{
+       int newlen = strlen(newdir);
+       int need_sep = (buf->buf[len] && !is_dir_sep(buf->buf[len])) &&
+               !is_dir_sep(newdir[newlen - 1]);
+       if (need_sep)
+               len--;   /* keep one char, to be replaced with '/'  */
+       strbuf_splice(buf, 0, len, newdir, newlen);
+       if (need_sep)
+               buf->buf[newlen] = '/';
+}
+
+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;
+       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 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->dirname; p++) {
+               const char *path = p->dirname;
+               if (p->ignore_garbage)
+                       continue;
+               strbuf_setlen(&sb, len);
+               strbuf_addstr(&sb, path);
+               if (file_exists(sb.buf))
+                       report_garbage("unused in linked checkout", sb.buf);
+       }
+       strbuf_release(&sb);
+}
+
+static void adjust_git_path(struct strbuf *buf, int git_dir_len)
+{
+       const char *base = buf->buf + git_dir_len;
+       if (git_graft_env && is_dir_file(base, "info", "grafts"))
+               strbuf_splice(buf, 0, buf->len,
+                             get_graft_file(), strlen(get_graft_file()));
+       else if (git_index_env && !strcmp(base, "index"))
+               strbuf_splice(buf, 0, buf->len,
+                             get_index_file(), strlen(get_index_file()));
+       else if (git_db_env && dir_prefix(base, "objects"))
+               replace_dir(buf, git_dir_len + 7, get_object_directory());
+       else if (git_common_dir_env)
+               update_common_dir(buf, git_dir_len);
+}
+
+static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
+{
+       int gitdir_len;
+       strbuf_addstr(buf, get_git_dir());
+       if (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))
+               strbuf_addch(buf, '/');
+       gitdir_len = buf->len;
+       strbuf_vaddf(buf, fmt, args);
+       adjust_git_path(buf, gitdir_len);
+       strbuf_cleanup_path(buf);
 }
 
-char *git_snpath(char *buf, size_t n, const char *fmt, ...)
+void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 {
-       char *ret;
        va_list args;
        va_start(args, fmt);
-       ret = vsnpath(buf, n, fmt, args);
+       do_git_path(sb, fmt, args);
        va_end(args);
-       return ret;
 }
 
-char *git_pathdup(const char *fmt, ...)
+const char *git_path(const char *fmt, ...)
 {
-       char path[PATH_MAX], *ret;
+       struct strbuf *pathname = get_pathname();
        va_list args;
        va_start(args, fmt);
-       ret = vsnpath(path, sizeof(path), fmt, args);
+       do_git_path(pathname, fmt, args);
        va_end(args);
-       return xstrdup(ret);
+       return pathname->buf;
 }
 
-char *mkpathdup(const char *fmt, ...)
+char *git_pathdup(const char *fmt, ...)
 {
-       char *path;
-       struct strbuf sb = STRBUF_INIT;
+       struct strbuf path = STRBUF_INIT;
        va_list args;
-
        va_start(args, fmt);
-       strbuf_vaddf(&sb, fmt, args);
+       do_git_path(&path, fmt, args);
        va_end(args);
-       path = xstrdup(cleanup_path(sb.buf));
-
-       strbuf_release(&sb);
-       return path;
+       return strbuf_detach(&path, NULL);
 }
 
-char *mkpath(const char *fmt, ...)
+char *mkpathdup(const char *fmt, ...)
 {
+       struct strbuf sb = STRBUF_INIT;
        va_list args;
-       unsigned len;
-       char *pathname = get_pathname();
-
        va_start(args, fmt);
-       len = vsnprintf(pathname, PATH_MAX, fmt, args);
+       strbuf_vaddf(&sb, fmt, args);
        va_end(args);
-       if (len >= PATH_MAX)
-               return bad_path;
-       return cleanup_path(pathname);
+       strbuf_cleanup_path(&sb);
+       return strbuf_detach(&sb, NULL);
 }
 
-char *git_path(const char *fmt, ...)
+const char *mkpath(const char *fmt, ...)
 {
-       char *pathname = get_pathname();
        va_list args;
-       char *ret;
-
+       struct strbuf *pathname = get_pathname();
        va_start(args, fmt);
-       ret = vsnpath(pathname, PATH_MAX, fmt, args);
+       strbuf_vaddf(pathname, fmt, args);
        va_end(args);
-       return ret;
+       return cleanup_path(pathname->buf);
 }
 
-char *git_path_submodule(const char *path, const char *fmt, ...)
+static void do_submodule_path(struct strbuf *buf, const char *path,
+                             const char *fmt, va_list args)
 {
-       char *pathname = get_pathname();
-       struct strbuf buf = STRBUF_INIT;
        const char *git_dir;
-       va_list args;
-       unsigned len;
-
-       len = strlen(path);
-       if (len > PATH_MAX-100)
-               return bad_path;
 
-       strbuf_addstr(&buf, path);
-       if (len && path[len-1] != '/')
-               strbuf_addch(&buf, '/');
-       strbuf_addstr(&buf, ".git");
+       strbuf_addstr(buf, path);
+       if (buf->len && buf->buf[buf->len - 1] != '/')
+               strbuf_addch(buf, '/');
+       strbuf_addstr(buf, ".git");
 
-       git_dir = read_gitfile(buf.buf);
+       git_dir = read_gitfile(buf->buf);
        if (git_dir) {
-               strbuf_reset(&buf);
-               strbuf_addstr(&buf, git_dir);
+               strbuf_reset(buf);
+               strbuf_addstr(buf, git_dir);
        }
-       strbuf_addch(&buf, '/');
+       strbuf_addch(buf, '/');
 
-       if (buf.len >= PATH_MAX)
-               return bad_path;
-       memcpy(pathname, buf.buf, buf.len + 1);
+       strbuf_vaddf(buf, fmt, args);
+       strbuf_cleanup_path(buf);
+}
 
-       strbuf_release(&buf);
-       len = strlen(pathname);
+char *git_pathdup_submodule(const char *path, const char *fmt, ...)
+{
+       va_list args;
+       struct strbuf buf = STRBUF_INIT;
+       va_start(args, fmt);
+       do_submodule_path(&buf, path, fmt, args);
+       va_end(args);
+       return strbuf_detach(&buf, NULL);
+}
 
+void strbuf_git_path_submodule(struct strbuf *buf, const char *path,
+                              const char *fmt, ...)
+{
+       va_list args;
        va_start(args, fmt);
-       len += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);
+       do_submodule_path(buf, path, fmt, args);
        va_end(args);
-       if (len >= PATH_MAX)
-               return bad_path;
-       return cleanup_path(pathname);
 }
 
 int validate_headref(const char *path)
@@ -838,3 +1150,13 @@ char *xdg_config_home(const char *filename)
                return mkpathdup("%s/.config/git/%s", home, filename);
        return NULL;
 }
+
+GIT_PATH_FUNC(git_path_cherry_pick_head, "CHERRY_PICK_HEAD")
+GIT_PATH_FUNC(git_path_revert_head, "REVERT_HEAD")
+GIT_PATH_FUNC(git_path_squash_msg, "SQUASH_MSG")
+GIT_PATH_FUNC(git_path_merge_msg, "MERGE_MSG")
+GIT_PATH_FUNC(git_path_merge_rr, "MERGE_RR")
+GIT_PATH_FUNC(git_path_merge_mode, "MERGE_MODE")
+GIT_PATH_FUNC(git_path_merge_head, "MERGE_HEAD")
+GIT_PATH_FUNC(git_path_fetch_head, "FETCH_HEAD")
+GIT_PATH_FUNC(git_path_shallow, "shallow")