hooks: allow customizing where the hook directory is
[gitweb.git] / dir.c
diff --git a/dir.c b/dir.c
index b39a034ae5c5fa28bdef7602207f93e9f3d6def6..a4a9d9fae154849fe38b153af9b16e00ef401f60 100644 (file)
--- a/dir.c
+++ b/dir.c
@@ -12,6 +12,7 @@
 #include "refs.h"
 #include "wildmatch.h"
 #include "pathspec.h"
+#include "utf8.h"
 #include "varint.h"
 #include "ewah/ewok.h"
 
@@ -393,6 +394,47 @@ int match_pathspec(const struct pathspec *ps,
        return negative ? 0 : positive;
 }
 
+int report_path_error(const char *ps_matched,
+                     const struct pathspec *pathspec,
+                     const char *prefix)
+{
+       /*
+        * Make sure all pathspec matched; otherwise it is an error.
+        */
+       int num, errors = 0;
+       for (num = 0; num < pathspec->nr; num++) {
+               int other, found_dup;
+
+               if (ps_matched[num])
+                       continue;
+               /*
+                * The caller might have fed identical pathspec
+                * twice.  Do not barf on such a mistake.
+                * FIXME: parse_pathspec should have eliminated
+                * duplicate pathspec.
+                */
+               for (found_dup = other = 0;
+                    !found_dup && other < pathspec->nr;
+                    other++) {
+                       if (other == num || !ps_matched[other])
+                               continue;
+                       if (!strcmp(pathspec->items[other].original,
+                                   pathspec->items[num].original))
+                               /*
+                                * Ok, we have a match already.
+                                */
+                               found_dup = 1;
+               }
+               if (found_dup)
+                       continue;
+
+               error("pathspec '%s' did not match any file(s) known to git.",
+                     pathspec->items[num].original);
+               errors++;
+       }
+       return errors;
+}
+
 /*
  * Return the length of the "simple" part of a path match limiter.
  */
