untracked cache: initial untracked cache validation
[gitweb.git] / dir.c
diff --git a/dir.c b/dir.c
index f23bd7bebcfcb3b108c507592d2f4a82339961fe..a0654885b6d04a473c9296277746d5cf9e107e8a 100644 (file)
--- a/dir.c
+++ b/dir.c
@@ -32,7 +32,7 @@ enum path_treatment {
 };
 
 static enum path_treatment read_directory_recursive(struct dir_struct *dir,
-       const char *path, int len,
+       const char *path, int len, struct untracked_cache_dir *untracked,
        int check_only, const struct path_simplify *simplify);
 static int get_dtype(struct dirent *de, const char *path, int len);
 
@@ -534,6 +534,70 @@ static void trim_trailing_spaces(char *buf)
                *last_space = '\0';
 }
 
+/*
+ * Given a subdirectory name and "dir" of the current directory,
+ * search the subdir in "dir" and return it, or create a new one if it
+ * does not exist in "dir".
+ *
+ * If "name" has the trailing slash, it'll be excluded in the search.
+ */
+static struct untracked_cache_dir *lookup_untracked(struct untracked_cache *uc,
+                                                   struct untracked_cache_dir *dir,
+                                                   const char *name, int len)
+{
+       int first, last;
+       struct untracked_cache_dir *d;
+       if (!dir)
+               return NULL;
+       if (len && name[len - 1] == '/')
+               len--;
+       first = 0;
+       last = dir->dirs_nr;
+       while (last > first) {
+               int cmp, next = (last + first) >> 1;
+               d = dir->dirs[next];
+               cmp = strncmp(name, d->name, len);
+               if (!cmp && strlen(d->name) > len)
+                       cmp = -1;
+               if (!cmp)
+                       return d;
+               if (cmp < 0) {
+                       last = next;
+                       continue;
+               }
+               first = next+1;
+       }
+
+       uc->dir_created++;
+       d = xmalloc(sizeof(*d) + len + 1);
+       memset(d, 0, sizeof(*d));
+       memcpy(d->name, name, len);
+       d->name[len] = '\0';
+
+       ALLOC_GROW(dir->dirs, dir->dirs_nr + 1, dir->dirs_alloc);
+       memmove(dir->dirs + first + 1, dir->dirs + first,
+               (dir->dirs_nr - first) * sizeof(*dir->dirs));
+       dir->dirs_nr++;
+       dir->dirs[first] = d;
+       return d;
+}
+
+static void do_invalidate_gitignore(struct untracked_cache_dir *dir)
+{
+       int i;
+       dir->valid = 0;
+       dir->untracked_nr = 0;
+       for (i = 0; i < dir->dirs_nr; i++)
+               do_invalidate_gitignore(dir->dirs[i]);
+}
+
+static void invalidate_gitignore(struct untracked_cache *uc,
+                                struct untracked_cache_dir *dir)
+{
+       uc->gitignore_invalidated++;
+       do_invalidate_gitignore(dir);
+}
+
 /*
  * Given a file with name "fname", read it (either from disk, or from
  * the index if "check_index" is non-zero), parse it and store the
@@ -646,14 +710,28 @@ struct exclude_list *add_exclude_list(struct dir_struct *dir,
 /*
  * Used to set up core.excludesfile and .git/info/exclude lists.
  */
-void add_excludes_from_file(struct dir_struct *dir, const char *fname)
+static void add_excludes_from_file_1(struct dir_struct *dir, const char *fname,
+                                    struct sha1_stat *sha1_stat)
 {
        struct exclude_list *el;
+       /*
+        * catch setup_standard_excludes() that's called before
+        * dir->untracked is assigned. That function behaves
+        * differently when dir->untracked is non-NULL.
+        */
+       if (!dir->untracked)
+               dir->unmanaged_exclude_files++;
        el = add_exclude_list(dir, EXC_FILE, fname);
-       if (add_excludes_from_file_to_list(fname, "", 0, el, 0) < 0)
+       if (add_excludes(fname, "", 0, el, 0, sha1_stat) < 0)
                die("cannot use %s as an exclude file", fname);
 }
 
