Merge branch 'nd/untracked-cache'
authorJunio C Hamano <gitster@pobox.com>
Tue, 26 May 2015 20:24:45 +0000 (13:24 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 26 May 2015 20:24:46 +0000 (13:24 -0700)
Teach the index to optionally remember already seen untracked files
to speed up "git status" in a working tree with tons of cruft.

* nd/untracked-cache: (24 commits)
git-status.txt: advertisement for untracked cache
untracked cache: guard and disable on system changes
mingw32: add uname()
t7063: tests for untracked cache
update-index: test the system before enabling untracked cache
update-index: manually enable or disable untracked cache
status: enable untracked cache
untracked-cache: temporarily disable with $GIT_DISABLE_UNTRACKED_CACHE
untracked cache: mark index dirty if untracked cache is updated
untracked cache: print stats with $GIT_TRACE_UNTRACKED_STATS
untracked cache: avoid racy timestamps
read-cache.c: split racy stat test to a separate function
untracked cache: invalidate at index addition or removal
untracked cache: load from UNTR index extension
untracked cache: save to an index extension
ewah: add convenient wrapper ewah_serialize_strbuf()
untracked cache: don't open non-existent .gitignore
untracked cache: mark what dirs should be recursed/saved
untracked cache: record/validate dir mtime and reuse cached output
untracked cache: make a wrapper around {open,read,close}dir()
...

1  2 
Documentation/git-status.txt
Makefile
builtin/commit.c
builtin/update-index.c
cache.h
compat/mingw.h
dir.c
dir.h
git-compat-util.h
read-cache.c
wt-status.c
index 5221f950ce06cda1a764424ec217760d044a5ac3,56573bd86e52c30b83e935819ee02e9091f915ab..335f3123353482cfd708420dac3b766f181e89ff
@@@ -41,14 -41,6 +41,14 @@@ OPTION
  --long::
        Give the output in the long-format. This is the default.
  
 +-v::
 +--verbose::
 +      In addition to the names of files that have been changed, also
 +      show the textual changes that are staged to be committed
 +      (i.e., like the output of `git diff --cached`). If `-v` is specified
 +      twice, then also show the changes in the working tree that
 +      have not yet been staged (i.e., like the output of `git diff`).
 +
  -u[<mode>]::
  --untracked-files[=<mode>]::
        Show untracked files.
@@@ -66,7 -58,10 +66,10 @@@ When `-u` option is not used, untracke
  shown (i.e. the same as specifying `normal`), to help you avoid
  forgetting to add newly created files.  Because it takes extra work
  to find untracked files in the filesystem, this mode may take some
- time in a large working tree.  You can use `no` to have `git status`
+ time in a large working tree.
+ Consider enabling untracked cache and split index if supported (see
+ `git update-index --untracked-cache` and `git update-index
+ --split-index`), Otherwise you can use `no` to have `git status`
  return more quickly without showing untracked files.
  +
  The default can be changed using the status.showUntrackedFiles
@@@ -85,7 -80,7 +88,7 @@@ configuration variable documented in li
        only changes to the commits stored in the superproject are shown (this was
        the behavior before 1.7.0). Using "all" hides all changes to submodules
        (and suppresses the output of submodule summaries when the config option
 -      `status.submodulesummary` is set).
 +      `status.submoduleSummary` is set).
  
  --ignored::
        Show ignored files as well.