@@ -461,12 +503,7 @@ void add_exclude(const char *string, const char *base,
 
        parse_exclude_pattern(&string, &patternlen, &flags, &nowildcardlen);
        if (flags & EXC_FLAG_MUSTBEDIR) {
-               char *s;
-               x = xmalloc(sizeof(*x) + patternlen + 1);
-               s = (char *)(x+1);
-               memcpy(s, string, patternlen);
-               s[patternlen] = '\0';
-               x->pattern = s;
+               FLEXPTR_ALLOC_MEM(x, pattern, string, patternlen);
        } else {
                x = xmalloc(sizeof(*x));
                x->pattern = string;
@@ -522,9 +559,7 @@ void clear_exclude_list(struct exclude_list *el)
        free(el->excludes);
        free(el->filebuf);
 
-       el->nr = 0;
-       el->excludes = NULL;
-       el->filebuf = NULL;
+       memset(el, 0, sizeof(*el));
 }
 
 static void trim_trailing_spaces(char *buf)
@@ -585,10 +620,7 @@ static struct untracked_cache_dir *lookup_untracked(struct untracked_cache *uc,
        }
 
        uc->dir_created++;
-       d = xmalloc(sizeof(*d) + len + 1);
-       memset(d, 0, sizeof(*d));
-       memcpy(d->name, name, len);
-       d->name[len] = '\0';
+       FLEX_ALLOC_MEM(d, name, name, len);
 
        ALLOC_GROW(dir->dirs, dir->dirs_nr + 1, dir->dirs_alloc);
        memmove(dir->dirs + first + 1, dir->dirs + first,
@@ -657,7 +689,7 @@ static int add_excludes(const char *fname, const char *base, int baselen,
                        return 0;
                }
                if (buf[size-1] != '\n') {
-                       buf = xrealloc(buf, size+1);
+                       buf = xrealloc(buf, st_add(size, 1));
                        buf[size++] = '\n';
                }
        } else {
@@ -671,7 +703,7 @@ static int add_excludes(const char *fname, const char *base, int baselen,
                        close(fd);
                        return 0;
                }
-               buf = xmalloc(size+1);
+               buf = xmallocz(size);
                if (read_in_full(fd, buf, size) != size) {
                        free(buf);
                        close(fd);
@@ -682,7 +714,7 @@ static int add_excludes(const char *fname, const char *base, int baselen,
                if (sha1_stat) {
                        int pos;
                        if (sha1_stat->valid &&
-                           !match_stat_data(&sha1_stat->stat, &st))
+                           !match_stat_data_racy(&the_index, &sha1_stat->stat, &st))
                                ; /* no content change, ss->sha1 still good */
                        else if (check_index &&
                                 (pos = cache_name_pos(fname, strlen(fname))) >= 0 &&
@@ -698,7 +730,12 @@ static int add_excludes(const char *fname, const char *base, int baselen,
        }
 
        el->filebuf = buf;
+
+       if (skip_utf8_bom(&buf, size))
+               size -= buf - el->filebuf;
+
        entry = buf;
+
        for (i = 0; i < size; i++) {
                if (buf[i] == '\n') {
                        if (entry != buf + i && entry[0] != '#') {
@@ -854,6 +891,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
                                                       int *dtype,
                                                       struct exclude_list *el)
 {
+       struct exclude *exc = NULL; /* undecided */
        int i;
 
        if (!el->nr)
@@ -875,18 +913,22 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
                        if (match_basename(basename,
                                           pathlen - (basename - pathname),
                                           exclude, prefix, x->patternlen,
-                                          x->flags))
-                               return x;
+                                          x->flags)) {
+                               exc = x;
+                               break;
+                       }
                        continue;
                }
 
                assert(x->baselen == 0 || x->base[x->baselen - 1] == '/');
                if (match_pathname(pathname, pathlen,
                                   x->base, x->baselen ? x->baselen - 1 : 0,
-                                  exclude, prefix, x->patternlen, x->flags))
-                       return x;
+                                  exclude, prefix, x->patternlen, x->flags)) {
+                       exc = x;
+                       break;
+               }
        }
-       return NULL; /* undecided */
+       return exc;
 }
 
 /*
@@ -1031,10 +1073,9 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
                    (!untracked || !untracked->valid ||
                     /*
                      * .. and .gitignore does not exist before
-                     * (i.e. null exclude_sha1 and skip_worktree is
-                     * not set). Then we can skip loading .gitignore,
-                     * which would result in ENOENT anyway.
-                     * skip_worktree is taken care in read_directory()
+                     * (i.e. null exclude_sha1). Then we can skip
+                     * loading .gitignore, which would result in
+                     * ENOENT anyway.
                      */
                     !is_null_sha1(untracked->exclude_sha1))) {
                        /*
@@ -1118,10 +1159,8 @@ static struct dir_entry *dir_entry_new(const char *pathname, int len)
 {
        struct dir_entry *ent;
 
-       ent = xmalloc(sizeof(*ent) + len + 1);
+       FLEX_ALLOC_MEM(ent, name, pathname, len);
        ent->len = len;
-       memcpy(ent->name, pathname, len);
-       ent->name[len] = 0;
        return ent;
 }
 
@@ -1156,29 +1195,15 @@ enum exist_status {
  */
 static enum exist_status directory_exists_in_index_icase(const char *dirname, int len)
 {
-       const struct cache_entry *ce = cache_dir_exists(dirname, len);
-       unsigned char endchar;
+       struct cache_entry *ce;
 
-       if (!ce)
-               return index_nonexistent;
-       endchar = ce->name[len];
-
-       /*
-        * The cache_entry structure returned will contain this dirname
-        * and possibly additional path components.
-        */
-       if (endchar == '/')
+       if (cache_dir_exists(dirname, len))
                return index_directory;
 
-       /*
-        * If there are no additional path components, then this cache_entry
-        * represents a submodule.  Submodules, despite being directories,
-        * are stored in the cache without a closing slash.
-        */
-       if (!endchar && S_ISGITLINK(ce->ce_mode))
+       ce = cache_file_exists(dirname, len, ignore_case);
+       if (ce && S_ISGITLINK(ce->ce_mode))
                return index_gitdir;
 
-       /* This should never be hit, but it exists just in case. */
        return index_nonexistent;
 }
 
@@ -1251,7 +1276,7 @@ static enum exist_status directory_exists_in_index(const char *dirname, int len)
  */
 static enum path_treatment treat_directory(struct dir_struct *dir,
        struct untracked_cache_dir *untracked,
-       const char *dirname, int len, int exclude,
+       const char *dirname, int len, int baselen, int exclude,
        const struct path_simplify *simplify)
 {
        /* The "len-1" is to strip the final '/' */
@@ -1278,7 +1303,8 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
        if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
                return exclude ? path_excluded : path_untracked;
 
-       untracked = lookup_untracked(dir->untracked, untracked, dirname, len);
+       untracked = lookup_untracked(dir->untracked, untracked,
+                                    dirname + baselen, len - baselen);
        return read_directory_recursive(dir, dirname, len,
                                        untracked, 1, simplify);
 }
@@ -1398,6 +1424,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,
+                                         int baselen,
                                          const struct path_simplify *simplify,
                                          int dtype, struct dirent *de)
 {
@@ -1449,8 +1476,8 @@ static enum path_treatment treat_one_path(struct dir_struct *dir,
                return path_none;
        case DT_DIR:
                strbuf_addch(path, '/');
-               return treat_directory(dir, untracked, path->buf, path->len, exclude,
-                       simplify);
+               return treat_directory(dir, untracked, path->buf, path->len,
+                                      baselen, exclude, simplify);
        case DT_REG:
        case DT_LNK:
                return exclude ? path_excluded : path_untracked;
@@ -1471,8 +1498,7 @@ static enum path_treatment treat_path_fast(struct dir_struct *dir,
        }
        strbuf_addstr(path, cdir->ucd->name);
        /* treat_one_path() does this before it calls treat_directory() */
-       if (path->buf[path->len - 1] != '/')
-               strbuf_addch(path, '/');
+       strbuf_complete(path, '/');
        if (cdir->ucd->check_only)
                /*
                 * check_only is set as a result of treat_directory() getting
@@ -1511,7 +1537,7 @@ static enum path_treatment treat_path(struct dir_struct *dir,
                return path_none;
 
        dtype = DTYPE(de);
-       return treat_one_path(dir, untracked, path, simplify, dtype, de);
+       return treat_one_path(dir, untracked, path, baselen, simplify, dtype, de);
 }
 
 static void add_untracked(struct untracked_cache_dir *dir, const char *name)
@@ -1539,7 +1565,7 @@ static int valid_cached_dir(struct dir_struct *dir,
                return 0;
        }
        if (!untracked->valid ||
-           match_stat_data(&untracked->stat_data, &st)) {
+           match_stat_data_racy(&the_index, &untracked->stat_data, &st)) {
                if (untracked->valid)
                        invalidate_directory(dir->untracked, untracked);
                fill_stat_data(&untracked->stat_data, &st);
@@ -1781,7 +1807,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, NULL, &sb, simplify,
+               if (treat_one_path(dir, NULL, &sb, baselen, simplify,
                                   DT_DIR, NULL) == path_none)
                        break; /* do not recurse into it */
                if (len <= baselen) {
@@ -1794,14 +1820,83 @@ static int treat_leading_path(struct dir_struct *dir,
        return rc;
 }
 
+static const char *get_ident_string(void)
+{
+       static struct strbuf sb = STRBUF_INIT;
+       struct utsname uts;
+
+       if (sb.len)
+               return sb.buf;
+       if (uname(&uts) < 0)
+               die_errno(_("failed to get kernel name and information"));
+       strbuf_addf(&sb, "Location %s, system %s", get_git_work_tree(),
+                   uts.sysname);
+       return sb.buf;
+}
+
+static int ident_in_untracked(const struct untracked_cache *uc)
+{
+       /*
+        * Previous git versions may have saved many NUL separated
+        * strings in the "ident" field, but it is insane to manage
+        * many locations, so just take care of the first one.
+        */
+
+       return !strcmp(uc->ident.buf, get_ident_string());
+}
+
+static void set_untracked_ident(struct untracked_cache *uc)
+{
+       strbuf_reset(&uc->ident);
+       strbuf_addstr(&uc->ident, get_ident_string());
+
+       /*
+        * This strbuf used to contain a list of NUL separated
+        * strings, so save NUL too for backward compatibility.
+        */
+       strbuf_addch(&uc->ident, 0);
+}
+
+static void new_untracked_cache(struct index_state *istate)
+{
+       struct untracked_cache *uc = xcalloc(1, sizeof(*uc));
+       strbuf_init(&uc->ident, 100);
+       uc->exclude_per_dir = ".gitignore";
+       /* should be the same flags used by git-status */
+       uc->dir_flags = DIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES;
+       set_untracked_ident(uc);
+       istate->untracked = uc;
+       istate->cache_changed |= UNTRACKED_CHANGED;
+}
+
+void add_untracked_cache(struct index_state *istate)
+{
+       if (!istate->untracked) {
+               new_untracked_cache(istate);
+       } else {
+               if (!ident_in_untracked(istate->untracked)) {
+                       free_untracked_cache(istate->untracked);
+                       new_untracked_cache(istate);
+               }
+       }
+}
+
+void remove_untracked_cache(struct index_state *istate)
+{
+       if (istate->untracked) {
+               free_untracked_cache(istate->untracked);
+               istate->untracked = NULL;
+               istate->cache_changed |= UNTRACKED_CHANGED;
+       }
+}
+
 static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *dir,
                                                      int base_len,
                                                      const struct pathspec *pathspec)
 {
        struct untracked_cache_dir *root;
-       int i;
 
-       if (!dir->untracked)
+       if (!dir->untracked || getenv("GIT_DISABLE_UNTRACKED_CACHE"))
                return NULL;
 
        /*
@@ -1851,14 +1946,10 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d
        if (dir->exclude_list_group[EXC_CMDL].nr)
                return NULL;
 
-       /*
-        * An optimization in prep_exclude() does not play well with
-        * CE_SKIP_WORKTREE. It's a rare case anyway, if a single
-        * entry has that bit set, disable the whole untracked cache.
-        */
-       for (i = 0; i < active_nr; i++)
-               if (ce_skip_worktree(active_cache[i]))
-                       return NULL;
+       if (!ident_in_untracked(dir->untracked)) {
+               warning(_("Untracked cache is disabled on this system or location."));
+               return NULL;
+       }
 
        if (!dir->untracked->root) {
                const int len = sizeof(*dir->untracked->root);
@@ -1923,6 +2014,27 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
        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);
+       if (dir->untracked) {
+               static struct trace_key trace_untracked_stats = TRACE_KEY_INIT(UNTRACKED_STATS);
+               trace_printf_key(&trace_untracked_stats,
+                                "node creation: %u\n"
+                                "gitignore invalidation: %u\n"
+                                "directory invalidation: %u\n"
+                                "opendir: %u\n",
+                                dir->untracked->dir_created,
+                                dir->untracked->gitignore_invalidated,
+                                dir->untracked->dir_invalidated,
+                                dir->untracked->dir_opened);
+               if (dir->untracked == the_index.untracked &&
+                   (dir->untracked->dir_opened ||
+                    dir->untracked->gitignore_invalidated ||
+                    dir->untracked->dir_invalidated))
+                       the_index.cache_changed |= UNTRACKED_CHANGED;
+               if (dir->untracked != the_index.untracked) {
+                       free(dir->untracked);
+                       dir->untracked = NULL;
+               }
+       }
        return dir->nr;
 }
 
@@ -1932,6 +2044,15 @@ int file_exists(const char *f)
        return lstat(f, &sb) == 0;
 }
 
+static int cmp_icase(char a, char b)
+{
+       if (a == b)
+               return 0;
+       if (ignore_case)
+               return toupper(a) - toupper(b);
+       return a - b;
+}
+
 /*
  * Given two normalized paths (a trailing slash is ok), if subdir is
  * outside dir, return -1.  Otherwise return the offset in subdir that
@@ -1943,7 +2064,7 @@ int dir_inside_of(const char *subdir, const char *dir)
 
        assert(dir && subdir && *dir && *subdir);
 
-       while (*dir && *subdir && *dir == *subdir) {
+       while (*dir && *subdir && !cmp_icase(*dir, *subdir)) {
                dir++;
                subdir++;
                offset++;
@@ -2028,8 +2149,7 @@ static int remove_dir_recurse(struct strbuf *path, int flag, int *kept_up)
                else
                        return -1;
        }
-       if (path->buf[original_len - 1] != '/')
-               strbuf_addch(path, '/');
+       strbuf_complete(path, '/');
 
        len = path->len;
        while ((e = readdir(dir)) != NULL) {
@@ -2078,23 +2198,26 @@ int remove_dir_recursively(struct strbuf *path, int flag)
        return remove_dir_recurse(path, flag, NULL);
 }
 
+static GIT_PATH_FUNC(git_path_info_exclude, "info/exclude")
+
 void setup_standard_excludes(struct dir_struct *dir)
 {
        const char *path;
-       char *xdg_path;
 
        dir->exclude_per_dir = ".gitignore";
-       path = git_path("info/exclude");
-       if (!excludes_file) {
-               home_config_paths(NULL, &xdg_path, "ignore");
-               excludes_file = xdg_path;
-       }
-       if (!access_or_warn(path, R_OK, 0))
-               add_excludes_from_file_1(dir, path,
-                                        dir->untracked ? &dir->ss_info_exclude : NULL);
+
+       /* core.excludefile defaulting to $XDG_HOME/git/ignore */
+       if (!excludes_file)
+               excludes_file = xdg_config_home("ignore");
        if (excludes_file && !access_or_warn(excludes_file, R_OK, 0))
                add_excludes_from_file_1(dir, excludes_file,
                                         dir->untracked ? &dir->ss_excludes_file : NULL);
+
+       /* per repository user preference */
+       path = git_path_info_exclude();
+       if (!access_or_warn(path, R_OK, 0))
+               add_excludes_from_file_1(dir, path,
+                                        dir->untracked ? &dir->ss_info_exclude : NULL);
 }
 
 int remove_path(const char *name)