+void add_excludes_from_file(struct dir_struct *dir, const char *fname)
+{
+       dir->unmanaged_exclude_files++; /* see validate_untracked_cache() */
+       add_excludes_from_file_1(dir, fname, NULL);
+}
+
 int match_basename(const char *basename, int basenamelen,
                   const char *pattern, int prefix, int patternlen,
                   int flags)
@@ -828,6 +906,7 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
        struct exclude_list_group *group;
        struct exclude_list *el;
        struct exclude_stack *stk = NULL;
+       struct untracked_cache_dir *untracked;
        int current;
 
        group = &dir->exclude_list_group[EXC_DIRS];
@@ -865,8 +944,14 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
        /* Read from the parent directories and push them down. */
        current = stk ? stk->baselen : -1;
        strbuf_setlen(&dir->basebuf, current < 0 ? 0 : current);
+       if (dir->untracked)
+               untracked = stk ? stk->ucd : dir->untracked->root;
+       else
+               untracked = NULL;
+
        while (current < baselen) {
                const char *cp;
+               struct sha1_stat sha1_stat;
 
                stk = xcalloc(1, sizeof(*stk));
                if (current < 0) {
@@ -877,10 +962,15 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
                        if (!cp)
                                die("oops in prep_exclude");
                        cp++;
+                       untracked =
+                               lookup_untracked(dir->untracked, untracked,
+                                                base + current,
+                                                cp - base - current);
                }
                stk->prev = dir->exclude_stack;
                stk->baselen = cp - base;
                stk->exclude_ix = group->nr;
+               stk->ucd = untracked;
                el = add_exclude_list(dir, EXC_DIRS, NULL);
                strbuf_add(&dir->basebuf, base + current, stk->baselen - current);
                assert(stk->baselen == dir->basebuf.len);
@@ -903,6 +993,8 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
                }
 
                /* Try to read per-directory file */