@@@ -215,7 -210,7 +218,7 @@@ If the config variable `status.relative
  paths shown are relative to the repository root, not to the current
  directory.
  
 -If `status.submodulesummary` is set to a non zero number or true (identical
 +If `status.submoduleSummary` is set to a non zero number or true (identical
  to -1 or an unlimited number), the submodule summary will be enabled for
  the long format and a summary of commits for modified submodules will be
  shown (see --summary-limit option of linkgit:git-submodule[1]). Please note
diff --combined Makefile
index 25a453bf2bfd1e17c3fee1ce6697f268804401fc,e63538a2e7ab2732d7f8c7605226ee61e9e25007..323c401e966eb8a69057e563fc43b4f87e7d897f
+++ b/Makefile
@@@ -357,10 -357,6 +357,10 @@@ all:
  # and define it to "no" if you need to remove the parentheses () around the
  # constant.  The default is "auto", which means to use parentheses if your
  # compiler is detected to support it.
 +#
 +# Define HAVE_BSD_SYSCTL if your platform has a BSD-compatible sysctl function.
 +#
 +# Define HAVE_GETDELIM if your system has the getdelim() function.
  
  GIT-VERSION-FILE: FORCE
        @$(SHELL_PATH) ./GIT-VERSION-GEN
@@@ -574,6 -570,7 +574,7 @@@ TEST_PROGRAMS_NEED_X += test-dat
  TEST_PROGRAMS_NEED_X += test-delta
  TEST_PROGRAMS_NEED_X += test-dump-cache-tree
  TEST_PROGRAMS_NEED_X += test-dump-split-index
+ TEST_PROGRAMS_NEED_X += test-dump-untracked-cache
  TEST_PROGRAMS_NEED_X += test-genrandom
  TEST_PROGRAMS_NEED_X += test-hashmap
  TEST_PROGRAMS_NEED_X += test-index-version
@@@ -1435,14 -1432,6 +1436,14 @@@ ifdef HAVE_CLOCK_MONOTONI
        BASIC_CFLAGS += -DHAVE_CLOCK_MONOTONIC
  endif
  
 +ifdef HAVE_BSD_SYSCTL
 +      BASIC_CFLAGS += -DHAVE_BSD_SYSCTL
 +endif
 +
 +ifdef HAVE_GETDELIM
 +      BASIC_CFLAGS += -DHAVE_GETDELIM
 +endif
 +
  ifeq ($(TCLTK_PATH),)
  NO_TCLTK = NoThanks
  endif
@@@ -2108,7 -2097,6 +2109,7 @@@ GIT-BUILD-OPTIONS: FORC
        @echo PYTHON_PATH=\''$(subst ','\'',$(PYTHON_PATH_SQ))'\' >>$@
        @echo TAR=\''$(subst ','\'',$(subst ','\'',$(TAR)))'\' >>$@
        @echo NO_CURL=\''$(subst ','\'',$(subst ','\'',$(NO_CURL)))'\' >>$@
 +      @echo NO_EXPAT=\''$(subst ','\'',$(subst ','\'',$(NO_EXPAT)))'\' >>$@
        @echo USE_LIBPCRE=\''$(subst ','\'',$(subst ','\'',$(USE_LIBPCRE)))'\' >>$@
        @echo NO_PERL=\''$(subst ','\'',$(subst ','\'',$(NO_PERL)))'\' >>$@
        @echo NO_PYTHON=\''$(subst ','\'',$(subst ','\'',$(NO_PYTHON)))'\' >>$@
diff --combined builtin/commit.c
index d6515a2a50e78e9376f1b55fa6fe79b1e867f25c,fe380a9b92cd7b4a301bfe0dba514319feda5832..254477fd1d4e8b96f50eb42dc11ec1253c7467f8
@@@ -170,7 -170,7 +170,7 @@@ static void determine_whence(struct wt_
                whence = FROM_MERGE;
        else if (file_exists(git_path("CHERRY_PICK_HEAD"))) {
                whence = FROM_CHERRY_PICK;
 -              if (file_exists(git_path("sequencer")))
 +              if (file_exists(git_path(SEQ_DIR)))
                        sequencer_in_use = 1;
        }
        else
@@@ -229,7 -229,7 +229,7 @@@ static int commit_index_files(void
  static int list_paths(struct string_list *list, const char *with_tree,
                      const char *prefix, const struct pathspec *pattern)
  {
 -      int i;
 +      int i, ret;
        char *m;
  
        if (!pattern->nr)
                        item->util = item; /* better a valid pointer than a fake one */
        }
  
 -      return report_path_error(m, pattern, prefix);
 +      ret = report_path_error(m, pattern, prefix);
 +      free(m);
 +      return ret;
  }
  
  static void add_remove_files(struct string_list *list)
@@@ -1366,13 -1364,14 +1366,14 @@@ int cmd_status(int argc, const char **a
        refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, &s.pathspec, NULL, NULL);
  
        fd = hold_locked_index(&index_lock, 0);
-       if (0 <= fd)
-               update_index_if_able(&the_index, &index_lock);
  
        s.is_initial = get_sha1(s.reference, sha1) ? 1 : 0;
        s.ignore_submodule_arg = ignore_submodule_arg;
        wt_status_collect(&s);
  
+       if (0 <= fd)
+               update_index_if_able(&the_index, &index_lock);
        if (s.relative_paths)
                s.prefix = prefix;
  
  
  static const char *implicit_ident_advice(void)
  {
 -      char *user_config = NULL;
 -      char *xdg_config = NULL;
 -      int config_exists;
 +      char *user_config = expand_user_path("~/.gitconfig");
 +      char *xdg_config = xdg_config_home("config");
 +      int config_exists = file_exists(user_config) || file_exists(xdg_config);
  
 -      home_config_paths(&user_config, &xdg_config, "config");
 -      config_exists = file_exists(user_config) || file_exists(xdg_config);
        free(user_config);
        free(xdg_config);
  
diff --combined builtin/update-index.c
index 0665b31ea1e59734a2ace701b613fffebd074dde,790a6aa9db3ce93036e7cb533abe47bee47ce580..7431938fa654ba9af524a7018ab2c7be8242caaa
@@@ -33,6 -33,7 +33,7 @@@ static int mark_valid_only
  static int mark_skip_worktree_only;
  #define MARK_FLAG 1
  #define UNMARK_FLAG 2
+ static struct strbuf mtime_dir = STRBUF_INIT;
  
  __attribute__((format (printf, 1, 2)))
  static void report(const char *fmt, ...)
        va_end(vp);
  }
  
+ static void remove_test_directory(void)
+ {
+       if (mtime_dir.len)
+               remove_dir_recursively(&mtime_dir, 0);
+ }
+ static const char *get_mtime_path(const char *path)
+ {
+       static struct strbuf sb = STRBUF_INIT;
+       strbuf_reset(&sb);
+       strbuf_addf(&sb, "%s/%s", mtime_dir.buf, path);
+       return sb.buf;
+ }
+ static void xmkdir(const char *path)
+ {
+       path = get_mtime_path(path);
+       if (mkdir(path, 0700))
+               die_errno(_("failed to create directory %s"), path);
+ }
+ static int xstat_mtime_dir(struct stat *st)
+ {
+       if (stat(mtime_dir.buf, st))
+               die_errno(_("failed to stat %s"), mtime_dir.buf);
+       return 0;
+ }
+ static int create_file(const char *path)
+ {
+       int fd;
+       path = get_mtime_path(path);
+       fd = open(path, O_CREAT | O_RDWR, 0644);
+       if (fd < 0)
+               die_errno(_("failed to create file %s"), path);
+       return fd;
+ }
+ static void xunlink(const char *path)
+ {
+       path = get_mtime_path(path);
+       if (unlink(path))
+               die_errno(_("failed to delete file %s"), path);
+ }
+ static void xrmdir(const char *path)
+ {
+       path = get_mtime_path(path);
+       if (rmdir(path))
+               die_errno(_("failed to delete directory %s"), path);
+ }
+ static void avoid_racy(void)
+ {
+       /*
+        * not use if we could usleep(10) if USE_NSEC is defined. The
+        * field nsec could be there, but the OS could choose to
+        * ignore it?
+        */
+       sleep(1);
+ }
+ static int test_if_untracked_cache_is_supported(void)
+ {
+       struct stat st;
+       struct stat_data base;
+       int fd, ret = 0;
+       strbuf_addstr(&mtime_dir, "mtime-test-XXXXXX");
+       if (!mkdtemp(mtime_dir.buf))
+               die_errno("Could not make temporary directory");
+       fprintf(stderr, _("Testing "));
+       atexit(remove_test_directory);
+       xstat_mtime_dir(&st);
+       fill_stat_data(&base, &st);
+       fputc('.', stderr);
+       avoid_racy();
+       fd = create_file("newfile");
+       xstat_mtime_dir(&st);
+       if (!match_stat_data(&base, &st)) {
+               close(fd);
+               fputc('\n', stderr);
+               fprintf_ln(stderr,_("directory stat info does not "
+                                   "change after adding a new file"));
+               goto done;
+       }
+       fill_stat_data(&base, &st);
+       fputc('.', stderr);
+       avoid_racy();
+       xmkdir("new-dir");
+       xstat_mtime_dir(&st);
+       if (!match_stat_data(&base, &st)) {
+               close(fd);
+               fputc('\n', stderr);
+               fprintf_ln(stderr, _("directory stat info does not change "
+                                    "after adding a new directory"));
+               goto done;
+       }
+       fill_stat_data(&base, &st);
+       fputc('.', stderr);
+       avoid_racy();
+       write_or_die(fd, "data", 4);
+       close(fd);
+       xstat_mtime_dir(&st);
+       if (match_stat_data(&base, &st)) {
+               fputc('\n', stderr);
+               fprintf_ln(stderr, _("directory stat info changes "
+                                    "after updating a file"));
+               goto done;
+       }
+       fputc('.', stderr);
+       avoid_racy();
+       close(create_file("new-dir/new"));
+       xstat_mtime_dir(&st);
+       if (match_stat_data(&base, &st)) {
+               fputc('\n', stderr);
+               fprintf_ln(stderr, _("directory stat info changes after "
+                                    "adding a file inside subdirectory"));
+               goto done;
+       }
+       fputc('.', stderr);
+       avoid_racy();
+       xunlink("newfile");
+       xstat_mtime_dir(&st);
+       if (!match_stat_data(&base, &st)) {
+               fputc('\n', stderr);
+               fprintf_ln(stderr, _("directory stat info does not "
+                                    "change after deleting a file"));
+               goto done;
+       }
+       fill_stat_data(&base, &st);
+       fputc('.', stderr);
+       avoid_racy();
+       xunlink("new-dir/new");
+       xrmdir("new-dir");
+       xstat_mtime_dir(&st);
+       if (!match_stat_data(&base, &st)) {
+               fputc('\n', stderr);
+               fprintf_ln(stderr, _("directory stat info does not "
+                                    "change after deleting a directory"));
+               goto done;
+       }
+       if (rmdir(mtime_dir.buf))
+               die_errno(_("failed to delete directory %s"), mtime_dir.buf);
+       fprintf_ln(stderr, _(" OK"));
+       ret = 1;
+ done:
+       strbuf_release(&mtime_dir);
+       return ret;
+ }
  static int mark_ce_flags(const char *path, int flag, int mark)
  {
        int namelen = strlen(path);
@@@ -532,9 -693,10 +693,9 @@@ static int do_unresolve(int ac, const c
  
        for (i = 1; i < ac; i++) {
                const char *arg = av[i];
 -              const char *p = prefix_path(prefix, prefix_length, arg);
 +              char *p = prefix_path(prefix, prefix_length, arg);
                err |= unresolve_one(p);
 -              if (p < arg || p > arg + strlen(arg))
 -                      free((char *)p);
 +              free(p);
        }
        return err;
  }
@@@ -583,7 -745,6 +744,7 @@@ static int do_reupdate(int ac, const ch
                path = xstrdup(ce->name);
                update_one(path);
                free(path);
 +              free(old);
                if (save_nr != active_nr)
                        goto redo;
        }
@@@ -741,6 -902,7 +902,7 @@@ static int reupdate_callback(struct par
  int cmd_update_index(int argc, const char **argv, const char *prefix)
  {
        int newfd, entries, has_errors = 0, line_termination = '\n';
+       int untracked_cache = -1;
        int read_from_stdin = 0;
        int prefix_length = prefix ? strlen(prefix) : 0;
        int preferred_index_format = 0;
                        N_("write index in this format")),
                OPT_BOOL(0, "split-index", &split_index,
                        N_("enable or disable split index")),
+               OPT_BOOL(0, "untracked-cache", &untracked_cache,
+                       N_("enable/disable untracked cache")),
+               OPT_SET_INT(0, "force-untracked-cache", &untracked_cache,
+                           N_("enable untracked cache without testing the filesystem"), 2),
                OPT_END()
        };
  
                case PARSE_OPT_DONE:
                {
                        const char *path = ctx.argv[0];
 -                      const char *p;
 +                      char *p;
  
                        setup_work_tree();
                        p = prefix_path(prefix, prefix_length, path);
                        update_one(p);
                        if (set_executable_bit)
                                chmod_path(set_executable_bit, p);
 -                      free((char *)p);
 +                      free(p);
                        ctx.argc--;
                        ctx.argv++;
                        break;
  
                setup_work_tree();
                while (strbuf_getline(&buf, stdin, line_termination) != EOF) {
 -                      const char *p;
 +                      char *p;
                        if (line_termination && buf.buf[0] == '"') {
                                strbuf_reset(&nbuf);
                                if (unquote_c_style(&nbuf, buf.buf, NULL))
                        update_one(p);
                        if (set_executable_bit)
                                chmod_path(set_executable_bit, p);
 -                      free((char *)p);
 +                      free(p);
                }
                strbuf_release(&nbuf);
                strbuf_release(&buf);
                the_index.split_index = NULL;
                the_index.cache_changed |= SOMETHING_CHANGED;
        }
+       if (untracked_cache > 0) {
+               struct untracked_cache *uc;
+               if (untracked_cache < 2) {
+                       setup_work_tree();
+                       if (!test_if_untracked_cache_is_supported())
+                               return 1;
+               }
+               if (!the_index.untracked) {
+                       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;
+                       the_index.untracked = uc;
+               }
+               add_untracked_ident(the_index.untracked);
+               the_index.cache_changed |= UNTRACKED_CHANGED;
+       } else if (!untracked_cache && the_index.untracked) {
+               the_index.untracked = NULL;
+               the_index.cache_changed |= UNTRACKED_CHANGED;
+       }
  
        if (active_cache_changed) {
                if (newfd < 0) {
diff --combined cache.h
index 1f4226be1580e368b22d62a6e27aa55d37a4dbd7,1392be1030ebb9b5e83221e0fac821646d02d75d..9da9784824dc3a93db2c753613bb56866a356070
+++ b/cache.h
@@@ -43,14 -43,6 +43,14 @@@ int git_deflate_end_gently(git_zstream 
  int git_deflate(git_zstream *, int flush);
  unsigned long git_deflate_bound(git_zstream *, unsigned long);
  
 +/* The length in bytes and in hex digits of an object name (SHA-1 value). */
 +#define GIT_SHA1_RAWSZ 20
 +#define GIT_SHA1_HEXSZ (2 * GIT_SHA1_RAWSZ)
 +
 +struct object_id {
 +      unsigned char hash[GIT_SHA1_RAWSZ];
 +};
 +
  #if defined(DT_UNKNOWN) && !defined(NO_D_TYPE_IN_DIRENT)
  #define DTYPE(de)     ((de)->d_type)
  #else
@@@ -297,8 -289,11 +297,11 @@@ static inline unsigned int canon_mode(u
  #define RESOLVE_UNDO_CHANGED  (1 << 4)
  #define CACHE_TREE_CHANGED    (1 << 5)
  #define SPLIT_INDEX_ORDERED   (1 << 6)
+ #define UNTRACKED_CHANGED     (1 << 7)
  
  struct split_index;
+ struct untracked_cache;
  struct index_state {
        struct cache_entry **cache;
        unsigned int version;
        struct hashmap name_hash;
        struct hashmap dir_hash;
        unsigned char sha1[20];
+       struct untracked_cache *untracked;
  };
  
  extern struct index_state the_index;
@@@ -378,7 -374,6 +382,7 @@@ static inline enum object_type object_t
  
  /* Double-check local_repo_env below if you add to this list. */
  #define GIT_DIR_ENVIRONMENT "GIT_DIR"
 +#define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR"
  #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE"
  #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE"
  #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX"
@@@ -432,13 -427,11 +436,13 @@@ extern int is_inside_git_dir(void)
  extern char *git_work_tree_cfg;
  extern int is_inside_work_tree(void);
  extern const char *get_git_dir(void);
 +extern const char *get_git_common_dir(void);
  extern int is_git_directory(const char *path);
  extern char *get_object_directory(void);
  extern char *get_index_file(void);
  extern char *get_graft_file(void);
  extern int set_git_dir(const char *path);
 +extern int get_common_dir(struct strbuf *sb, const char *gitdir);
  extern const char *get_git_namespace(void);
  extern const char *strip_namespace(const char *namespaced_ref);
  extern const char *get_git_work_tree(void);
@@@ -563,6 -556,8 +567,8 @@@ extern void fill_stat_data(struct stat_
   * INODE_CHANGED, and DATA_CHANGED.
   */
  extern int match_stat_data(const struct stat_data *sd, struct stat *st);
+ extern int match_stat_data_racy(const struct index_state *istate,
+                               const struct stat_data *sd, struct stat *st);
  
  extern void fill_stat_cache_info(struct cache_entry *ce, struct stat *st);
  
@@@ -623,15 -618,6 +629,15 @@@ extern int core_apply_sparse_checkout
  extern int precomposed_unicode;
  extern int protect_hfs;
  extern int protect_ntfs;
 +extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
 +
 +/*
 + * Include broken refs in all ref iterations, which will
 + * generally choke dangerous operations rather than letting
 + * them silently proceed without taking the broken ref into
 + * account.
 + */
 +extern int ref_paranoia;
  
  /*
   * The character that begins a commented line in user-editable file
@@@ -694,19 -680,18 +700,19 @@@ extern int check_repository_format(void
  
  extern char *mksnpath(char *buf, size_t n, const char *fmt, ...)
        __attribute__((format (printf, 3, 4)));
 -extern char *git_snpath(char *buf, size_t n, const char *fmt, ...)
 -      __attribute__((format (printf, 3, 4)));
 +extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 +      __attribute__((format (printf, 2, 3)));
  extern char *git_pathdup(const char *fmt, ...)
        __attribute__((format (printf, 1, 2)));
  extern char *mkpathdup(const char *fmt, ...)
        __attribute__((format (printf, 1, 2)));
  
  /* Return a statically allocated filename matching the sha1 signature */
 -extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 -extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 -extern char *git_path_submodule(const char *path, const char *fmt, ...)
 +extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 +extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 +extern const char *git_path_submodule(const char *path, const char *fmt, ...)
        __attribute__((format (printf, 2, 3)));
 +extern void report_linked_checkout_garbage(void);
  
  /*
   * Return the name of the file in the local object database that would
@@@ -731,13 -716,13 +737,13 @@@ extern char *sha1_pack_name(const unsig
  extern char *sha1_pack_index_name(const unsigned char *sha1);
  
  extern const char *find_unique_abbrev(const unsigned char *sha1, int);
 -extern const unsigned char null_sha1[20];
 +extern const unsigned char null_sha1[GIT_SHA1_RAWSZ];
  
  static inline int hashcmp(const unsigned char *sha1, const unsigned char *sha2)
  {
        int i;
  
 -      for (i = 0; i < 20; i++, sha1++, sha2++) {
 +      for (i = 0; i < GIT_SHA1_RAWSZ; i++, sha1++, sha2++) {
                if (*sha1 != *sha2)
                        return *sha1 - *sha2;
        }
        return 0;
  }
  
 +static inline int oidcmp(const struct object_id *oid1, const struct object_id *oid2)
 +{
 +      return hashcmp(oid1->hash, oid2->hash);
 +}
 +
  static inline int is_null_sha1(const unsigned char *sha1)
  {
        return !hashcmp(sha1, null_sha1);
  }
  
 +static inline int is_null_oid(const struct object_id *oid)
 +{
 +      return !hashcmp(oid->hash, null_sha1);
 +}
 +
  static inline void hashcpy(unsigned char *sha_dst, const unsigned char *sha_src)
  {
 -      memcpy(sha_dst, sha_src, 20);
 +      memcpy(sha_dst, sha_src, GIT_SHA1_RAWSZ);
 +}
 +
 +static inline void oidcpy(struct object_id *dst, const struct object_id *src)
 +{
 +      hashcpy(dst->hash, src->hash);
  }
 +
  static inline void hashclr(unsigned char *hash)
  {
 -      memset(hash, 0, 20);
 +      memset(hash, 0, GIT_SHA1_RAWSZ);
  }
  
 +static inline void oidclr(struct object_id *oid)
 +{
 +      hashclr(oid->hash);
 +}
 +
 +
  #define EMPTY_TREE_SHA1_HEX \
        "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
  #define EMPTY_TREE_SHA1_BIN_LITERAL \
@@@ -851,6 -814,7 +857,6 @@@ enum scld_error safe_create_leading_dir
  enum scld_error safe_create_leading_directories_const(const char *path);
  
  int mkdir_in_gitdir(const char *path);
 -extern void home_config_paths(char **global, char **xdg, char *file);
  extern char *expand_user_path(const char *path);
  const char *enter_repo(const char *path, int strict);
  static inline int is_absolute_path(const char *path)
@@@ -870,16 -834,8 +876,16 @@@ char *strip_path_suffix(const char *pat
  int daemon_avoid_alias(const char *path);
  extern int is_ntfs_dotgit(const char *name);
  
 +/**
 + * Return a newly allocated string with the evaluation of
 + * "$XDG_CONFIG_HOME/git/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
 + * "$HOME/.config/git/$filename". Return NULL upon error.
 + */
 +extern char *xdg_config_home(const char *filename);
 +
  /* object replacement */
  #define LOOKUP_REPLACE_OBJECT 1
 +#define LOOKUP_UNKNOWN_OBJECT 2
  extern void *read_sha1_file_extended(const unsigned char *sha1, enum object_type *type, unsigned long *size, unsigned flag);
  static inline void *read_sha1_file(const unsigned char *sha1, enum object_type *type, unsigned long *size)
  {
@@@ -916,7 -872,6 +922,7 @@@ static inline const unsigned char *look
  extern int sha1_object_info(const unsigned char *, unsigned long *);
  extern int hash_sha1_file(const void *buf, unsigned long len, const char *type, unsigned char *sha1);
  extern int write_sha1_file(const void *buf, unsigned long len, const char *type, unsigned char *return_sha1);
 +extern int hash_sha1_file_literally(const void *buf, unsigned long len, const char *type, unsigned char *sha1, unsigned flags);
  extern int pretend_sha1_file(void *, unsigned long, enum object_type, unsigned char *);
  extern int force_object_loose(const unsigned char *sha1, time_t mtime);
  extern int git_open_noatime(const char *name);
@@@ -995,10 -950,8 +1001,10 @@@ extern int for_each_abbrev(const char *
   * null-terminated string.
   */
  extern int get_sha1_hex(const char *hex, unsigned char *sha1);
 +extern int get_oid_hex(const char *hex, struct object_id *sha1);
  
  extern char *sha1_to_hex(const unsigned char *sha1);  /* static buffer result! */
 +extern char *oid_to_hex(const struct object_id *oid); /* same static buffer as sha1_to_hex */
  extern int read_ref_full(const char *refname, int resolve_flags,
                         unsigned char *sha1, int *flags);
  extern int read_ref(const char *refname, unsigned char *sha1);
@@@ -1219,7 -1172,6 +1225,7 @@@ extern struct packed_git 
        int pack_fd;
        unsigned pack_local:1,
                 pack_keep:1,
 +               freshened:1,
                 do_not_close:1;
        unsigned char sha1[20];
        /* something like ".git/objects/pack/xxxxx.pack" */
@@@ -1335,16 -1287,14 +1341,16 @@@ int for_each_loose_file_in_objdir_buf(s
  
  /*
   * Iterate over loose and packed objects in both the local
 - * repository and any alternates repositories.
 + * repository and any alternates repositories (unless the
 + * LOCAL_ONLY flag is set).
   */
 +#define FOR_EACH_OBJECT_LOCAL_ONLY 0x1
  typedef int each_packed_object_fn(const unsigned char *sha1,
                                  struct packed_git *pack,
                                  uint32_t pos,
                                  void *data);
 -extern int for_each_loose_object(each_loose_object_fn, void *);
 -extern int for_each_packed_object(each_packed_object_fn, void *);
 +extern int for_each_loose_object(each_loose_object_fn, void *, unsigned flags);
 +extern int for_each_packed_object(each_packed_object_fn, void *, unsigned flags);
  
  struct object_info {
        /* Request */
        unsigned long *sizep;
        unsigned long *disk_sizep;
        unsigned char *delta_base_sha1;
 +      struct strbuf *typename;
  
        /* Response */
        enum {
@@@ -1541,13 -1490,9 +1547,13 @@@ extern const char *git_mailmap_blob
  extern void maybe_flush_or_die(FILE *, const char *);
  __attribute__((format (printf, 2, 3)))
  extern void fprintf_or_die(FILE *, const char *fmt, ...);
 +
 +#define COPY_READ_ERROR (-2)
 +#define COPY_WRITE_ERROR (-3)
  extern int copy_fd(int ifd, int ofd);
  extern int copy_file(const char *dst, const char *src, int mode);
  extern int copy_file_with_time(const char *dst, const char *src, int mode);
 +
  extern void write_or_die(int fd, const void *buf, size_t count);
  extern int write_or_whine(int fd, const void *buf, size_t count, const char *msg);
  extern int write_or_whine_pipe(int fd, const void *buf, size_t count, const char *msg);
@@@ -1561,8 -1506,6 +1567,8 @@@ static inline ssize_t write_str_in_full
  {
        return write_in_full(fd, str, strlen(str));
  }
 +__attribute__((format (printf, 3, 4)))
 +extern int write_file(const char *path, int fatal, const char *fmt, ...);
  
  /* pager.c */
  extern void setup_pager(void);
@@@ -1632,6 -1575,7 +1638,6 @@@ extern int ws_blank_line(const char *li
  #define ws_tab_width(rule)     ((rule) & WS_TAB_WIDTH_MASK)
  
  /* ls-files */
 -int report_path_error(const char *ps_matched, const struct pathspec *pathspec, const char *prefix);
  void overlay_tree_on_cache(const char *tree_name, const char *prefix);
  
  char *alias_lookup(const char *alias);
diff --combined compat/mingw.h
index 98c5e44294cd1f41d5b177b8d4be5219066c2dcd,a6f7b9f1a79e6e13bec379482e5ab23edb057dd8..738865c6c068ed7d8849aff5a9b533dfb1ef8bab
@@@ -76,6 -76,14 +76,14 @@@ struct itimerval 
  };
  #define ITIMER_REAL 0
  
+ struct utsname {
+       char sysname[16];
+       char nodename[1];
+       char release[16];
+       char version[16];
+       char machine[1];
+ };
  /*
   * sanitize preprocessor namespace polluted by Windows headers defining
   * macros which collide with git local versions
@@@ -98,6 -106,8 +106,6 @@@ static inline unsigned int alarm(unsign
  { return 0; }
  static inline int fsync(int fd)
  { return _commit(fd); }
 -static inline pid_t getppid(void)
 -{ return 1; }
  static inline void sync(void)
  {}
  static inline uid_t getuid(void)
@@@ -119,12 -129,6 +127,12 @@@ static inline int sigaddset(sigset_t *s
  #define SIG_UNBLOCK 0
  static inline int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
  { return 0; }
 +static inline pid_t getppid(void)
 +{ return 1; }
 +static inline pid_t getpgid(pid_t pid)
 +{ return pid == 0 ? getpid() : pid; }
 +static inline pid_t tcgetpgrp(int fd)
 +{ return getpid(); }
  
  /*
   * simple adaptors
@@@ -175,6 -179,7 +183,7 @@@ struct passwd *getpwuid(uid_t uid)
  int setitimer(int type, struct itimerval *in, struct itimerval *out);
  int sigaction(int sig, struct sigaction *in, struct sigaction *out);
  int link(const char *oldpath, const char *newpath);
+ int uname(struct utsname *buf);
  
  /*
   * replacements of existing functions
diff --combined dir.c
index 4183acc082671f135fe64cbcaa66ed3b17bc6364,e9eaf97efec17e4e96c3ee6cc09e7d4a74304f6d..d318ffcb2a6a51feca5aee993049fd0fa64acc8f
--- 1/dir.c
--- 2/dir.c
+++ b/dir.c
@@@ -12,7 -12,8 +12,9 @@@
  #include "refs.h"
  #include "wildmatch.h"
  #include "pathspec.h"
 +#include "utf8.h"
+ #include "varint.h"
+ #include "ewah/ewok.h"
  
  struct path_simplify {
        int len;
@@@ -32,8 -33,22 +34,22 @@@ enum path_treatment 
        path_untracked
  };
  
+ /*
+  * Support data structure for our opendir/readdir/closedir wrappers
+  */
+ struct cached_dir {
+       DIR *fdir;
+       struct untracked_cache_dir *untracked;
+       int nr_files;
+       int nr_dirs;
+       struct dirent *de;
+       const char *file;
+       struct untracked_cache_dir *ucd;
+ };
  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);
  
@@@ -378,49 -393,6 +394,49 @@@ int match_pathspec(const struct pathspe
        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.
 +       */
 +      struct strbuf sb = STRBUF_INIT;
 +      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++;
 +      }
 +      strbuf_release(&sb);
 +      return errors;
 +}
 +
  /*
   * Return the length of the "simple" part of a path match limiter.
   */
@@@ -510,7 -482,8 +526,8 @@@ void add_exclude(const char *string, co
        x->el = el;
  }
  
- static void *read_skip_worktree_file_from_index(const char *path, size_t *size)
+ static void *read_skip_worktree_file_from_index(const char *path, size_t *size,
+                                               struct sha1_stat *sha1_stat)
  {
        int pos, len;
        unsigned long sz;
                return NULL;
        }
        *size = xsize_t(sz);
+       if (sha1_stat) {
+               memset(&sha1_stat->stat, 0, sizeof(sha1_stat->stat));
+               hashcpy(sha1_stat->sha1, active_cache[pos]->sha1);
+       }
        return data;
  }
  
@@@ -573,11 -550,93 +594,93 @@@ static void trim_trailing_spaces(char *
                *last_space = '\0';
  }
  
- int add_excludes_from_file_to_list(const char *fname,
-                                  const char *base,
-                                  int baselen,
-                                  struct exclude_list *el,
-                                  int check_index)
+ /*
+  * 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);
+ }
+ static void invalidate_directory(struct untracked_cache *uc,
+                                struct untracked_cache_dir *dir)
+ {
+       int i;
+       uc->dir_invalidated++;
+       dir->valid = 0;
+       dir->untracked_nr = 0;
+       for (i = 0; i < dir->dirs_nr; i++)
+               dir->dirs[i]->recurse = 0;
+ }
+ /*
+  * 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
+  * exclude rules in "el".
+  *
+  * If "ss" is not NULL, compute SHA-1 of the exclude file and fill
+  * stat data from disk (only valid if add_excludes returns zero). If
+  * ss_valid is non-zero, "ss" must contain good value as input.
+  */
+ static int add_excludes(const char *fname, const char *base, int baselen,
+                       struct exclude_list *el, int check_index,
+                       struct sha1_stat *sha1_stat)
  {
        struct stat st;
        int fd, i, lineno = 1;
                if (0 <= fd)
                        close(fd);
                if (!check_index ||
-                   (buf = read_skip_worktree_file_from_index(fname, &size)) == NULL)
+                   (buf = read_skip_worktree_file_from_index(fname, &size, sha1_stat)) == NULL)
                        return -1;
                if (size == 0) {
                        free(buf);
        } else {
                size = xsize_t(st.st_size);
                if (size == 0) {
+                       if (sha1_stat) {
+                               fill_stat_data(&sha1_stat->stat, &st);
+                               hashcpy(sha1_stat->sha1, EMPTY_BLOB_SHA1_BIN);
+                               sha1_stat->valid = 1;
+                       }
                        close(fd);
                        return 0;
                }
                }
                buf[size++] = '\n';
                close(fd);
+               if (sha1_stat) {
+                       int pos;
+                       if (sha1_stat->valid &&
+                           !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 &&
+                                !ce_stage(active_cache[pos]) &&
+                                ce_uptodate(active_cache[pos]) &&
+                                !would_convert_to_git(fname))
+                               hashcpy(sha1_stat->sha1, active_cache[pos]->sha1);
+                       else
+                               hash_sha1_file(buf, size, "blob", sha1_stat->sha1);
+                       fill_stat_data(&sha1_stat->stat, &st);
+                       sha1_stat->valid = 1;
+               }
        }
  
        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] != '#') {
        return 0;
  }
  
+ int add_excludes_from_file_to_list(const char *fname, const char *base,
+                                  int baselen, struct exclude_list *el,
+                                  int check_index)
+ {
+       return add_excludes(fname, base, baselen, el, check_index, NULL);
+ }
  struct exclude_list *add_exclude_list(struct dir_struct *dir,
                                      int group_type, const char *src)
  {
  /*
   * 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)
@@@ -837,6 -933,7 +982,7 @@@ static void prep_exclude(struct dir_str
        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];
        /* 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) {
                        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);
                }
  
                /* Try to read per-directory file */
-               if (dir->exclude_per_dir) {
+               hashclr(sha1_stat.sha1);
+               sha1_stat.valid = 0;
+               if (dir->exclude_per_dir &&
+                   /*
+                    * If we know that no files have been added in
+                    * this directory (i.e. valid_cached_dir() has
+                    * been executed and set untracked->valid) ..
+                    */
+                   (!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()
+                     */
+                    !is_null_sha1(untracked->exclude_sha1))) {
                        /*
                         * dir->basebuf gets reused by the traversal, but we
                         * need fname to remain unchanged to ensure the src
                        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);
+               }
+               /*
+                * NEEDSWORK: when untracked cache is enabled, prep_exclude()
+                * will first be called in valid_cached_dir() then maybe many
+                * times more in last_exclude_matching(). When the cache is
+                * used, last_exclude_matching() will not be called and
+                * reading .gitignore content will be a waste.
+                *
+                * So when it's called by valid_cached_dir() and we can get
+                * .gitignore SHA-1 from the index (i.e. .gitignore is not
+                * modified on work tree), we could delay reading the
+                * .gitignore content until we absolutely need it in
+                * last_exclude_matching(). Be careful about ignore rule
+                * order, though, if you do that.
+                */
+               if (untracked &&
+                   hashcmp(sha1_stat.sha1, untracked->exclude_sha1)) {
+                       invalidate_gitignore(dir->untracked, untracked);
+                       hashcpy(untracked->exclude_sha1, sha1_stat.sha1);
                }
                dir->exclude_stack = stk;
                current = stk->baselen;
@@@ -1107,6 -1250,7 +1299,7 @@@ static enum exist_status directory_exis
   *  (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)
  {
        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);
  }
  
  /*
@@@ -1250,6 -1396,7 +1445,7 @@@ static int get_dtype(struct dirent *de
  }
  
  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)
                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:
        }
  }
  
+ static enum path_treatment treat_path_fast(struct dir_struct *dir,
+                                          struct untracked_cache_dir *untracked,
+                                          struct cached_dir *cdir,
+                                          struct strbuf *path,
+                                          int baselen,
+                                          const struct path_simplify *simplify)
+ {
+       strbuf_setlen(path, baselen);
+       if (!cdir->ucd) {
+               strbuf_addstr(path, cdir->file);
+               return path_untracked;
+       }
+       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, '/');
+       if (cdir->ucd->check_only)
+               /*
+                * check_only is set as a result of treat_directory() getting
+                * to its bottom. Verify again the same set of directories
+                * with check_only set.
+                */
+               return read_directory_recursive(dir, path->buf, path->len,
+                                               cdir->ucd, 1, simplify);
+       /*
+        * We get path_recurse in the first run when
+        * directory_exists_in_index() returns index_nonexistent. We
+        * are sure that new changes in the index does not impact the
+        * outcome. Return now.
+        */
+       return path_recurse;
+ }
  static enum path_treatment treat_path(struct dir_struct *dir,
-                                     struct dirent *de,
+                                     struct untracked_cache_dir *untracked,
+                                     struct cached_dir *cdir,
                                      struct strbuf *path,
                                      int baselen,
                                      const struct path_simplify *simplify)
  {
        int dtype;
+       struct dirent *de = cdir->de;
  
+       if (!de)
+               return treat_path_fast(dir, untracked, cdir, path,
+                                      baselen, simplify);
        if (is_dot_or_dotdot(de->d_name) || !strcmp(de->d_name, ".git"))
                return path_none;
        strbuf_setlen(path, baselen);
                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);
+ }
+ static int valid_cached_dir(struct dir_struct *dir,
+                           struct untracked_cache_dir *untracked,
+                           struct strbuf *path,
+                           int check_only)
+ {
+       struct stat st;
+       if (!untracked)
+               return 0;
+       if (stat(path->len ? path->buf : ".", &st)) {
+               invalidate_directory(dir->untracked, untracked);
+               memset(&untracked->stat_data, 0, sizeof(untracked->stat_data));
+               return 0;
+       }
+       if (!untracked->valid ||
+           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);
+               return 0;
+       }
+       if (untracked->check_only != !!check_only) {
+               invalidate_directory(dir->untracked, untracked);
+               return 0;
+       }
+       /*
+        * prep_exclude will be called eventually on this directory,
+        * but it's called much later in last_exclude_matching(). We
+        * need it now to determine the validity of the cache for this
+        * path. The next calls will be nearly no-op, the way
+        * prep_exclude() is designed.
+        */
+       if (path->len && path->buf[path->len - 1] != '/') {
+               strbuf_addch(path, '/');
+               prep_exclude(dir, path->buf, path->len);
+               strbuf_setlen(path, path->len - 1);
+       } else
+               prep_exclude(dir, path->buf, path->len);
+       /* hopefully prep_exclude() haven't invalidated this entry... */
+       return untracked->valid;
+ }
+ static int open_cached_dir(struct cached_dir *cdir,
+                          struct dir_struct *dir,
+                          struct untracked_cache_dir *untracked,
+                          struct strbuf *path,
+                          int check_only)
+ {
+       memset(cdir, 0, sizeof(*cdir));
+       cdir->untracked = untracked;
+       if (valid_cached_dir(dir, untracked, path, check_only))
+               return 0;
+       cdir->fdir = opendir(path->len ? path->buf : ".");
+       if (dir->untracked)
+               dir->untracked->dir_opened++;
+       if (!cdir->fdir)
+               return -1;
+       return 0;
+ }
+ static int read_cached_dir(struct cached_dir *cdir)
+ {
+       if (cdir->fdir) {
+               cdir->de = readdir(cdir->fdir);
+               if (!cdir->de)
+                       return -1;
+               return 0;
+       }
+       while (cdir->nr_dirs < cdir->untracked->dirs_nr) {
+               struct untracked_cache_dir *d = cdir->untracked->dirs[cdir->nr_dirs];
+               if (!d->recurse) {
+                       cdir->nr_dirs++;
+                       continue;
+               }
+               cdir->ucd = d;
+               cdir->nr_dirs++;
+               return 0;
+       }
+       cdir->ucd = NULL;
+       if (cdir->nr_files < cdir->untracked->untracked_nr) {
+               struct untracked_cache_dir *d = cdir->untracked;
+               cdir->file = d->untracked[cdir->nr_files++];
+               return 0;
+       }
+       return -1;
+ }
+ static void close_cached_dir(struct cached_dir *cdir)
+ {
+       if (cdir->fdir)
+               closedir(cdir->fdir);
+       /*
+        * We have gone through this directory and found no untracked
+        * entries. Mark it valid.
+        */
+       if (cdir->untracked) {
+               cdir->untracked->valid = 1;
+               cdir->untracked->recurse = 1;
+       }
  }
  
  /*
   */
  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;
+       struct cached_dir cdir;
        enum path_treatment state, subdir_state, dir_state = path_none;
-       struct dirent *de;
        struct strbuf path = STRBUF_INIT;
  
        strbuf_add(&path, base, baselen);
  
-       fdir = opendir(path.len ? path.buf : ".");
-       if (!fdir)
+       if (open_cached_dir(&cdir, dir, untracked, &path, check_only))
                goto out;
  
-       while ((de = readdir(fdir)) != NULL) {
+       if (untracked)
+               untracked->check_only = !!check_only;
+       while (!read_cached_dir(&cdir)) {
                /* check how the file or directory should be treated */
-               state = treat_path(dir, de, &path, baselen, simplify);
+               state = treat_path(dir, untracked, &cdir, &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 (cdir.fdir)
+                                       add_untracked(untracked, path.buf + baselen);
                                break;
+                       }
                        /* skip the dir_add_* part */
                        continue;
                }
                        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 (cdir.fdir)
+                               add_untracked(untracked, path.buf + baselen);
                        break;
  
                default:
                        break;
                }
        }
-       closedir(fdir);
+       close_cached_dir(&cdir);
   out:
        strbuf_release(&path);
  
@@@ -1469,7 -1781,7 +1830,7 @@@ static int treat_leading_path(struct di
                        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) {
        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))
+               die_errno(_("failed to get kernel name and information"));
+       strbuf_addf(&sb, "Location %s, system %s %s %s", get_git_work_tree(),
+                   uts.sysname, uts.release, uts.version);
+       return sb.buf;
+ }
+ static int ident_in_untracked(const struct untracked_cache *uc)
+ {
+       const char *end = uc->ident.buf + uc->ident.len;
+       const char *p   = uc->ident.buf;
+       for (p = uc->ident.buf; p < end; p += strlen(p) + 1)
+               if (!strcmp(p, get_ident_string()))
+                       return 1;
+       return 0;
+ }
+ void add_untracked_ident(struct untracked_cache *uc)
+ {
+       if (ident_in_untracked(uc))
+               return;
+       strbuf_addstr(&uc->ident, get_ident_string());
+       /* this strbuf contains a list of strings, save NUL too */
+       strbuf_addch(&uc->ident, 0);
+ }
+ 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 || getenv("GIT_DISABLE_UNTRACKED_CACHE"))
+               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;
+       /*
+        * 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."));
+               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;
+       }
+       /* Make sure this directory is not dropped out at saving phase */
+       root->recurse = 1;
+       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()
         * 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);
+       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;
  }
  
@@@ -1671,19 -2141,20 +2190,21 @@@ int remove_dir_recursively(struct strbu
  void setup_standard_excludes(struct dir_struct *dir)
  {
        const char *path;
 -      char *xdg_path;
  
        dir->exclude_per_dir = ".gitignore";
-               add_excludes_from_file(dir, excludes_file);
 +
 +      /* 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 (!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(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_1(dir, excludes_file,
 -                                       dir->untracked ? &dir->ss_excludes_file : NULL);
  }
  
  int remove_path(const char *name)
@@@ -1735,3 -2206,404 +2256,404 @@@ void clear_directory(struct dir_struct 
        }
        strbuf_release(&dir->basebuf);
  }
+ struct ondisk_untracked_cache {
+       struct stat_data info_exclude_stat;
+       struct stat_data excludes_file_stat;
+       uint32_t dir_flags;
+       unsigned char info_exclude_sha1[20];
+       unsigned char excludes_file_sha1[20];
+       char exclude_per_dir[FLEX_ARRAY];
+ };
+ #define ouc_size(len) (offsetof(struct ondisk_untracked_cache, exclude_per_dir) + len + 1)
+ struct write_data {
+       int index;         /* number of written untracked_cache_dir */
+       struct ewah_bitmap *check_only; /* from untracked_cache_dir */
+       struct ewah_bitmap *valid;      /* from untracked_cache_dir */
+       struct ewah_bitmap *sha1_valid; /* set if exclude_sha1 is not null */
+       struct strbuf out;
+       struct strbuf sb_stat;
+       struct strbuf sb_sha1;
+ };
+ static void stat_data_to_disk(struct stat_data *to, const struct stat_data *from)
+ {
+       to->sd_ctime.sec  = htonl(from->sd_ctime.sec);
+       to->sd_ctime.nsec = htonl(from->sd_ctime.nsec);
+       to->sd_mtime.sec  = htonl(from->sd_mtime.sec);
+       to->sd_mtime.nsec = htonl(from->sd_mtime.nsec);
+       to->sd_dev        = htonl(from->sd_dev);
+       to->sd_ino        = htonl(from->sd_ino);
+       to->sd_uid        = htonl(from->sd_uid);
+       to->sd_gid        = htonl(from->sd_gid);
+       to->sd_size       = htonl(from->sd_size);
+ }
+ static void write_one_dir(struct untracked_cache_dir *untracked,
+                         struct write_data *wd)
+ {
+       struct stat_data stat_data;
+       struct strbuf *out = &wd->out;
+       unsigned char intbuf[16];
+       unsigned int intlen, value;
+       int i = wd->index++;
+       /*
+        * untracked_nr should be reset whenever valid is clear, but
+        * for safety..
+        */
+       if (!untracked->valid) {
+               untracked->untracked_nr = 0;
+               untracked->check_only = 0;
+       }
+       if (untracked->check_only)
+               ewah_set(wd->check_only, i);
+       if (untracked->valid) {
+               ewah_set(wd->valid, i);
+               stat_data_to_disk(&stat_data, &untracked->stat_data);
+               strbuf_add(&wd->sb_stat, &stat_data, sizeof(stat_data));
+       }
+       if (!is_null_sha1(untracked->exclude_sha1)) {
+               ewah_set(wd->sha1_valid, i);
+               strbuf_add(&wd->sb_sha1, untracked->exclude_sha1, 20);
+       }
+       intlen = encode_varint(untracked->untracked_nr, intbuf);
+       strbuf_add(out, intbuf, intlen);
+       /* skip non-recurse directories */
+       for (i = 0, value = 0; i < untracked->dirs_nr; i++)
+               if (untracked->dirs[i]->recurse)
+                       value++;
+       intlen = encode_varint(value, intbuf);
+       strbuf_add(out, intbuf, intlen);
+       strbuf_add(out, untracked->name, strlen(untracked->name) + 1);
+       for (i = 0; i < untracked->untracked_nr; i++)
+               strbuf_add(out, untracked->untracked[i],
+                          strlen(untracked->untracked[i]) + 1);
+       for (i = 0; i < untracked->dirs_nr; i++)
+               if (untracked->dirs[i]->recurse)
+                       write_one_dir(untracked->dirs[i], wd);
+ }
+ void write_untracked_extension(struct strbuf *out, struct untracked_cache *untracked)
+ {
+       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);
+       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;
+       if (!untracked->root) {
+               varint_len = encode_varint(0, varbuf);
+               strbuf_add(out, varbuf, varint_len);
+               return;
+       }
+       wd.index      = 0;
+       wd.check_only = ewah_new();
+       wd.valid      = ewah_new();
+       wd.sha1_valid = ewah_new();
+       strbuf_init(&wd.out, 1024);
+       strbuf_init(&wd.sb_stat, 1024);
+       strbuf_init(&wd.sb_sha1, 1024);
+       write_one_dir(untracked->root, &wd);
+       varint_len = encode_varint(wd.index, varbuf);
+       strbuf_add(out, varbuf, varint_len);
+       strbuf_addbuf(out, &wd.out);
+       ewah_serialize_strbuf(wd.valid, out);
+       ewah_serialize_strbuf(wd.check_only, out);
+       ewah_serialize_strbuf(wd.sha1_valid, out);
+       strbuf_addbuf(out, &wd.sb_stat);
+       strbuf_addbuf(out, &wd.sb_sha1);
+       strbuf_addch(out, '\0'); /* safe guard for string lists */
+       ewah_free(wd.valid);
+       ewah_free(wd.check_only);
+       ewah_free(wd.sha1_valid);
+       strbuf_release(&wd.out);
+       strbuf_release(&wd.sb_stat);
+       strbuf_release(&wd.sb_sha1);
+ }
+ static void free_untracked(struct untracked_cache_dir *ucd)
+ {
+       int i;
+       if (!ucd)
+               return;
+       for (i = 0; i < ucd->dirs_nr; i++)
+               free_untracked(ucd->dirs[i]);
+       for (i = 0; i < ucd->untracked_nr; i++)
+               free(ucd->untracked[i]);
+       free(ucd->untracked);
+       free(ucd->dirs);
+       free(ucd);
+ }
+ void free_untracked_cache(struct untracked_cache *uc)
+ {
+       if (uc)
+               free_untracked(uc->root);
+       free(uc);
+ }
+ struct read_data {
+       int index;
+       struct untracked_cache_dir **ucd;
+       struct ewah_bitmap *check_only;
+       struct ewah_bitmap *valid;
+       struct ewah_bitmap *sha1_valid;
+       const unsigned char *data;
+       const unsigned char *end;
+ };
+ static void stat_data_from_disk(struct stat_data *to, const struct stat_data *from)
+ {
+       to->sd_ctime.sec  = get_be32(&from->sd_ctime.sec);
+       to->sd_ctime.nsec = get_be32(&from->sd_ctime.nsec);
+       to->sd_mtime.sec  = get_be32(&from->sd_mtime.sec);
+       to->sd_mtime.nsec = get_be32(&from->sd_mtime.nsec);
+       to->sd_dev        = get_be32(&from->sd_dev);
+       to->sd_ino        = get_be32(&from->sd_ino);
+       to->sd_uid        = get_be32(&from->sd_uid);
+       to->sd_gid        = get_be32(&from->sd_gid);
+       to->sd_size       = get_be32(&from->sd_size);
+ }
+ static int read_one_dir(struct untracked_cache_dir **untracked_,
+                       struct read_data *rd)
+ {
+       struct untracked_cache_dir ud, *untracked;
+       const unsigned char *next, *data = rd->data, *end = rd->end;
+       unsigned int value;
+       int i, len;
+       memset(&ud, 0, sizeof(ud));
+       next = data;
+       value = decode_varint(&next);
+       if (next > end)
+               return -1;
+       ud.recurse         = 1;
+       ud.untracked_alloc = value;
+       ud.untracked_nr    = value;
+       if (ud.untracked_nr)
+               ud.untracked = xmalloc(sizeof(*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);
+       data = next;
+       len = strlen((const char *)data);
+       next = data + len + 1;
+       if (next > rd->end)
+               return -1;
+       *untracked_ = untracked = xmalloc(sizeof(*untracked) + len);
+       memcpy(untracked, &ud, sizeof(ud));
+       memcpy(untracked->name, data, len + 1);
+       data = next;
+       for (i = 0; i < untracked->untracked_nr; i++) {
+               len = strlen((const char *)data);
+               next = data + len + 1;
+               if (next > rd->end)
+                       return -1;
+               untracked->untracked[i] = xstrdup((const char*)data);
+               data = next;
+       }
+       rd->ucd[rd->index++] = untracked;
+       rd->data = data;
+       for (i = 0; i < untracked->dirs_nr; i++) {
+               len = read_one_dir(untracked->dirs + i, rd);
+               if (len < 0)
+                       return -1;
+       }
+       return 0;
+ }
+ static void set_check_only(size_t pos, void *cb)
+ {
+       struct read_data *rd = cb;
+       struct untracked_cache_dir *ud = rd->ucd[pos];
+       ud->check_only = 1;
+ }
+ static void read_stat(size_t pos, void *cb)
+ {
+       struct read_data *rd = cb;
+       struct untracked_cache_dir *ud = rd->ucd[pos];
+       if (rd->data + sizeof(struct stat_data) > rd->end) {
+               rd->data = rd->end + 1;
+               return;
+       }
+       stat_data_from_disk(&ud->stat_data, (struct stat_data *)rd->data);
+       rd->data += sizeof(struct stat_data);
+       ud->valid = 1;
+ }
+ static void read_sha1(size_t pos, void *cb)
+ {
+       struct read_data *rd = cb;
+       struct untracked_cache_dir *ud = rd->ucd[pos];
+       if (rd->data + 20 > rd->end) {
+               rd->data = rd->end + 1;
+               return;
+       }
+       hashcpy(ud->exclude_sha1, rd->data);
+       rd->data += 20;
+ }
+ static void load_sha1_stat(struct sha1_stat *sha1_stat,
+                          const struct stat_data *stat,
+                          const unsigned char *sha1)
+ {
+       stat_data_from_disk(&sha1_stat->stat, stat);
+       hashcpy(sha1_stat->sha1, sha1);
+       sha1_stat->valid = 1;
+ }
+ struct untracked_cache *read_untracked_extension(const void *data, unsigned long sz)
+ {
+       const struct ondisk_untracked_cache *ouc;
+       struct untracked_cache *uc;
+       struct read_data rd;
+       const unsigned char *next = data, *end = (const unsigned char *)data + sz;
+       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,
+                      ouc->excludes_file_sha1);
+       uc->dir_flags = get_be32(&ouc->dir_flags);
+       uc->exclude_per_dir = xstrdup(ouc->exclude_per_dir);
+       /* NUL after exclude_per_dir is covered by sizeof(*ouc) */
+       next += ouc_size(strlen(ouc->exclude_per_dir));
+       if (next >= end)
+               goto done2;
+       len = decode_varint(&next);
+       if (next > end || len == 0)
+               goto done2;
+       rd.valid      = ewah_new();
+       rd.check_only = ewah_new();
+       rd.sha1_valid = ewah_new();
+       rd.data       = next;
+       rd.end        = end;
+       rd.index      = 0;
+       rd.ucd        = xmalloc(sizeof(*rd.ucd) * len);
+       if (read_one_dir(&uc->root, &rd) || rd.index != len)
+               goto done;
+       next = rd.data;
+       len = ewah_read_mmap(rd.valid, next, end - next);
+       if (len < 0)
+               goto done;
+       next += len;
+       len = ewah_read_mmap(rd.check_only, next, end - next);
+       if (len < 0)
+               goto done;
+       next += len;
+       len = ewah_read_mmap(rd.sha1_valid, next, end - next);
+       if (len < 0)
+               goto done;
+       ewah_each_bit(rd.check_only, set_check_only, &rd);
+       rd.data = next + len;
+       ewah_each_bit(rd.valid, read_stat, &rd);
+       ewah_each_bit(rd.sha1_valid, read_sha1, &rd);
+       next = rd.data;
+ done:
+       free(rd.ucd);
+       ewah_free(rd.valid);
+       ewah_free(rd.check_only);
+       ewah_free(rd.sha1_valid);
+ done2:
+       if (next != end) {
+               free_untracked_cache(uc);
+               uc = NULL;
+       }
+       return uc;
+ }
+ void untracked_cache_invalidate_path(struct index_state *istate,
+                                    const char *path)
+ {
+       const char *sep;
+       struct untracked_cache_dir *d;
+       if (!istate->untracked || !istate->untracked->root)
+               return;
+       sep = strrchr(path, '/');
+       if (sep)
+               d = lookup_untracked(istate->untracked,
+                                    istate->untracked->root,
+                                    path, sep - path);
+       else
+               d = istate->untracked->root;
+       istate->untracked->dir_invalidated++;
+       d->valid = 0;
+       d->untracked_nr = 0;
+ }
+ 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);
+ }
diff --combined dir.h
index 72b73c65dcfc6f31004a032d0706626424ac2241,6ccbc454ac47da69e1b07b43961b9aaf9a21cbe0..7b5855dd80eda02973a55c827d79826a25937880
--- 1/dir.h
--- 2/dir.h
+++ b/dir.h
@@@ -66,6 -66,7 +66,7 @@@ struct exclude_stack 
        struct exclude_stack *prev; /* the struct exclude_stack for the parent directory */
        int baselen;
        int exclude_ix; /* index of exclude_list within EXC_DIRS exclude_list_group */
+       struct untracked_cache_dir *ucd;
  };
  
  struct exclude_list_group {
        struct exclude_list *el;
  };
  
+ struct sha1_stat {
+       struct stat_data stat;
+       unsigned char sha1[20];
+       int valid;
+ };
+ /*
+  *  Untracked cache
+  *
+  *  The following inputs are sufficient to determine what files in a
+  *  directory are excluded:
+  *
+  *   - The list of files and directories of the directory in question
+  *   - The $GIT_DIR/index
+  *   - dir_struct flags
+  *   - The content of $GIT_DIR/info/exclude
+  *   - The content of core.excludesfile
+  *   - The content (or the lack) of .gitignore of all parent directories
+  *     from $GIT_WORK_TREE
+  *   - The check_only flag in read_directory_recursive (for
+  *     DIR_HIDE_EMPTY_DIRECTORIES)
+  *
+  *  The first input can be checked using directory mtime. In many
+  *  filesystems, directory mtime (stat_data field) is updated when its
+  *  files or direct subdirs are added or removed.
+  *
+  *  The second one can be hooked from cache_tree_invalidate_path().
+  *  Whenever a file (or a submodule) is added or removed from a
+  *  directory, we invalidate that directory.
+  *
+  *  The remaining inputs are easy, their SHA-1 could be used to verify
+  *  their contents (exclude_sha1[], info_exclude_sha1[] and
+  *  excludes_file_sha1[])
+  */
+ struct untracked_cache_dir {
+       struct untracked_cache_dir **dirs;
+       char **untracked;
+       struct stat_data stat_data;
+       unsigned int untracked_alloc, dirs_nr, dirs_alloc;
+       unsigned int untracked_nr;
+       unsigned int check_only : 1;
+       /* all data except 'dirs' in this struct are good */
+       unsigned int valid : 1;
+       unsigned int recurse : 1;
+       /* null SHA-1 means this directory does not have .gitignore */
+       unsigned char exclude_sha1[20];
+       char name[FLEX_ARRAY];
+ };
+ struct untracked_cache {
+       struct sha1_stat ss_info_exclude;
+       struct sha1_stat ss_excludes_file;
+       const char *exclude_per_dir;
+       struct strbuf ident;
+       /*
+        * dir_struct#flags must match dir_flags or the untracked
+        * cache is ignored.
+        */
+       unsigned dir_flags;
+       struct untracked_cache_dir *root;
+       /* Statistics */
+       int dir_created;
+       int gitignore_invalidated;
+       int dir_invalidated;
+       int dir_opened;
+ };
  struct dir_struct {
        int nr, alloc;
        int ignored_nr, ignored_alloc;
        struct exclude_stack *exclude_stack;
        struct exclude *exclude;
        struct strbuf basebuf;
+       /* Enable untracked file cache if set */
+       struct untracked_cache *untracked;
+       struct sha1_stat ss_info_exclude;
+       struct sha1_stat ss_excludes_file;
+       unsigned unmanaged_exclude_files;
  };
  
  /*
@@@ -137,7 -211,6 +211,7 @@@ extern char *common_prefix(const struc
  extern int match_pathspec(const struct pathspec *pathspec,
                          const char *name, int namelen,
                          int prefix, char *seen, int is_dir);
 +extern int report_path_error(const char *ps_matched, const struct pathspec *pathspec, const char *prefix);
  extern int within_depth(const char *name, int namelen, int depth, int max_depth);
  
  extern int fill_directory(struct dir_struct *dir, const struct pathspec *pathspec);
@@@ -226,4 -299,12 +300,12 @@@ static inline int dir_path_match(const 
                              has_trailing_dir);
  }
  
+ void untracked_cache_invalidate_path(struct index_state *, const char *);
+ void untracked_cache_remove_from_index(struct index_state *, const char *);
+ void untracked_cache_add_to_index(struct index_state *, const char *);
+ void free_untracked_cache(struct untracked_cache *);
+ struct untracked_cache *read_untracked_extension(const void *data, unsigned long sz);
+ void write_untracked_extension(struct strbuf *out, struct untracked_cache *untracked);
+ void add_untracked_ident(struct untracked_cache *);
  #endif
diff --combined git-compat-util.h
index b7a97fbe8300532505b9d63dace2a3f76d8e04e2,1663537791e42e19f2d8cbd2493993eae41b55ff..17584adbd093a0848debcc23b2a73dcf520a8fb4
@@@ -3,23 -3,6 +3,23 @@@
  
  #define _FILE_OFFSET_BITS 64
  
 +
 +/* Derived from Linux "Features Test Macro" header
 + * Convenience macros to test the versions of gcc (or
 + * a compatible compiler).
 + * Use them like this:
 + *  #if GIT_GNUC_PREREQ (2,8)
 + *   ... code requiring gcc 2.8 or later ...
 + *  #endif
 +*/
 +#if defined(__GNUC__) && defined(__GNUC_MINOR__)
 +# define GIT_GNUC_PREREQ(maj, min) \
 +      ((__GNUC__ << 16) + __GNUC_MINOR__ >= ((maj) << 16) + (min))
 +#else
 + #define GIT_GNUC_PREREQ(maj, min) 0
 +#endif
 +
 +
  #ifndef FLEX_ARRAY
  /*
   * See if our compiler is known to support flexible array members.
  #endif
  #endif
  
 -#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
 +
 +/*
 + * BUILD_ASSERT_OR_ZERO - assert a build-time dependency, as an expression.
 + * @cond: the compile-time condition which must be true.
 + *
 + * Your compile will fail if the condition isn't true, or can't be evaluated
 + * by the compiler.  This can be used in an expression: its value is "0".
 + *
 + * Example:
 + *    #define foo_to_char(foo)                                        \
 + *             ((char *)(foo)                                         \
 + *              + BUILD_ASSERT_OR_ZERO(offsetof(struct foo, string) == 0))
 + */
 +#define BUILD_ASSERT_OR_ZERO(cond) \
 +      (sizeof(char [1 - 2*!(cond)]) - 1)
 +
 +#if defined(__GNUC__) && (__GNUC__ >= 3)
 +# if GIT_GNUC_PREREQ(3, 1)
 + /* &arr[0] degrades to a pointer: a different type from an array */
 +# define BARF_UNLESS_AN_ARRAY(arr)                                            \
 +      BUILD_ASSERT_OR_ZERO(!__builtin_types_compatible_p(__typeof__(arr), \
 +                                                         __typeof__(&(arr)[0])))
 +# else
 +#  define BARF_UNLESS_AN_ARRAY(arr) 0
 +# endif
 +#endif
 +/*
 + * ARRAY_SIZE - get the number of elements in a visible array
 + *  <at> x: the array whose size you want.
 + *
 + * This does not work on pointers, or arrays declared as [], or
 + * function parameters.  With correct compiler support, such usage
 + * will cause a build error (see the build_assert_or_zero macro).
 + */
 +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]) + BARF_UNLESS_AN_ARRAY(x))
 +
  #define bitsizeof(x)  (CHAR_BIT * sizeof(x))
  
  #define maximum_signed_value_of_type(a) \
  #else
  #include <poll.h>
  #endif
 +#ifdef HAVE_BSD_SYSCTL
 +#include <sys/sysctl.h>
 +#endif
  
  #if defined(__MINGW32__)
  /* pull in Windows compatibility stuff */
  #elif defined(_MSC_VER)
  #include "compat/msvc.h"
  #else
+ #include <sys/utsname.h>
  #include <sys/wait.h>
  #include <sys/resource.h>
  #include <sys/socket.h>
@@@ -931,14 -877,4 +932,14 @@@ struct tm *git_gmtime_r(const time_t *
  #define USE_PARENS_AROUND_GETTEXT_N 1
  #endif
  
 +#ifndef SHELL_PATH
 +# define SHELL_PATH "/bin/sh"
 +#endif
 +
 +#ifndef _POSIX_THREAD_SAFE_FUNCTIONS
 +#define flockfile(fh)
 +#define funlockfile(fh)
 +#define getc_unlocked(fh) getc(fh)
 +#endif
 +
  #endif
diff --combined read-cache.c
index 36ff89f29e5f56a5b3dcfd803f12cd139295b8b8,705469eb7a90f3168acec47740d6b8b6b2661864..723d48dddfe58f50b30105689dfeb9bcb388c8f2
@@@ -39,11 -39,12 +39,12 @@@ static struct cache_entry *refresh_cach
  #define CACHE_EXT_TREE 0x54524545     /* "TREE" */
  #define CACHE_EXT_RESOLVE_UNDO 0x52455543 /* "REUC" */
  #define CACHE_EXT_LINK 0x6c696e6b       /* "link" */
+ #define CACHE_EXT_UNTRACKED 0x554E5452          /* "UNTR" */
  
  /* changes that can be kept in $GIT_DIR/index (basically all extensions) */
  #define EXTMASK (RESOLVE_UNDO_CHANGED | CACHE_TREE_CHANGED | \
                 CE_ENTRY_ADDED | CE_ENTRY_REMOVED | CE_ENTRY_CHANGED | \
-                SPLIT_INDEX_ORDERED)
+                SPLIT_INDEX_ORDERED | UNTRACKED_CHANGED)
  
  struct index_state the_index;
  static const char *alternate_index_output;
@@@ -79,6 -80,7 +80,7 @@@ void rename_index_entry_at(struct index
        memcpy(new->name, new_name, namelen + 1);
  
        cache_tree_invalidate_path(istate, old->name);
+       untracked_cache_remove_from_index(istate, old->name);
        remove_index_entry_at(istate, nr);
        add_index_entry(istate, new, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE);
  }
@@@ -270,20 -272,34 +272,34 @@@ static int ce_match_stat_basic(const st
        return changed;
  }
  
- static int is_racy_timestamp(const struct index_state *istate,
-                            const struct cache_entry *ce)
+ static int is_racy_stat(const struct index_state *istate,
+                       const struct stat_data *sd)
  {
-       return (!S_ISGITLINK(ce->ce_mode) &&
-               istate->timestamp.sec &&
+       return (istate->timestamp.sec &&
  #ifdef USE_NSEC
                 /* nanosecond timestamped files can also be racy! */
-               (istate->timestamp.sec < ce->ce_stat_data.sd_mtime.sec ||
-                (istate->timestamp.sec == ce->ce_stat_data.sd_mtime.sec &&
-                 istate->timestamp.nsec <= ce->ce_stat_data.sd_mtime.nsec))
+               (istate->timestamp.sec < sd->sd_mtime.sec ||
+                (istate->timestamp.sec == sd->sd_mtime.sec &&
+                 istate->timestamp.nsec <= sd->sd_mtime.nsec))
  #else
-               istate->timestamp.sec <= ce->ce_stat_data.sd_mtime.sec
+               istate->timestamp.sec <= sd->sd_mtime.sec
  #endif
-                );
+               );
+ }
+ static int is_racy_timestamp(const struct index_state *istate,
+                            const struct cache_entry *ce)
+ {
+       return (!S_ISGITLINK(ce->ce_mode) &&
+               is_racy_stat(istate, &ce->ce_stat_data));
+ }
+ int match_stat_data_racy(const struct index_state *istate,
+                        const struct stat_data *sd, struct stat *st)
+ {
+       if (is_racy_stat(istate, sd))
+               return MTIME_CHANGED;
+       return match_stat_data(sd, st);
  }
  
  int ie_match_stat(const struct index_state *istate,
@@@ -538,6 -554,7 +554,7 @@@ int remove_file_from_index(struct index
        if (pos < 0)
                pos = -pos-1;
        cache_tree_invalidate_path(istate, path);
+       untracked_cache_remove_from_index(istate, path);
        while (pos < istate->cache_nr && !strcmp(istate->cache[pos]->name, path))
                remove_index_entry_at(istate, pos);
        return 0;
@@@ -681,18 -698,15 +698,18 @@@ int add_to_index(struct index_state *is
        alias = index_file_exists(istate, ce->name, ce_namelen(ce), ignore_case);
        if (alias && !ce_stage(alias) && !ie_match_stat(istate, alias, st, ce_option)) {
                /* Nothing changed, really */
 -              free(ce);
                if (!S_ISGITLINK(alias->ce_mode))
                        ce_mark_uptodate(alias);
                alias->ce_flags |= CE_ADDED;
 +
 +              free(ce);
                return 0;
        }
        if (!intent_only) {
 -              if (index_path(ce->sha1, path, st, HASH_WRITE_OBJECT))
 +              if (index_path(ce->sha1, path, st, HASH_WRITE_OBJECT)) {
 +                      free(ce);
                        return error("unable to index file %s", path);
 +              }
        } else
                set_object_name_for_intent_to_add_entry(ce);
  
                    ce->ce_mode == alias->ce_mode);
  
        if (pretend)
 -              ;
 -      else if (add_index_entry(istate, ce, add_option))
 -              return error("unable to add %s to index",path);
 +              free(ce);
 +      else if (add_index_entry(istate, ce, add_option)) {
 +              free(ce);
 +              return error("unable to add %s to index", path);
 +      }
        if (verbose && !was_same)
                printf("add '%s'\n", path);
        return 0;
@@@ -748,9 -760,12 +765,9 @@@ struct cache_entry *make_cache_entry(un
        ce->ce_mode = create_ce_mode(mode);
  
        ret = refresh_cache_entry(ce, refresh_options);
 -      if (!ret) {
 +      if (ret != ce)
                free(ce);
 -              return NULL;
 -      } else {
 -              return ret;
 -      }
 +      return ret;
  }
  
  int ce_same_name(const struct cache_entry *a, const struct cache_entry *b)
@@@ -982,6 -997,8 +999,8 @@@ static int add_index_entry_with_check(s
        }
        pos = -pos-1;
  
+       untracked_cache_add_to_index(istate, ce->name);
        /*
         * Inserting a merged entry ("stage 0") into the index
         * will always replace all non-merged entries..
@@@ -1372,6 -1389,9 +1391,9 @@@ static int read_index_extension(struct 
                if (read_link_extension(istate, data, sz))
                        return -1;
                break;
+       case CACHE_EXT_UNTRACKED:
+               istate->untracked = read_untracked_extension(data, sz);
+               break;
        default:
                if (*ext < 'A' || 'Z' < *ext)
                        return error("index uses %.4s extension, which we do not understand",
@@@ -1488,25 -1508,18 +1510,25 @@@ static struct cache_entry *create_from_
        return ce;
  }
  
 -static void check_ce_order(struct cache_entry *ce, struct cache_entry *next_ce)
 +static void check_ce_order(struct index_state *istate)
  {
 -      int name_compare = strcmp(ce->name, next_ce->name);
 -      if (0 < name_compare)
 -              die("unordered stage entries in index");
 -      if (!name_compare) {
 -              if (!ce_stage(ce))
 -                      die("multiple stage entries for merged file '%s'",
 -                              ce->name);
 -              if (ce_stage(ce) > ce_stage(next_ce))
 -                      die("unordered stage entries for '%s'",
 -                              ce->name);
 +      unsigned int i;
 +
 +      for (i = 1; i < istate->cache_nr; i++) {
 +              struct cache_entry *ce = istate->cache[i - 1];
 +              struct cache_entry *next_ce = istate->cache[i];
 +              int name_compare = strcmp(ce->name, next_ce->name);
 +
 +              if (0 < name_compare)
 +                      die("unordered stage entries in index");
 +              if (!name_compare) {
 +                      if (!ce_stage(ce))
 +                              die("multiple stage entries for merged file '%s'",
 +                                  ce->name);
 +                      if (ce_stage(ce) > ce_stage(next_ce))
 +                              die("unordered stage entries for '%s'",
 +                                  ce->name);
 +              }
        }
  }
  
@@@ -1571,6 -1584,9 +1593,6 @@@ int do_read_index(struct index_state *i
                ce = create_from_disk(disk_ce, &consumed, previous_name);
                set_index_entry(istate, i, ce);
  
 -              if (i > 0)
 -                      check_ce_order(istate->cache[i - 1], ce);
 -
                src_offset += consumed;
        }
        strbuf_release(&previous_name_buf);
@@@ -1614,10 -1630,11 +1636,10 @@@ int read_index_from(struct index_state 
  
        ret = do_read_index(istate, path, 0);
        split_index = istate->split_index;
 -      if (!split_index)
 -              return ret;
 -
 -      if (is_null_sha1(split_index->base_sha1))
 +      if (!split_index || is_null_sha1(split_index->base_sha1)) {
 +              check_ce_order(istate);
                return ret;
 +      }
  
        if (split_index->base)
                discard_index(split_index->base);
                                     sha1_to_hex(split_index->base_sha1)),
                    sha1_to_hex(split_index->base->sha1));
        merge_base_index(istate);
 +      check_ce_order(istate);
        return ret;
  }
  
@@@ -1667,6 -1683,8 +1689,8 @@@ int discard_index(struct index_state *i
        istate->cache = NULL;
        istate->cache_alloc = 0;
        discard_split_index(istate);
+       free_untracked_cache(istate->untracked);
+       istate->untracked = NULL;
        return 0;
  }
  
@@@ -2053,6 -2071,17 +2077,17 @@@ static int do_write_index(struct index_
                if (err)
                        return -1;
        }
+       if (!strip_extensions && istate->untracked) {
+               struct strbuf sb = STRBUF_INIT;
+               write_untracked_extension(&sb, istate->untracked);
+               err = write_index_ext_header(&c, newfd, CACHE_EXT_UNTRACKED,
+                                            sb.len) < 0 ||
+                       ce_write(&c, newfd, sb.buf, sb.len) < 0;
+               strbuf_release(&sb);
+               if (err)
+                       return -1;
+       }
  
        if (ce_flush(&c, newfd, istate->sha1) || fstat(newfd, &st))
                return -1;
diff --combined wt-status.c
index 38cb165f124d610c789a8d82de42281c795111f7,fc1b82e2c188078e515f6cdcc298ac854b5223c3..33452f169dfca7a3cd4f15b1c3cdc91a3cf40735
@@@ -585,6 -585,8 +585,8 @@@ static void wt_status_collect_untracked
                        DIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES;
        if (s->show_ignored_files)
                dir.flags |= DIR_SHOW_IGNORED_TOO;
+       else
+               dir.untracked = the_index.untracked;
        setup_standard_excludes(&dir);
  
        fill_directory(&dir, &s->pathspec);
@@@ -729,6 -731,7 +731,6 @@@ static void wt_status_print_submodule_s
        struct strbuf cmd_stdout = STRBUF_INIT;
        struct strbuf summary = STRBUF_INIT;
        char *summary_content;
 -      size_t len;
  
        argv_array_pushf(&sm_summary.env_array, "GIT_INDEX_FILE=%s",
                         s->index_file);
  
        sm_summary.git_cmd = 1;
        sm_summary.no_stdin = 1;
 -      fflush(s->fp);
 -      sm_summary.out = -1;
  
 -      run_command(&sm_summary);
 -
 -      len = strbuf_read(&cmd_stdout, sm_summary.out, 1024);
 +      capture_command(&sm_summary, &cmd_stdout, 1024);
  
        /* prepend header, only if there's an actual output */
 -      if (len) {
 +      if (cmd_stdout.len) {
                if (uncommitted)
                        strbuf_addstr(&summary, _("Submodules changed but not updated:"));
                else
        strbuf_release(&cmd_stdout);
  
        if (s->display_comment_prefix) {
 +              size_t len;
                summary_content = strbuf_detach(&summary, &len);
                strbuf_add_commented_lines(&summary, summary_content, len);
                free(summary_content);
@@@ -845,8 -851,6 +847,8 @@@ static void wt_status_print_verbose(str
  {
        struct rev_info rev;
        struct setup_revision_opt opt;
 +      int dirty_submodules;
 +      const char *c = color(WT_STATUS_HEADER, s);
  
        init_revisions(&rev, NULL);
        DIFF_OPT_SET(&rev.diffopt, ALLOW_TEXTCONV);
                rev.diffopt.use_color = 0;
                wt_status_add_cut_line(s->fp);
        }
 +      if (s->verbose > 1 && s->commitable) {
 +              /* print_updated() printed a header, so do we */
 +              if (s->fp != stdout)
 +                      wt_status_print_trailer(s);
 +              status_printf_ln(s, c, _("Changes to be committed:"));
 +              rev.diffopt.a_prefix = "c/";
 +              rev.diffopt.b_prefix = "i/";
 +      } /* else use prefix as per user config */
        run_diff_index(&rev, 1);
 +      if (s->verbose > 1 &&
 +          wt_status_check_worktree_changes(s, &dirty_submodules)) {
 +              status_printf_ln(s, c,
 +                      "--------------------------------------------------");
 +              status_printf_ln(s, c, _("Changes not staged for commit:"));
 +              setup_work_tree();
 +              rev.diffopt.a_prefix = "i/";
 +              rev.diffopt.b_prefix = "w/";
 +              run_diff_files(&rev, 0);
 +      }
  }
  
  static void wt_status_print_tracking(struct wt_status *s)
@@@ -1238,8 -1224,6 +1240,8 @@@ static void wt_status_get_detached_from
                state->detached_from =
                        xstrdup(find_unique_abbrev(cb.nsha1, DEFAULT_ABBREV));
        hashcpy(state->detached_sha1, cb.nsha1);
 +      state->detached_at = !get_sha1("HEAD", sha1) &&
 +                           !hashcmp(sha1, state->detached_sha1);
  
        free(ref);
        strbuf_release(&cb.buf);
@@@ -1328,8 -1312,10 +1330,8 @@@ void wt_status_print(struct wt_status *
                                on_what = _("rebase in progress; onto ");
                                branch_name = state.onto;
                        } else if (state.detached_from) {
 -                              unsigned char sha1[20];
                                branch_name = state.detached_from;
 -                              if (!get_sha1("HEAD", sha1) &&
 -                                  !hashcmp(sha1, state.detached_sha1))
 +                              if (state.detached_at)
                                        on_what = _("HEAD detached at ");
                                else
                                        on_what = _("HEAD detached from ");
@@@ -1550,7 -1536,6 +1552,7 @@@ static void wt_shortstatus_print_tracki
        base = shorten_unambiguous_ref(base, 0);
        color_fprintf(s->fp, header_color, "...");
        color_fprintf(s->fp, branch_color_remote, "%s", base);
 +      free((char *)base);
  
        if (!upstream_is_gone && !num_ours && !num_theirs) {
                fputc(s->null_termination ? '\0' : '\n', s->fp);