@@ -2237,16 +2360,20 @@ void write_untracked_extension(struct strbuf *out, struct untracked_cache *untra
        struct ondisk_untracked_cache *ouc;
        struct write_data wd;
        unsigned char varbuf[16];
-       int len = 0, varint_len;
-       if (untracked->exclude_per_dir)
-               len = strlen(untracked->exclude_per_dir);
-       ouc = xmalloc(sizeof(*ouc) + len + 1);
+       int varint_len;
+       size_t len = strlen(untracked->exclude_per_dir);
+
+       FLEX_ALLOC_MEM(ouc, exclude_per_dir, untracked->exclude_per_dir, len);
        stat_data_to_disk(&ouc->info_exclude_stat, &untracked->ss_info_exclude.stat);
        stat_data_to_disk(&ouc->excludes_file_stat, &untracked->ss_excludes_file.stat);
        hashcpy(ouc->info_exclude_sha1, untracked->ss_info_exclude.sha1);
        hashcpy(ouc->excludes_file_sha1, untracked->ss_excludes_file.sha1);
        ouc->dir_flags = htonl(untracked->dir_flags);
-       memcpy(ouc->exclude_per_dir, untracked->exclude_per_dir, len + 1);
+
+       varint_len = encode_varint(untracked->ident.len, varbuf);
+       strbuf_add(out, varbuf, varint_len);
+       strbuf_add(out, untracked->ident.buf, untracked->ident.len);
+
        strbuf_add(out, ouc, ouc_size(len));
        free(ouc);
        ouc = NULL;
@@ -2346,21 +2473,21 @@ static int read_one_dir(struct untracked_cache_dir **untracked_,
        ud.untracked_alloc = value;
        ud.untracked_nr    = value;
        if (ud.untracked_nr)
-               ud.untracked = xmalloc(sizeof(*ud.untracked) * ud.untracked_nr);
+               ALLOC_ARRAY(ud.untracked, ud.untracked_nr);
        data = next;
 
        next = data;
        ud.dirs_alloc = ud.dirs_nr = decode_varint(&next);
        if (next > end)
                return -1;
-       ud.dirs = xmalloc(sizeof(*ud.dirs) * ud.dirs_nr);
+       ALLOC_ARRAY(ud.dirs, ud.dirs_nr);
        data = next;
 
        len = strlen((const char *)data);
        next = data + len + 1;
        if (next > rd->end)
                return -1;
-       *untracked_ = untracked = xmalloc(sizeof(*untracked) + len);
+       *untracked_ = untracked = xmalloc(st_add(sizeof(*untracked), len));
        memcpy(untracked, &ud, sizeof(ud));
        memcpy(untracked->name, data, len + 1);
        data = next;
@@ -2432,17 +2559,26 @@ struct untracked_cache *read_untracked_extension(const void *data, unsigned long
        struct untracked_cache *uc;
        struct read_data rd;
        const unsigned char *next = data, *end = (const unsigned char *)data + sz;
-       int len;
+       const char *ident;
+       int ident_len, len;
 
        if (sz <= 1 || end[-1] != '\0')
                return NULL;
        end--;
 
+       ident_len = decode_varint(&next);
+       if (next + ident_len > end)
+               return NULL;
+       ident = (const char *)next;
+       next += ident_len;
+
        ouc = (const struct ondisk_untracked_cache *)next;
        if (next + ouc_size(0) > end)
                return NULL;
 
        uc = xcalloc(1, sizeof(*uc));
+       strbuf_init(&uc->ident, ident_len);
+       strbuf_add(&uc->ident, ident, ident_len);
        load_sha1_stat(&uc->ss_info_exclude, &ouc->info_exclude_stat,
                       ouc->info_exclude_sha1);
        load_sha1_stat(&uc->ss_excludes_file, &ouc->excludes_file_stat,
@@ -2464,7 +2600,7 @@ struct untracked_cache *read_untracked_extension(const void *data, unsigned long
        rd.data       = next;
        rd.end        = end;
        rd.index      = 0;
-       rd.ucd        = xmalloc(sizeof(*rd.ucd) * len);
+       ALLOC_ARRAY(rd.ucd, len);
 
        if (read_one_dir(&uc->root, &rd) || rd.index != len)
                goto done;
@@ -2502,3 +2638,78 @@ struct untracked_cache *read_untracked_extension(const void *data, unsigned long
        }
        return uc;
 }
+
+static void invalidate_one_directory(struct untracked_cache *uc,
+                                    struct untracked_cache_dir *ucd)
+{
+       uc->dir_invalidated++;
+       ucd->valid = 0;
+       ucd->untracked_nr = 0;
+}
+
+/*
+ * Normally when an entry is added or removed from a directory,
+ * invalidating that directory is enough. No need to touch its
+ * ancestors. When a directory is shown as "foo/bar/" in git-status
+ * however, deleting or adding an entry may have cascading effect.
+ *
+ * Say the "foo/bar/file" has become untracked, we need to tell the
+ * untracked_cache_dir of "foo" that "bar/" is not an untracked
+ * directory any more (because "bar" is managed by foo as an untracked
+ * "file").
+ *
+ * Similarly, if "foo/bar/file" moves from untracked to tracked and it
+ * was the last untracked entry in the entire "foo", we should show
+ * "foo/" instead. Which means we have to invalidate past "bar" up to
+ * "foo".
+ *
+ * This function traverses all directories from root to leaf. If there
+ * is a chance of one of the above cases happening, we invalidate back
+ * to root. Otherwise we just invalidate the leaf. There may be a more
+ * sophisticated way than checking for SHOW_OTHER_DIRECTORIES to
+ * detect these cases and avoid unnecessary invalidation, for example,
+ * checking for the untracked entry named "bar/" in "foo", but for now
+ * stick to something safe and simple.
+ */
+static int invalidate_one_component(struct untracked_cache *uc,
+                                   struct untracked_cache_dir *dir,
+                                   const char *path, int len)
+{
+       const char *rest = strchr(path, '/');
+
+       if (rest) {
+               int component_len = rest - path;
+               struct untracked_cache_dir *d =
+                       lookup_untracked(uc, dir, path, component_len);
+               int ret =
+                       invalidate_one_component(uc, d, rest + 1,
+                                                len - (component_len + 1));
+               if (ret)
+                       invalidate_one_directory(uc, dir);
+               return ret;
+       }
+
+       invalidate_one_directory(uc, dir);
+       return uc->dir_flags & DIR_SHOW_OTHER_DIRECTORIES;
+}
+
+void untracked_cache_invalidate_path(struct index_state *istate,
+                                    const char *path)
+{
+       if (!istate->untracked || !istate->untracked->root)
+               return;
+       invalidate_one_component(istate->untracked, istate->untracked->root,
+                                path, strlen(path));
+}
+
+void untracked_cache_remove_from_index(struct index_state *istate,
+                                      const char *path)
+{
+       untracked_cache_invalidate_path(istate, path);
+}
+
+void untracked_cache_add_to_index(struct index_state *istate,
+                                 const char *path)
+{
+       untracked_cache_invalidate_path(istate, path);
+}