+               hashclr(sha1_stat.sha1);
+               sha1_stat.valid = 0;
                if (dir->exclude_per_dir) {
                        /*
                         * dir->basebuf gets reused by the traversal, but we
@@ -916,8 +1008,11 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
                        strbuf_addbuf(&sb, &dir->basebuf);
                        strbuf_addstr(&sb, dir->exclude_per_dir);
                        el->src = strbuf_detach(&sb, NULL);
-                       add_excludes_from_file_to_list(el->src, el->src,
-                                                      stk->baselen, el, 1);
+                       add_excludes(el->src, el->src, stk->baselen, el, 1,
+                                    untracked ? &sha1_stat : NULL);
+               }
+               if (untracked) {
+                       hashcpy(untracked->exclude_sha1, sha1_stat.sha1);
                }
                dir->exclude_stack = stk;
                current = stk->baselen;
@@ -1098,6 +1193,7 @@ static enum exist_status directory_exists_in_index(const char *dirname, int len)
  *  (c) otherwise, we recurse into it.
  */
 static enum path_treatment treat_directory(struct dir_struct *dir,
+       struct untracked_cache_dir *untracked,
        const char *dirname, int len, int exclude,
        const struct path_simplify *simplify)
 {
@@ -1125,7 +1221,9 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
        if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
                return exclude ? path_excluded : path_untracked;
 
-       return read_directory_recursive(dir, dirname, len, 1, simplify);
+       untracked = lookup_untracked(dir->untracked, untracked, dirname, len);
+       return read_directory_recursive(dir, dirname, len,
+                                       untracked, 1, simplify);
 }
 
 /*
@@ -1241,6 +1339,7 @@ static int get_dtype(struct dirent *de, const char *path, int len)
 }
 
 static enum path_treatment treat_one_path(struct dir_struct *dir,
+                                         struct untracked_cache_dir *untracked,
                                          struct strbuf *path,
                                          const struct path_simplify *simplify,
                                          int dtype, struct dirent *de)
@@ -1293,7 +1392,7 @@ static enum path_treatment treat_one_path(struct dir_struct *dir,
                return path_none;
        case DT_DIR:
                strbuf_addch(path, '/');
-               return treat_directory(dir, path->buf, path->len, exclude,
+               return treat_directory(dir, untracked, path->buf, path->len, exclude,
                        simplify);
        case DT_REG:
        case DT_LNK:
@@ -1302,6 +1401,7 @@ static enum path_treatment treat_one_path(struct dir_struct *dir,
 }
 
 static enum path_treatment treat_path(struct dir_struct *dir,
+                                     struct untracked_cache_dir *untracked,
                                      struct dirent *de,
                                      struct strbuf *path,
                                      int baselen,
@@ -1317,7 +1417,16 @@ static enum path_treatment treat_path(struct dir_struct *dir,
                return path_none;
 
        dtype = DTYPE(de);
-       return treat_one_path(dir, path, simplify, dtype, de);
+       return treat_one_path(dir, untracked, path, simplify, dtype, de);
+}
+
+static void add_untracked(struct untracked_cache_dir *dir, const char *name)
+{
+       if (!dir)
+               return;
+       ALLOC_GROW(dir->untracked, dir->untracked_nr + 1,
+                  dir->untracked_alloc);
+       dir->untracked[dir->untracked_nr++] = xstrdup(name);
 }
 
 /*
@@ -1333,7 +1442,7 @@ static enum path_treatment treat_path(struct dir_struct *dir,
  */
 static enum path_treatment read_directory_recursive(struct dir_struct *dir,
                                    const char *base, int baselen,
-                                   int check_only,
+                                   struct untracked_cache_dir *untracked, int check_only,
                                    const struct path_simplify *simplify)
 {
        DIR *fdir;
@@ -1347,24 +1456,36 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
        if (!fdir)
                goto out;
 
+       if (untracked)
+               untracked->check_only = !!check_only;
+
        while ((de = readdir(fdir)) != NULL) {
                /* check how the file or directory should be treated */
-               state = treat_path(dir, de, &path, baselen, simplify);
+               state = treat_path(dir, untracked, de, &path, baselen, simplify);
+
                if (state > dir_state)
                        dir_state = state;
 
                /* recurse into subdir if instructed by treat_path */
                if (state == path_recurse) {
-                       subdir_state = read_directory_recursive(dir, path.buf,
-                               path.len, check_only, simplify);
+                       struct untracked_cache_dir *ud;
+                       ud = lookup_untracked(dir->untracked, untracked,
+                                             path.buf + baselen,
+                                             path.len - baselen);
+                       subdir_state =
+                               read_directory_recursive(dir, path.buf, path.len,
+                                                        ud, check_only, simplify);
                        if (subdir_state > dir_state)
                                dir_state = subdir_state;
                }
 
                if (check_only) {
                        /* abort early if maximum state has been reached */
-                       if (dir_state == path_untracked)
+                       if (dir_state == path_untracked) {
+                               if (untracked)
+                                       add_untracked(untracked, path.buf + baselen);
                                break;
+                       }
                        /* skip the dir_add_* part */
                        continue;
                }
@@ -1382,8 +1503,11 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
                        break;
 
                case path_untracked:
-                       if (!(dir->flags & DIR_SHOW_IGNORED))
-                               dir_add_name(dir, path.buf, path.len);
+                       if (dir->flags & DIR_SHOW_IGNORED)
+                               break;
+                       dir_add_name(dir, path.buf, path.len);
+                       if (untracked)
+                               add_untracked(untracked, path.buf + baselen);
                        break;
 
                default:
@@ -1460,7 +1584,7 @@ static int treat_leading_path(struct dir_struct *dir,
                        break;
                if (simplify_away(sb.buf, sb.len, simplify))
                        break;
-               if (treat_one_path(dir, &sb, simplify,
+               if (treat_one_path(dir, NULL, &sb, simplify,
                                   DT_DIR, NULL) == path_none)
                        break; /* do not recurse into it */
                if (len <= baselen) {
@@ -1473,9 +1597,87 @@ static int treat_leading_path(struct dir_struct *dir,
        return rc;
 }
 
+static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *dir,
+                                                     int base_len,
+                                                     const struct pathspec *pathspec)
+{
+       struct untracked_cache_dir *root;
+
+       if (!dir->untracked)
+               return NULL;
+
+       /*
+        * We only support $GIT_DIR/info/exclude and core.excludesfile
+        * as the global ignore rule files. Any other additions
+        * (e.g. from command line) invalidate the cache. This
+        * condition also catches running setup_standard_excludes()
+        * before setting dir->untracked!
+        */
+       if (dir->unmanaged_exclude_files)
+               return NULL;
+
+       /*
+        * Optimize for the main use case only: whole-tree git
+        * status. More work involved in treat_leading_path() if we
+        * use cache on just a subset of the worktree. pathspec
+        * support could make the matter even worse.
+        */
+       if (base_len || (pathspec && pathspec->nr))
+               return NULL;
+
+       /* Different set of flags may produce different results */
+       if (dir->flags != dir->untracked->dir_flags ||
+           /*
+            * See treat_directory(), case index_nonexistent. Without
+            * this flag, we may need to also cache .git file content
+            * for the resolve_gitlink_ref() call, which we don't.
+            */
+           !(dir->flags & DIR_SHOW_OTHER_DIRECTORIES) ||
+           /* We don't support collecting ignore files */
+           (dir->flags & (DIR_SHOW_IGNORED | DIR_SHOW_IGNORED_TOO |
+                          DIR_COLLECT_IGNORED)))
+               return NULL;
+
+       /*
+        * If we use .gitignore in the cache and now you change it to
+        * .gitexclude, everything will go wrong.
+        */
+       if (dir->exclude_per_dir != dir->untracked->exclude_per_dir &&
+           strcmp(dir->exclude_per_dir, dir->untracked->exclude_per_dir))
+               return NULL;
+
+       /*
+        * EXC_CMDL is not considered in the cache. If people set it,
+        * skip the cache.
+        */
+       if (dir->exclude_list_group[EXC_CMDL].nr)
+               return NULL;
+
+       if (!dir->untracked->root) {
+               const int len = sizeof(*dir->untracked->root);
+               dir->untracked->root = xmalloc(len);
+               memset(dir->untracked->root, 0, len);
+       }
+
+       /* Validate $GIT_DIR/info/exclude and core.excludesfile */
+       root = dir->untracked->root;
+       if (hashcmp(dir->ss_info_exclude.sha1,
+                   dir->untracked->ss_info_exclude.sha1)) {
+               invalidate_gitignore(dir->untracked, root);
+               dir->untracked->ss_info_exclude = dir->ss_info_exclude;
+       }
+       if (hashcmp(dir->ss_excludes_file.sha1,
+                   dir->untracked->ss_excludes_file.sha1)) {
+               invalidate_gitignore(dir->untracked, root);
+               dir->untracked->ss_excludes_file = dir->ss_excludes_file;
+       }
+       return root;
+}
+
 int read_directory(struct dir_struct *dir, const char *path, int len, const struct pathspec *pathspec)
 {
        struct path_simplify *simplify;
+       struct untracked_cache_dir *untracked;
 
        /*
         * Check out create_simplify()
@@ -1499,8 +1701,15 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
         * create_simplify().
         */
        simplify = create_simplify(pathspec ? pathspec->_raw : NULL);
+       untracked = validate_untracked_cache(dir, len, pathspec);
+       if (!untracked)
+               /*
+                * make sure untracked cache code path is disabled,
+                * e.g. prep_exclude()
+                */
+               dir->untracked = NULL;
        if (!len || treat_leading_path(dir, path, len, simplify))
-               read_directory_recursive(dir, path, len, 0, simplify);
+               read_directory_recursive(dir, path, len, untracked, 0, simplify);
        free_simplify(simplify);
        qsort(dir->entries, dir->nr, sizeof(struct dir_entry *), cmp_name);
        qsort(dir->ignored, dir->ignored_nr, sizeof(struct dir_entry *), cmp_name);
@@ -1671,9 +1880,11 @@ void setup_standard_excludes(struct dir_struct *dir)
                excludes_file = xdg_path;
        }
        if (!access_or_warn(path, R_OK, 0))
-               add_excludes_from_file(dir, path);
+               add_excludes_from_file_1(dir, path,
+                                        dir->untracked ? &dir->ss_info_exclude : NULL);
        if (excludes_file && !access_or_warn(excludes_file, R_OK, 0))
-               add_excludes_from_file(dir, excludes_file);
+               add_excludes_from_file_1(dir, excludes_file,
+                                        dir->untracked ? &dir->ss_excludes_file : NULL);
 }
 
 int remove_path(const char *name)