]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Merge branch 'en/fill-directory-exponential'
authorJunio C Hamano <gitster@pobox.com>
Wed, 29 Apr 2020 23:15:30 +0000 (16:15 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 29 Apr 2020 23:15:31 +0000 (16:15 -0700)
The directory traversal code had redundant recursive calls which
made its performance characteristics exponential with respect to
the depth of the tree, which was corrected.

* en/fill-directory-exponential:
  completion: fix 'git add' on paths under an untracked directory
  Fix error-prone fill_directory() API; make it only return matches
  dir: replace double pathspec matching with single in treat_directory()
  dir: include DIR_KEEP_UNTRACKED_CONTENTS handling in treat_directory()
  dir: replace exponential algorithm with a linear one
  dir: refactor treat_directory to clarify control flow
  dir: fix confusion based on variable tense
  dir: fix broken comment
  dir: consolidate treat_path() and treat_one_path()
  dir: fix simple typo in comment
  t3000: add more testcases testing a variety of ls-files issues
  t7063: more thorough status checking

1  2 
builtin/clean.c
builtin/grep.c
builtin/stash.c
contrib/completion/git-completion.bash
dir.c
t/t7063-status-untracked-cache.sh
t/t9902-completion.sh

diff --combined builtin/clean.c
index c8c011d2ddfa3e2653535c6d4b36675a20a90031,b189b7b4ea08de2c2c7450991800082516ec4a21..f14c21b8638db70a5fa070096f7853e9900b5edd
@@@ -18,7 -18,6 +18,7 @@@
  #include "color.h"
  #include "pathspec.h"
  #include "help.h"
 +#include "prompt.h"
  
  static int force = -1; /* unset */
  static int interactive;
@@@ -421,6 -420,7 +421,6 @@@ static int find_unique(const char *choi
        return found;
  }
  
 -
  /*
   * Parse user input, and return choice(s) for menu (menu_stuff).
   *
@@@ -580,7 -580,9 +580,7 @@@ static int *list_and_choose(struct menu
                               clean_get_color(CLEAN_COLOR_RESET));
                }
  
 -              if (strbuf_getline_lf(&choice, stdin) != EOF) {
 -                      strbuf_trim(&choice);
 -              } else {
 +              if (git_read_line_interactively(&choice) == EOF) {
                        eof = 1;
                        break;
                }
@@@ -660,7 -662,9 +660,7 @@@ static int filter_by_patterns_cmd(void
                clean_print_color(CLEAN_COLOR_PROMPT);
                printf(_("Input ignore patterns>> "));
                clean_print_color(CLEAN_COLOR_RESET);
 -              if (strbuf_getline_lf(&confirm, stdin) != EOF)
 -                      strbuf_trim(&confirm);
 -              else
 +              if (git_read_line_interactively(&confirm) == EOF)
                        putchar('\n');
  
                /* quit filter_by_pattern mode if press ENTER or Ctrl-D */
@@@ -756,7 -760,9 +756,7 @@@ static int ask_each_cmd(void
                        qname = quote_path_relative(item->string, NULL, &buf);
                        /* TRANSLATORS: Make sure to keep [y/N] as is */
                        printf(_("Remove %s [y/N]? "), qname);
 -                      if (strbuf_getline_lf(&confirm, stdin) != EOF) {
 -                              strbuf_trim(&confirm);
 -                      } else {
 +                      if (git_read_line_interactively(&confirm) == EOF) {
                                putchar('\n');
                                eof = 1;
                        }
@@@ -983,12 -989,6 +983,6 @@@ int cmd_clean(int argc, const char **ar
                if (!cache_name_is_other(ent->name, ent->len))
                        continue;
  
-               if (pathspec.nr)
-                       matches = dir_path_match(&the_index, ent, &pathspec, 0, NULL);
-               if (pathspec.nr && !matches)
-                       continue;
                if (lstat(ent->name, &st))
                        die_errno("Cannot lstat '%s'", ent->name);
  
diff --combined builtin/grep.c
index bdf1a4bbc99d77f814b2a2c7f130a2eedd7118d4,20bc8481317036b6dd57d11ebd28fd3bd9f33e62..5e150f5825b3a549c8b676a3cf91e23d9a72a85c
@@@ -24,7 -24,6 +24,7 @@@
  #include "submodule.h"
  #include "submodule-config.h"
  #include "object-store.h"
 +#include "packfile.h"
  
  static char const * const grep_usage[] = {
        N_("git grep [<options>] [-e] <pattern> [<rev>...] [[--] <path>...]"),
@@@ -33,6 -32,7 +33,6 @@@
  
  static int recurse_submodules;
  
 -#define GREP_NUM_THREADS_DEFAULT 8
  static int num_threads;
  
  static pthread_t *threads;
@@@ -91,11 -91,8 +91,11 @@@ static pthread_cond_t cond_result
  
  static int skip_first_line;
  
 -static void add_work(struct grep_opt *opt, const struct grep_source *gs)
 +static void add_work(struct grep_opt *opt, struct grep_source *gs)
  {
 +      if (opt->binary != GREP_BINARY_TEXT)
 +              grep_source_load_driver(gs, opt->repo->index);
 +
        grep_lock();
  
        while ((todo_end+1) % ARRAY_SIZE(todo) == todo_done) {
        }
  
        todo[todo_end].source = *gs;
 -      if (opt->binary != GREP_BINARY_TEXT)
 -              grep_source_load_driver(&todo[todo_end].source,
 -                                      opt->repo->index);
        todo[todo_end].done = 0;
        strbuf_reset(&todo[todo_end].out);
        todo_end = (todo_end + 1) % ARRAY_SIZE(todo);
@@@ -200,12 -200,12 +200,12 @@@ static void start_threads(struct grep_o
        int i;
  
        pthread_mutex_init(&grep_mutex, NULL);
 -      pthread_mutex_init(&grep_read_mutex, NULL);
        pthread_mutex_init(&grep_attr_mutex, NULL);
        pthread_cond_init(&cond_add, NULL);
        pthread_cond_init(&cond_write, NULL);
        pthread_cond_init(&cond_result, NULL);
        grep_use_locks = 1;
 +      enable_obj_read_lock();
  
        for (i = 0; i < ARRAY_SIZE(todo); i++) {
                strbuf_init(&todo[i].out, 0);
@@@ -257,12 -257,12 +257,12 @@@ static int wait_all(void
        free(threads);
  
        pthread_mutex_destroy(&grep_mutex);
 -      pthread_mutex_destroy(&grep_read_mutex);
        pthread_mutex_destroy(&grep_attr_mutex);
        pthread_cond_destroy(&cond_add);
        pthread_cond_destroy(&cond_write);
        pthread_cond_destroy(&cond_result);
        grep_use_locks = 0;
 +      disable_obj_read_lock();
  
        return hit;
  }
@@@ -295,36 -295,14 +295,36 @@@ static int grep_cmd_config(const char *
        return st;
  }
  
 -static void *lock_and_read_oid_file(const struct object_id *oid, enum object_type *type, unsigned long *size)
 +static void grep_source_name(struct grep_opt *opt, const char *filename,
 +                           int tree_name_len, struct strbuf *out)
  {
 -      void *data;
 +      strbuf_reset(out);
  
 -      grep_read_lock();
 -      data = read_object_file(oid, type, size);
 -      grep_read_unlock();
 -      return data;
 +      if (opt->null_following_name) {
 +              if (opt->relative && opt->prefix_length) {
 +                      struct strbuf rel_buf = STRBUF_INIT;
 +                      const char *rel_name =
 +                              relative_path(filename + tree_name_len,
 +                                            opt->prefix, &rel_buf);
 +
 +                      if (tree_name_len)
 +                              strbuf_add(out, filename, tree_name_len);
 +
 +                      strbuf_addstr(out, rel_name);
 +                      strbuf_release(&rel_buf);
 +              } else {
 +                      strbuf_addstr(out, filename);
 +              }
 +              return;
 +      }
 +
 +      if (opt->relative && opt->prefix_length)
 +              quote_path_relative(filename + tree_name_len, opt->prefix, out);
 +      else
 +              quote_c_style(filename + tree_name_len, out, NULL, 0);
 +
 +      if (tree_name_len)
 +              strbuf_insert(out, 0, filename, tree_name_len);
  }
  
  static int grep_oid(struct grep_opt *opt, const struct object_id *oid,
        struct strbuf pathbuf = STRBUF_INIT;
        struct grep_source gs;
  
 -      if (opt->relative && opt->prefix_length) {
 -              quote_path_relative(filename + tree_name_len, opt->prefix, &pathbuf);
 -              strbuf_insert(&pathbuf, 0, filename, tree_name_len);
 -      } else {
 -              strbuf_addstr(&pathbuf, filename);
 -      }
 -
 +      grep_source_name(opt, filename, tree_name_len, &pathbuf);
        grep_source_init(&gs, GREP_SOURCE_OID, pathbuf.buf, path, oid);
        strbuf_release(&pathbuf);
  
@@@ -360,7 -344,11 +360,7 @@@ static int grep_file(struct grep_opt *o
        struct strbuf buf = STRBUF_INIT;
        struct grep_source gs;
  
 -      if (opt->relative && opt->prefix_length)
 -              quote_path_relative(filename, opt->prefix, &buf);
 -      else
 -              strbuf_addstr(&buf, filename);
 -
 +      grep_source_name(opt, filename, 0, &buf);
        grep_source_init(&gs, GREP_SOURCE_FILE, buf.buf, filename, filename);
        strbuf_release(&buf);
  
@@@ -419,28 -407,30 +419,28 @@@ static int grep_submodule(struct grep_o
  {
        struct repository subrepo;
        struct repository *superproject = opt->repo;
 -      const struct submodule *sub = submodule_from_path(superproject,
 -                                                        &null_oid, path);
 +      const struct submodule *sub;
        struct grep_opt subopt;
        int hit;
  
 -      /*
 -       * NEEDSWORK: submodules functions need to be protected because they
 -       * access the object store via config_from_gitmodules(): the latter
 -       * uses get_oid() which, for now, relies on the global the_repository
 -       * object.
 -       */
 -      grep_read_lock();
 +      sub = submodule_from_path(superproject, &null_oid, path);
  
 -      if (!is_submodule_active(superproject, path)) {
 -              grep_read_unlock();
 +      if (!is_submodule_active(superproject, path))
                return 0;
 -      }
  
 -      if (repo_submodule_init(&subrepo, superproject, sub)) {
 -              grep_read_unlock();
 +      if (repo_submodule_init(&subrepo, superproject, sub))
                return 0;
 -      }
  
 -      repo_read_gitmodules(&subrepo);
 +      /*
 +       * NEEDSWORK: repo_read_gitmodules() might call
 +       * add_to_alternates_memory() via config_from_gitmodules(). This
 +       * operation causes a race condition with concurrent object readings
 +       * performed by the worker threads. That's why we need obj_read_lock()
 +       * here. It should be removed once it's no longer necessary to add the
 +       * subrepo's odbs to the in-memory alternates list.
 +       */
 +      obj_read_lock();
 +      repo_read_gitmodules(&subrepo, 0);
  
        /*
         * NEEDSWORK: This adds the submodule's object directory to the list of
         * object.
         */
        add_to_alternates_memory(subrepo.objects->odb->path);
 -      grep_read_unlock();
 +      obj_read_unlock();
  
        memcpy(&subopt, opt, sizeof(subopt));
        subopt.repo = &subrepo;
                unsigned long size;
                struct strbuf base = STRBUF_INIT;
  
 +              obj_read_lock();
                object = parse_object_or_die(oid, oid_to_hex(oid));
 -
 -              grep_read_lock();
 +              obj_read_unlock();
                data = read_object_with_reference(&subrepo,
                                                  &object->oid, tree_type,
                                                  &size, NULL);
 -              grep_read_unlock();
 -
                if (!data)
                        die(_("unable to read tree (%s)"), oid_to_hex(&object->oid));
  
@@@ -595,7 -587,7 +595,7 @@@ static int grep_tree(struct grep_opt *o
                        void *data;
                        unsigned long size;
  
 -                      data = lock_and_read_oid_file(&entry.oid, &type, &size);
 +                      data = read_object_file(&entry.oid, &type, &size);
                        if (!data)
                                die(_("unable to read tree (%s)"),
                                    oid_to_hex(&entry.oid));
@@@ -633,9 -625,12 +633,9 @@@ static int grep_object(struct grep_opt 
                struct strbuf base;
                int hit, len;
  
 -              grep_read_lock();
                data = read_object_with_reference(opt->repo,
                                                  &obj->oid, tree_type,
                                                  &size, NULL);
 -              grep_read_unlock();
 -
                if (!data)
                        die(_("unable to read tree (%s)"), oid_to_hex(&obj->oid));
  
@@@ -664,18 -659,13 +664,18 @@@ static int grep_objects(struct grep_op
  
        for (i = 0; i < nr; i++) {
                struct object *real_obj;
 +
 +              obj_read_lock();
                real_obj = deref_tag(opt->repo, list->objects[i].item,
                                     NULL, 0);
 +              obj_read_unlock();
  
                /* load the gitmodules file for this rev */
                if (recurse_submodules) {
                        submodule_free(opt->repo);
 +                      obj_read_lock();
                        gitmodules_config_oid(&real_obj->oid);
 +                      obj_read_unlock();
                }
                if (grep_object(opt, pathspec, real_obj, list->objects[i].name,
                                list->objects[i].path)) {
@@@ -701,8 -691,6 +701,6 @@@ static int grep_directory(struct grep_o
  
        fill_directory(&dir, opt->repo->index, pathspec);
        for (i = 0; i < dir.nr; i++) {
-               if (!dir_path_match(opt->repo->index, dir.entries[i], pathspec, 0, NULL))
-                       continue;
                hit |= grep_file(opt, dir.entries[i]->name);
                if (hit && opt->status_only)
                        break;
@@@ -1075,10 -1063,7 +1073,10 @@@ int cmd_grep(int argc, const char **arg
        pathspec.recursive = 1;
        pathspec.recurse_submodules = !!recurse_submodules;
  
 -      if (list.nr || cached || show_in_pager) {
 +      if (recurse_submodules && untracked)
 +              die(_("--untracked not supported with --recurse-submodules"));
 +
 +      if (show_in_pager) {
                if (num_threads > 1)
                        warning(_("invalid option combination, ignoring --threads"));
                num_threads = 1;
        } else if (num_threads < 0)
                die(_("invalid number of threads specified (%d)"), num_threads);
        else if (num_threads == 0)
 -              num_threads = HAVE_THREADS ? GREP_NUM_THREADS_DEFAULT : 1;
 +              num_threads = HAVE_THREADS ? online_cpus() : 1;
  
        if (num_threads > 1) {
                if (!HAVE_THREADS)
                    && (opt.pre_context || opt.post_context ||
                        opt.file_break || opt.funcbody))
                        skip_first_line = 1;
 +
 +              /*
 +               * Pre-read gitmodules (if not read already) and force eager
 +               * initialization of packed_git to prevent racy lazy
 +               * reading/initialization once worker threads are started.
 +               */
 +              if (recurse_submodules)
 +                      repo_read_gitmodules(the_repository, 1);
 +              if (startup_info->have_repository)
 +                      (void)get_packed_git(the_repository);
 +
                start_threads(&opt);
        } else {
                /*
                }
        }
  
 -      if (recurse_submodules && untracked)
 -              die(_("--untracked not supported with --recurse-submodules"));
 -
        if (!show_in_pager && !opt.status_only)
                setup_pager();
  
diff --combined builtin/stash.c
index a43a92ec7438c571c9322d5d206effb0ea07febb,704740b245cec1166f3761c2aefc3466931951b5..0c52a3b849c4c6f811995f3be89284052c2dd313
@@@ -27,7 -27,6 +27,7 @@@ static const char * const git_stash_usa
        N_("git stash clear"),
        N_("git stash [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n"
           "          [-u|--include-untracked] [-a|--all] [-m|--message <message>]\n"
 +         "          [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
           "          [--] [<pathspec>...]]"),
        N_("git stash save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n"
           "          [-u|--include-untracked] [-a|--all] [<message>]"),
@@@ -702,7 -701,6 +702,7 @@@ static int list_stash(int argc, const c
  
  static int show_stat = 1;
  static int show_patch;
 +static int use_legacy_stash;
  
  static int git_stash_config(const char *var, const char *value, void *cb)
  {
                show_patch = git_config_bool(var, value);
                return 0;
        }
 -      return git_default_config(var, value, cb);
 +      if (!strcmp(var, "stash.usebuiltin")) {
 +              use_legacy_stash = !git_config_bool(var, value);
 +              return 0;
 +      }
 +      return git_diff_basic_config(var, value, cb);
  }
  
  static int show_stash(int argc, const char **argv, const char *prefix)
         * any options.
         */
        if (revision_args.argc == 1) {
 -              git_config(git_stash_config, NULL);
                if (show_stat)
                        rev.diffopt.output_format = DIFF_FORMAT_DIFFSTAT;
  
@@@ -861,30 -856,23 +861,23 @@@ static int get_untracked_files(const st
                               struct strbuf *untracked_files)
  {
        int i;
-       int max_len;
        int found = 0;
-       char *seen;
        struct dir_struct dir;
  
        memset(&dir, 0, sizeof(dir));
        if (include_untracked != INCLUDE_ALL_FILES)
                setup_standard_excludes(&dir);
  
-       seen = xcalloc(ps->nr, 1);
-       max_len = fill_directory(&dir, the_repository->index, ps);
+       fill_directory(&dir, the_repository->index, ps);
        for (i = 0; i < dir.nr; i++) {
                struct dir_entry *ent = dir.entries[i];
-               if (dir_path_match(&the_index, ent, ps, max_len, seen)) {
-                       found++;
-                       strbuf_addstr(untracked_files, ent->name);
-                       /* NUL-terminate: will be fed to update-index -z */
-                       strbuf_addch(untracked_files, '\0');
-               }
+               found++;
+               strbuf_addstr(untracked_files, ent->name);
+               /* NUL-terminate: will be fed to update-index -z */
+               strbuf_addch(untracked_files, '\0');
                free(ent);
        }
  
-       free(seen);
        free(dir.entries);
        free(dir.ignored);
        clear_directory(&dir);
@@@ -1003,9 -991,9 +996,9 @@@ static int stash_patch(struct stash_inf
  {
        int ret = 0;
        struct child_process cp_read_tree = CHILD_PROCESS_INIT;
 -      struct child_process cp_add_i = CHILD_PROCESS_INIT;
        struct child_process cp_diff_tree = CHILD_PROCESS_INIT;
        struct index_state istate = { NULL };
 +      char *old_index_env = NULL, *old_repo_index_file;
  
        remove_path(stash_index_path.buf);
  
        }
  
        /* Find out what the user wants. */
 -      cp_add_i.git_cmd = 1;
 -      argv_array_pushl(&cp_add_i.args, "add--interactive", "--patch=stash",
 -                       "--", NULL);
 -      add_pathspecs(&cp_add_i.args, ps);
 -      argv_array_pushf(&cp_add_i.env_array, "GIT_INDEX_FILE=%s",
 -                       stash_index_path.buf);
 -      if (run_command(&cp_add_i)) {
 -              ret = -1;
 -              goto done;
 -      }
 +      old_repo_index_file = the_repository->index_file;
 +      the_repository->index_file = stash_index_path.buf;
 +      old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
 +      setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
 +
 +      ret = run_add_interactive(NULL, "--patch=stash", ps);
 +
 +      the_repository->index_file = old_repo_index_file;
 +      if (old_index_env && *old_index_env)
 +              setenv(INDEX_ENVIRONMENT, old_index_env, 1);
 +      else
 +              unsetenv(INDEX_ENVIRONMENT);
 +      FREE_AND_NULL(old_index_env);
  
        /* State of the working tree. */
        if (write_index_as_tree(&info->w_tree, &istate, stash_index_path.buf, 0,
        }
  
        cp_diff_tree.git_cmd = 1;
 -      argv_array_pushl(&cp_diff_tree.args, "diff-tree", "-p", "HEAD",
 +      argv_array_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD",
                         oid_to_hex(&info->w_tree), "--", NULL);
        if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) {
                ret = -1;
@@@ -1456,17 -1441,13 +1449,17 @@@ done
        return ret;
  }
  
 -static int push_stash(int argc, const char **argv, const char *prefix)
 +static int push_stash(int argc, const char **argv, const char *prefix,
 +                    int push_assumed)
  {
 +      int force_assume = 0;
        int keep_index = -1;
        int patch_mode = 0;
        int include_untracked = 0;
        int quiet = 0;
 +      int pathspec_file_nul = 0;
        const char *stash_msg = NULL;
 +      const char *pathspec_from_file = NULL;
        struct pathspec ps;
        struct option options[] = {
                OPT_BOOL('k', "keep-index", &keep_index,
                            N_("include ignore files"), 2),
                OPT_STRING('m', "message", &stash_msg, N_("message"),
                           N_("stash message")),
 +              OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
 +              OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),
                OPT_END()
        };
  
 -      if (argc)
 +      if (argc) {
 +              force_assume = !strcmp(argv[0], "-p");
                argc = parse_options(argc, argv, prefix, options,
                                     git_stash_push_usage,
 -                                   0);
 +                                   PARSE_OPT_KEEP_DASHDASH);
 +      }
 +
 +      if (argc) {
 +              if (!strcmp(argv[0], "--")) {
 +                      argc--;
 +                      argv++;
 +              } else if (push_assumed && !force_assume) {
 +                      die("subcommand wasn't specified; 'push' can't be assumed due to unexpected token '%s'",
 +                          argv[0]);
 +              }
 +      }
  
        parse_pathspec(&ps, 0, PATHSPEC_PREFER_FULL | PATHSPEC_PREFIX_ORIGIN,
                       prefix, argv);
 +
 +      if (pathspec_from_file) {
 +              if (patch_mode)
 +                      die(_("--pathspec-from-file is incompatible with --patch"));
 +
 +              if (ps.nr)
 +                      die(_("--pathspec-from-file is incompatible with pathspec arguments"));
 +
 +              parse_pathspec_file(&ps, 0,
 +                                  PATHSPEC_PREFER_FULL | PATHSPEC_PREFIX_ORIGIN,
 +                                  prefix, pathspec_from_file, pathspec_file_nul);
 +      } else if (pathspec_file_nul) {
 +              die(_("--pathspec-file-nul requires --pathspec-from-file"));
 +      }
 +
        return do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
                             include_untracked);
  }
@@@ -1563,8 -1515,32 +1556,8 @@@ static int save_stash(int argc, const c
        return ret;
  }
  
 -static int use_builtin_stash(void)
 -{
 -      struct child_process cp = CHILD_PROCESS_INIT;
 -      struct strbuf out = STRBUF_INIT;
 -      int ret, env = git_env_bool("GIT_TEST_STASH_USE_BUILTIN", -1);
 -
 -      if (env != -1)
 -              return env;
 -
 -      argv_array_pushl(&cp.args,
 -                       "config", "--bool", "stash.usebuiltin", NULL);
 -      cp.git_cmd = 1;
 -      if (capture_command(&cp, &out, 6)) {
 -              strbuf_release(&out);
 -              return 1;
 -      }
 -
 -      strbuf_trim(&out);
 -      ret = !strcmp("true", out.buf);
 -      strbuf_release(&out);
 -      return ret;
 -}
 -
  int cmd_stash(int argc, const char **argv, const char *prefix)
  {
 -      int i = -1;
        pid_t pid = getpid();
        const char *index_file;
        struct argv_array args = ARGV_ARRAY_INIT;
                OPT_END()
        };
  
 -      if (!use_builtin_stash()) {
 -              const char *path = mkpath("%s/git-legacy-stash",
 -                                        git_exec_path());
 -
 -              if (sane_execvp(path, (char **)argv) < 0)
 -                      die_errno(_("could not exec %s"), path);
 -              else
 -                      BUG("sane_execvp() returned???");
 -      }
 +      git_config(git_stash_config, NULL);
  
 -      prefix = setup_git_directory();
 -      trace_repo_setup(prefix);
 -      setup_work_tree();
 -
 -      git_config(git_diff_basic_config, NULL);
 +      if (use_legacy_stash ||
 +          !git_env_bool("GIT_TEST_STASH_USE_BUILTIN", -1))
 +              warning(_("the stash.useBuiltin support has been removed!\n"
 +                        "See its entry in 'git help config' for details."));
  
        argc = parse_options(argc, argv, prefix, options, git_stash_usage,
                             PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);
                    (uintmax_t)pid);
  
        if (!argc)
 -              return !!push_stash(0, NULL, prefix);
 +              return !!push_stash(0, NULL, prefix, 0);
        else if (!strcmp(argv[0], "apply"))
                return !!apply_stash(argc, argv, prefix);
        else if (!strcmp(argv[0], "clear"))
        else if (!strcmp(argv[0], "create"))
                return !!create_stash(argc, argv, prefix);
        else if (!strcmp(argv[0], "push"))
 -              return !!push_stash(argc, argv, prefix);
 +              return !!push_stash(argc, argv, prefix, 0);
        else if (!strcmp(argv[0], "save"))
                return !!save_stash(argc, argv, prefix);
        else if (*argv[0] != '-')
                usage_msg_opt(xstrfmt(_("unknown subcommand: %s"), argv[0]),
                              git_stash_usage, options);
  
 -      if (strcmp(argv[0], "-p")) {
 -              while (++i < argc && strcmp(argv[i], "--")) {
 -                      /*
 -                       * `akpqu` is a string which contains all short options,
 -                       * except `-m` which is verified separately.
 -                       */
 -                      if ((strlen(argv[i]) == 2) && *argv[i] == '-' &&
 -                          strchr("akpqu", argv[i][1]))
 -                              continue;
 -
 -                      if (!strcmp(argv[i], "--all") ||
 -                          !strcmp(argv[i], "--keep-index") ||
 -                          !strcmp(argv[i], "--no-keep-index") ||
 -                          !strcmp(argv[i], "--patch") ||
 -                          !strcmp(argv[i], "--quiet") ||
 -                          !strcmp(argv[i], "--include-untracked"))
 -                              continue;
 -
 -                      /*
 -                       * `-m` and `--message=` are verified separately because
 -                       * they need to be immediately followed by a string
 -                       * (i.e.`-m"foobar"` or `--message="foobar"`).
 -                       */
 -                      if (starts_with(argv[i], "-m") ||
 -                          starts_with(argv[i], "--message="))
 -                              continue;
 -
 -                      usage_with_options(git_stash_usage, options);
 -              }
 -      }
 -
 +      /* Assume 'stash push' */
        argv_array_push(&args, "push");
        argv_array_pushv(&args, argv);
 -      return !!push_stash(args.argc, args.argv, prefix);
 +      return !!push_stash(args.argc, args.argv, prefix, 1);
  }
index c21786f2fd00263e9b068f1d4750fa16b2411225,1032b6422973d92522c6deffeec6b618a50b7776..b1d6e5ebed7c1839770f03f39157141b4edbefd8
@@@ -504,7 -504,7 +504,7 @@@ __git_index_files (
  {
        local root="$2" match="$3"
  
-       __git_ls_files_helper "$root" "$1" "$match" |
+       __git_ls_files_helper "$root" "$1" "${match:-?}" |
        awk -F / -v pfx="${2//\\/\\\\}" '{
                paths[$1] = 1
        }
@@@ -1069,32 -1069,15 +1069,32 @@@ __git_aliased_command (
        done
  }
  
 -# __git_find_on_cmdline requires 1 argument
 +# Check whether one of the given words is present on the command line,
 +# and print the first word found.
 +#
 +# Usage: __git_find_on_cmdline [<option>]... "<wordlist>"
 +# --show-idx: Optionally show the index of the found word in the $words array.
  __git_find_on_cmdline ()
  {
 -      local word subcommand c=1
 +      local word c=1 show_idx
 +
 +      while test $# -gt 1; do
 +              case "$1" in
 +              --show-idx)     show_idx=y ;;
 +              *)              return 1 ;;
 +              esac
 +              shift
 +      done
 +      local wordlist="$1"
 +
        while [ $c -lt $cword ]; do
 -              word="${words[c]}"
 -              for subcommand in $1; do
 -                      if [ "$subcommand" = "$word" ]; then
 -                              echo "$subcommand"
 +              for word in $wordlist; do
 +                      if [ "$word" = "${words[c]}" ]; then
 +                              if [ -n "$show_idx" ]; then
 +                                      echo "$c $word"
 +                              else
 +                                      echo "$word"
 +                              fi
                                return
                        fi
                done
@@@ -1197,7 -1180,6 +1197,7 @@@ __git_count_arguments (
  
  __git_whitespacelist="nowarn warn error error-all fix"
  __git_patchformat="mbox stgit stgit-series hg mboxrd"
 +__git_showcurrentpatch="diff raw"
  __git_am_inprogress_options="--skip --continue --resolved --abort --quit --show-current-patch"
  
  _git_am ()
                __gitcomp "$__git_patchformat" "" "${cur##--patch-format=}"
                return
                ;;
 +      --show-current-patch=*)
 +              __gitcomp "$__git_showcurrentpatch" "" "${cur##--show-current-patch=}"
 +              return
 +              ;;
        --*)
                __gitcomp_builtin am "" \
                        "$__git_am_inprogress_options"
@@@ -1492,16 -1470,9 +1492,16 @@@ __git_diff_algorithms="myers minimal pa
  
  __git_diff_submodule_formats="diff log short"
  
 +__git_color_moved_opts="no default plain blocks zebra dimmed-zebra"
 +
 +__git_color_moved_ws_opts="no ignore-space-at-eol ignore-space-change
 +                      ignore-all-space allow-indentation-change"
 +
  __git_diff_common_options="--stat --numstat --shortstat --summary
                        --patch-with-stat --name-only --name-status --color
                        --no-color --color-words --no-renames --check
 +                      --color-moved --color-moved= --no-color-moved
 +                      --color-moved-ws= --no-color-moved-ws
                        --full-index --binary --abbrev --diff-filter=
                        --find-copies-harder --ignore-cr-at-eol
                        --text --ignore-space-at-eol --ignore-space-change
@@@ -1532,14 -1503,6 +1532,14 @@@ _git_diff (
                __gitcomp "$__git_diff_submodule_formats" "" "${cur##--submodule=}"
                return
                ;;
 +      --color-moved=*)
 +              __gitcomp "$__git_color_moved_opts" "" "${cur##--color-moved=}"
 +              return
 +              ;;
 +      --color-moved-ws=*)
 +              __gitcomp "$__git_color_moved_ws_opts" "" "${cur##--color-moved-ws=}"
 +              return
 +              ;;
        --*)
                __gitcomp "--cached --staged --pickaxe-all --pickaxe-regex
                        --base --ours --theirs --no-index
@@@ -2755,27 -2718,6 +2755,27 @@@ _git_show_branch (
        __git_complete_revlist
  }
  
 +_git_sparse_checkout ()
 +{
 +      local subcommands="list init set disable"
 +      local subcommand="$(__git_find_on_cmdline "$subcommands")"
 +      if [ -z "$subcommand" ]; then
 +              __gitcomp "$subcommands"
 +              return
 +      fi
 +
 +      case "$subcommand,$cur" in
 +      init,--*)
 +              __gitcomp "--cone"
 +              ;;
 +      set,--*)
 +              __gitcomp "--stdin"
 +              ;;
 +      *)
 +              ;;
 +      esac
 +}
 +
  _git_stash ()
  {
        local save_opts='--all --keep-index --no-keep-index --quiet --patch --include-untracked'
@@@ -3027,83 -2969,33 +3027,83 @@@ _git_whatchanged (
        _git_log
  }
  
 +__git_complete_worktree_paths ()
 +{
 +      local IFS=$'\n'
 +      __gitcomp_nl "$(git worktree list --porcelain |
 +              # Skip the first entry: it's the path of the main worktree,
 +              # which can't be moved, removed, locked, etc.
 +              sed -n -e '2,$ s/^worktree //p')"
 +}
 +
  _git_worktree ()
  {
        local subcommands="add list lock move prune remove unlock"
 -      local subcommand="$(__git_find_on_cmdline "$subcommands")"
 -      if [ -z "$subcommand" ]; then
 +      local subcommand subcommand_idx
 +
 +      subcommand="$(__git_find_on_cmdline --show-idx "$subcommands")"
 +      subcommand_idx="${subcommand% *}"
 +      subcommand="${subcommand#* }"
 +
 +      case "$subcommand,$cur" in
 +      ,*)
                __gitcomp "$subcommands"
 -      else
 -              case "$subcommand,$cur" in
 -              add,--*)
 -                      __gitcomp_builtin worktree_add
 -                      ;;
 -              list,--*)
 -                      __gitcomp_builtin worktree_list
 -                      ;;
 -              lock,--*)
 -                      __gitcomp_builtin worktree_lock
 -                      ;;
 -              prune,--*)
 -                      __gitcomp_builtin worktree_prune
 +              ;;
 +      *,--*)
 +              __gitcomp_builtin worktree_$subcommand
 +              ;;
 +      add,*)  # usage: git worktree add [<options>] <path> [<commit-ish>]
 +              # Here we are not completing an --option, it's either the
 +              # path or a ref.
 +              case "$prev" in
 +              -b|-B)  # Complete refs for branch to be created/reseted.
 +                      __git_complete_refs
                        ;;
 -              remove,--*)
 -                      __gitcomp "--force"
 +              -*)     # The previous word is an -o|--option without an
 +                      # unstuck argument: have to complete the path for
 +                      # the new worktree, so don't list anything, but let
 +                      # Bash fall back to filename completion.
                        ;;
 -              *)
 +              *)      # The previous word is not an --option, so it must
 +                      # be either the 'add' subcommand, the unstuck
 +                      # argument of an option (e.g. branch for -b|-B), or
 +                      # the path for the new worktree.
 +                      if [ $cword -eq $((subcommand_idx+1)) ]; then
 +                              # Right after the 'add' subcommand: have to
 +                              # complete the path, so fall back to Bash
 +                              # filename completion.
 +                              :
 +                      else
 +                              case "${words[cword-2]}" in
 +                              -b|-B)  # After '-b <branch>': have to
 +                                      # complete the path, so fall back
 +                                      # to Bash filename completion.
 +                                      ;;
 +                              *)      # After the path: have to complete
 +                                      # the ref to be checked out.
 +                                      __git_complete_refs
 +                                      ;;
 +                              esac
 +                      fi
                        ;;
                esac
 -      fi
 +              ;;
 +      lock,*|remove,*|unlock,*)
 +              __git_complete_worktree_paths
 +              ;;
 +      move,*)
 +              if [ $cword -eq $((subcommand_idx+1)) ]; then
 +                      # The first parameter must be an existing working
 +                      # tree to be moved.
 +                      __git_complete_worktree_paths
 +              else
 +                      # The second parameter is the destination: it could
 +                      # be any path, so don't list anything, but let Bash
 +                      # fall back to filename completion.
 +                      :
 +              fi
 +              ;;
 +      esac
  }
  
  __git_complete_common () {
diff --combined dir.c
index 0ffb1b3302452c2cce0bdaa55e5259d9be168b63,2de64910401540455217b1e5df6ffbdc9b31a295..d97e9558489d3bf622673b1f5b90053154ea9033
--- 1/dir.c
--- 2/dir.c
+++ b/dir.c
@@@ -636,42 -636,11 +636,42 @@@ int pl_hashmap_cmp(const void *unused_c
        return strncmp(ee1->pattern, ee2->pattern, min_len);
  }
  
 +static char *dup_and_filter_pattern(const char *pattern)
 +{
 +      char *set, *read;
 +      size_t count  = 0;
 +      char *result = xstrdup(pattern);
 +
 +      set = result;
 +      read = result;
 +
 +      while (*read) {
 +              /* skip escape characters (once) */
 +              if (*read == '\\')
 +                      read++;
 +
 +              *set = *read;
 +
 +              set++;
 +              read++;
 +              count++;
 +      }
 +      *set = 0;
 +
 +      if (count > 2 &&
 +          *(set - 1) == '*' &&
 +          *(set - 2) == '/')
 +              *(set - 2) = 0;
 +
 +      return result;
 +}
 +
  static void add_pattern_to_hashsets(struct pattern_list *pl, struct path_pattern *given)
  {
        struct pattern_entry *translated;
        char *truncated;
        char *data = NULL;
 +      const char *prev, *cur, *next;
  
        if (!pl->use_cone_patterns)
                return;
                return;
        }
  
 +      if (given->patternlen < 2 ||
 +          *given->pattern == '*' ||
 +          strstr(given->pattern, "**")) {
 +              /* Not a cone pattern. */
 +              warning(_("unrecognized pattern: '%s'"), given->pattern);
 +              goto clear_hashmaps;
 +      }
 +
 +      prev = given->pattern;
 +      cur = given->pattern + 1;
 +      next = given->pattern + 2;
 +
 +      while (*cur) {
 +              /* Watch for glob characters '*', '\', '[', '?' */
 +              if (!is_glob_special(*cur))
 +                      goto increment;
 +
 +              /* But only if *prev != '\\' */
 +              if (*prev == '\\')
 +                      goto increment;
 +
 +              /* But allow the initial '\' */
 +              if (*cur == '\\' &&
 +                  is_glob_special(*next))
 +                      goto increment;
 +
 +              /* But a trailing '/' then '*' is fine */
 +              if (*prev == '/' &&
 +                  *cur == '*' &&
 +                  *next == 0)
 +                      goto increment;
 +
 +              /* Not a cone pattern. */
 +              warning(_("unrecognized pattern: '%s'"), given->pattern);
 +              goto clear_hashmaps;
 +
 +      increment:
 +              prev++;
 +              cur++;
 +              next++;
 +      }
 +
        if (given->patternlen > 2 &&
            !strcmp(given->pattern + given->patternlen - 2, "/*")) {
                if (!(given->flags & PATTERN_FLAG_NEGATIVE)) {
                        /* Not a cone pattern. */
 -                      pl->use_cone_patterns = 0;
                        warning(_("unrecognized pattern: '%s'"), given->pattern);
                        goto clear_hashmaps;
                }
  
 -              truncated = xstrdup(given->pattern);
 -              truncated[given->patternlen - 2] = 0;
 +              truncated = dup_and_filter_pattern(given->pattern);
  
                translated = xmalloc(sizeof(struct pattern_entry));
                translated->pattern = truncated;
  
        translated = xmalloc(sizeof(struct pattern_entry));
  
 -      translated->pattern = xstrdup(given->pattern);
 +      translated->pattern = dup_and_filter_pattern(given->pattern);
        translated->patternlen = given->patternlen;
        hashmap_entry_init(&translated->ent,
                           ignore_case ?
@@@ -1074,8 -1003,8 +1074,8 @@@ static int add_patterns(const char *fna
                                oidcpy(&oid_stat->oid,
                                       &istate->cache[pos]->oid);
                        else
 -                              hash_object_file(buf, size, "blob",
 -                                               &oid_stat->oid);
 +                              hash_object_file(the_hash_algo, buf, size,
 +                                               "blob", &oid_stat->oid);
                        fill_stat_data(&oid_stat->stat, &st);
                        oid_stat->valid = 1;
                }
@@@ -1727,36 -1656,59 +1727,59 @@@ static enum exist_status directory_exis
  static enum path_treatment treat_directory(struct dir_struct *dir,
        struct index_state *istate,
        struct untracked_cache_dir *untracked,
-       const char *dirname, int len, int baselen, int exclude,
+       const char *dirname, int len, int baselen, int excluded,
        const struct pathspec *pathspec)
  {
-       int nested_repo = 0;
+       /*
+        * WARNING: From this function, you can return path_recurse or you
+        *          can call read_directory_recursive() (or neither), but
+        *          you CAN'T DO BOTH.
+        */
+       enum path_treatment state;
+       int matches_how = 0;
+       int nested_repo = 0, check_only, stop_early;
+       int old_ignored_nr, old_untracked_nr;
        /* The "len-1" is to strip the final '/' */
-       switch (directory_exists_in_index(istate, dirname, len-1)) {
-       case index_directory:
-               return path_recurse;
+       enum exist_status status = directory_exists_in_index(istate, dirname, len-1);
  
-       case index_gitdir:
+       if (status == index_directory)
+               return path_recurse;
+       if (status == index_gitdir)
                return path_none;
+       if (status != index_nonexistent)
+               BUG("Unhandled value for directory_exists_in_index: %d\n", status);
  
-       case index_nonexistent:
-               if ((dir->flags & DIR_SKIP_NESTED_GIT) ||
-                   !(dir->flags & DIR_NO_GITLINKS)) {
-                       struct strbuf sb = STRBUF_INIT;
-                       strbuf_addstr(&sb, dirname);
-                       nested_repo = is_nonbare_repository_dir(&sb);
-                       strbuf_release(&sb);
-               }
-               if (nested_repo)
-                       return ((dir->flags & DIR_SKIP_NESTED_GIT) ? path_none :
-                               (exclude ? path_excluded : path_untracked));
+       /*
+        * We don't want to descend into paths that don't match the necessary
+        * patterns.  Clearly, if we don't have a pathspec, then we can't check
+        * for matching patterns.  Also, if (excluded) then we know we matched
+        * the exclusion patterns so as an optimization we can skip checking
+        * for matching patterns.
+        */
+       if (pathspec && !excluded) {
+               matches_how = do_match_pathspec(istate, pathspec, dirname, len,
+                                               0 /* prefix */, NULL /* seen */,
+                                               DO_MATCH_LEADING_PATHSPEC);
+               if (!matches_how)
+                       return path_none;
+       }
  
-               if (dir->flags & DIR_SHOW_OTHER_DIRECTORIES)
-                       break;
-               if (exclude &&
-                       (dir->flags & DIR_SHOW_IGNORED_TOO) &&
-                       (dir->flags & DIR_SHOW_IGNORED_TOO_MODE_MATCHING)) {
+       if ((dir->flags & DIR_SKIP_NESTED_GIT) ||
+               !(dir->flags & DIR_NO_GITLINKS)) {
+               struct strbuf sb = STRBUF_INIT;
+               strbuf_addstr(&sb, dirname);
+               nested_repo = is_nonbare_repository_dir(&sb);
+               strbuf_release(&sb);
+       }
+       if (nested_repo)
+               return ((dir->flags & DIR_SKIP_NESTED_GIT) ? path_none :
+                       (excluded ? path_excluded : path_untracked));
+       if (!(dir->flags & DIR_SHOW_OTHER_DIRECTORIES)) {
+               if (excluded &&
+                   (dir->flags & DIR_SHOW_IGNORED_TOO) &&
+                   (dir->flags & DIR_SHOW_IGNORED_TOO_MODE_MATCHING)) {
  
                        /*
                         * This is an excluded directory and we are
  
        /* This is the "show_other_directories" case */
  
-       if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
-               return exclude ? path_excluded : path_untracked;
+       /*
+        * If we have a pathspec which could match something _below_ this
+        * directory (e.g. when checking 'subdir/' having a pathspec like
+        * 'subdir/some/deep/path/file' or 'subdir/widget-*.c'), then we
+        * need to recurse.
+        */
+       if (matches_how == MATCHED_RECURSIVELY_LEADING_PATHSPEC)
+               return path_recurse;
+       /*
+        * Other than the path_recurse case immediately above, we only need
+        * to recurse into untracked/ignored directories if either of the
+        * following bits is set:
+        *   - DIR_SHOW_IGNORED_TOO (because then we need to determine if
+        *                           there are ignored directories below)
+        *   - DIR_HIDE_EMPTY_DIRECTORIES (because we have to determine if
+        *                                 the directory is empty)
+        */
+       if (!(dir->flags & (DIR_SHOW_IGNORED_TOO | DIR_HIDE_EMPTY_DIRECTORIES)))
+               return excluded ? path_excluded : path_untracked;
  
+       /*
+        * ...and even if DIR_SHOW_IGNORED_TOO is set, we can still avoid
+        * recursing into ignored directories if the path is excluded and
+        * DIR_SHOW_IGNORED_TOO_MODE_MATCHING is also set.
+        */
+       if (excluded &&
+           (dir->flags & DIR_SHOW_IGNORED_TOO) &&
+           (dir->flags & DIR_SHOW_IGNORED_TOO_MODE_MATCHING))
+               return path_excluded;
+       /*
+        * If we have we don't want to know the all the paths under an
+        * untracked or ignored directory, we still need to go into the
+        * directory to determine if it is empty (because an empty directory
+        * should be path_none instead of path_excluded or path_untracked).
+        */
+       check_only = ((dir->flags & DIR_HIDE_EMPTY_DIRECTORIES) &&
+                     !(dir->flags & DIR_SHOW_IGNORED_TOO));
+       /*
+        * However, there's another optimization possible as a subset of
+        * check_only, based on the cases we have to consider:
+        *   A) Directory matches no exclude patterns:
+        *     * Directory is empty => path_none
+        *     * Directory has an untracked file under it => path_untracked
+        *     * Directory has only ignored files under it => path_excluded
+        *   B) Directory matches an exclude pattern:
+        *     * Directory is empty => path_none
+        *     * Directory has an untracked file under it => path_excluded
+        *     * Directory has only ignored files under it => path_excluded
+        * In case A, we can exit as soon as we've found an untracked
+        * file but otherwise have to walk all files.  In case B, though,
+        * we can stop at the first file we find under the directory.
+        */
+       stop_early = check_only && excluded;
+       /*
+        * If /every/ file within an untracked directory is ignored, then
+        * we want to treat the directory as ignored (for e.g. status
+        * --porcelain), without listing the individual ignored files
+        * underneath.  To do so, we'll save the current ignored_nr, and
+        * pop all the ones added after it if it turns out the entire
+        * directory is ignored.  Also, when DIR_SHOW_IGNORED_TOO and
+        * !DIR_KEEP_UNTRACKED_CONTENTS then we don't want to show
+        * untracked paths so will need to pop all those off the last
+        * after we traverse.
+        */
+       old_ignored_nr = dir->ignored_nr;
+       old_untracked_nr = dir->nr;
+       /* Actually recurse into dirname now, we'll fixup the state later. */
        untracked = lookup_untracked(dir->untracked, untracked,
                                     dirname + baselen, len - baselen);
+       state = read_directory_recursive(dir, istate, dirname, len, untracked,
+                                        check_only, stop_early, pathspec);
+       /* There are a variety of reasons we may need to fixup the state... */
+       if (state == path_excluded) {
+               /* state == path_excluded implies all paths under
+                * dirname were ignored...
+                *
+                * if running e.g. `git status --porcelain --ignored=matching`,
+                * then we want to see the subpaths that are ignored.
+                *
+                * if running e.g. just `git status --porcelain`, then
+                * we just want the directory itself to be listed as ignored
+                * and not the individual paths underneath.
+                */
+               int want_ignored_subpaths =
+                       ((dir->flags & DIR_SHOW_IGNORED_TOO) &&
+                        (dir->flags & DIR_SHOW_IGNORED_TOO_MODE_MATCHING));
+               if (want_ignored_subpaths) {
+                       /*
+                        * with --ignored=matching, we want the subpaths
+                        * INSTEAD of the directory itself.
+                        */
+                       state = path_none;
+               } else {
+                       int i;
+                       for (i = old_ignored_nr + 1; i<dir->ignored_nr; ++i)
+                               FREE_AND_NULL(dir->ignored[i]);
+                       dir->ignored_nr = old_ignored_nr;
+               }
+       }
  
        /*
-        * If this is an excluded directory, then we only need to check if
-        * the directory contains any files.
+        * We may need to ignore some of the untracked paths we found while
+        * traversing subdirectories.
         */
-       return read_directory_recursive(dir, istate, dirname, len,
-                                       untracked, 1, exclude, pathspec);
+       if ((dir->flags & DIR_SHOW_IGNORED_TOO) &&
+           !(dir->flags & DIR_KEEP_UNTRACKED_CONTENTS)) {
+               int i;
+               for (i = old_untracked_nr + 1; i<dir->nr; ++i)
+                       FREE_AND_NULL(dir->entries[i]);
+               dir->nr = old_untracked_nr;
+       }
+       /*
+        * If there is nothing under the current directory and we are not
+        * hiding empty directories, then we need to report on the
+        * untracked or ignored status of the directory itself.
+        */
+       if (state == path_none && !(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
+               state = excluded ? path_excluded : path_untracked;
+       return state;
  }
  
  /*
@@@ -1934,85 -2002,6 +2073,6 @@@ static int resolve_dtype(int dtype, str
        return dtype;
  }
  
- static enum path_treatment treat_one_path(struct dir_struct *dir,
-                                         struct untracked_cache_dir *untracked,
-                                         struct index_state *istate,
-                                         struct strbuf *path,
-                                         int baselen,
-                                         const struct pathspec *pathspec,
-                                         int dtype)
- {
-       int exclude;
-       int has_path_in_index = !!index_file_exists(istate, path->buf, path->len, ignore_case);
-       enum path_treatment path_treatment;
-       dtype = resolve_dtype(dtype, istate, path->buf, path->len);
-       /* Always exclude indexed files */
-       if (dtype != DT_DIR && has_path_in_index)
-               return path_none;
-       /*
-        * When we are looking at a directory P in the working tree,
-        * there are three cases:
-        *
-        * (1) P exists in the index.  Everything inside the directory P in
-        * the working tree needs to go when P is checked out from the
-        * index.
-        *
-        * (2) P does not exist in the index, but there is P/Q in the index.
-        * We know P will stay a directory when we check out the contents
-        * of the index, but we do not know yet if there is a directory
-        * P/Q in the working tree to be killed, so we need to recurse.
-        *
-        * (3) P does not exist in the index, and there is no P/Q in the index
-        * to require P to be a directory, either.  Only in this case, we
-        * know that everything inside P will not be killed without
-        * recursing.
-        */
-       if ((dir->flags & DIR_COLLECT_KILLED_ONLY) &&
-           (dtype == DT_DIR) &&
-           !has_path_in_index &&
-           (directory_exists_in_index(istate, path->buf, path->len) == index_nonexistent))
-               return path_none;
-       exclude = is_excluded(dir, istate, path->buf, &dtype);
-       /*
-        * Excluded? If we don't explicitly want to show
-        * ignored files, ignore it
-        */
-       if (exclude && !(dir->flags & (DIR_SHOW_IGNORED|DIR_SHOW_IGNORED_TOO)))
-               return path_excluded;
-       switch (dtype) {
-       default:
-               return path_none;
-       case DT_DIR:
-               strbuf_addch(path, '/');
-               path_treatment = treat_directory(dir, istate, untracked,
-                                                path->buf, path->len,
-                                                baselen, exclude, pathspec);
-               /*
-                * If 1) we only want to return directories that
-                * match an exclude pattern and 2) this directory does
-                * not match an exclude pattern but all of its
-                * contents are excluded, then indicate that we should
-                * recurse into this directory (instead of marking the
-                * directory itself as an ignored path).
-                */
-               if (!exclude &&
-                   path_treatment == path_excluded &&
-                   (dir->flags & DIR_SHOW_IGNORED_TOO) &&
-                   (dir->flags & DIR_SHOW_IGNORED_TOO_MODE_MATCHING))
-                       return path_recurse;
-               return path_treatment;
-       case DT_REG:
-       case DT_LNK:
-               return exclude ? path_excluded : path_untracked;
-       }
- }
  static enum path_treatment treat_path_fast(struct dir_struct *dir,
                                           struct untracked_cache_dir *untracked,
                                           struct cached_dir *cdir,
                                           int baselen,
                                           const struct pathspec *pathspec)
  {
+       /*
+        * WARNING: From this function, you can return path_recurse or you
+        *          can call read_directory_recursive() (or neither), but
+        *          you CAN'T DO BOTH.
+        */
        strbuf_setlen(path, baselen);
        if (!cdir->ucd) {
                strbuf_addstr(path, cdir->file);
@@@ -2054,6 -2048,8 +2119,8 @@@ static enum path_treatment treat_path(s
                                      int baselen,
                                      const struct pathspec *pathspec)
  {
+       int has_path_in_index, dtype, excluded;
        if (!cdir->d_name)
                return treat_path_fast(dir, untracked, cdir, istate, path,
                                       baselen, pathspec);
        if (simplify_away(path->buf, path->len, pathspec))
                return path_none;
  
-       return treat_one_path(dir, untracked, istate, path, baselen, pathspec,
-                             cdir->d_type);
+       dtype = resolve_dtype(cdir->d_type, istate, path->buf, path->len);
+       /* Always exclude indexed files */
+       has_path_in_index = !!index_file_exists(istate, path->buf, path->len,
+                                               ignore_case);
+       if (dtype != DT_DIR && has_path_in_index)
+               return path_none;
+       /*
+        * When we are looking at a directory P in the working tree,
+        * there are three cases:
+        *
+        * (1) P exists in the index.  Everything inside the directory P in
+        * the working tree needs to go when P is checked out from the
+        * index.
+        *
+        * (2) P does not exist in the index, but there is P/Q in the index.
+        * We know P will stay a directory when we check out the contents
+        * of the index, but we do not know yet if there is a directory
+        * P/Q in the working tree to be killed, so we need to recurse.
+        *
+        * (3) P does not exist in the index, and there is no P/Q in the index
+        * to require P to be a directory, either.  Only in this case, we
+        * know that everything inside P will not be killed without
+        * recursing.
+        */
+       if ((dir->flags & DIR_COLLECT_KILLED_ONLY) &&
+           (dtype == DT_DIR) &&
+           !has_path_in_index &&
+           (directory_exists_in_index(istate, path->buf, path->len) == index_nonexistent))
+               return path_none;
+       excluded = is_excluded(dir, istate, path->buf, &dtype);
+       /*
+        * Excluded? If we don't explicitly want to show
+        * ignored files, ignore it
+        */
+       if (excluded && !(dir->flags & (DIR_SHOW_IGNORED|DIR_SHOW_IGNORED_TOO)))
+               return path_excluded;
+       switch (dtype) {
+       default:
+               return path_none;
+       case DT_DIR:
+               /*
+                * WARNING: Do not ignore/amend the return value from
+                * treat_directory(), and especially do not change it to return
+                * path_recurse as that can cause exponential slowdown.
+                * Instead, modify treat_directory() to return the right value.
+                */
+               strbuf_addch(path, '/');
+               return treat_directory(dir, istate, untracked,
+                                      path->buf, path->len,
+                                      baselen, excluded, pathspec);
+       case DT_REG:
+       case DT_LNK:
+               if (excluded)
+                       return path_excluded;
+               if (pathspec &&
+                   !do_match_pathspec(istate, pathspec, path->buf, path->len,
+                                      0 /* prefix */, NULL /* seen */,
+                                      0 /* flags */))
+                       return path_none;
+               return path_untracked;
+       }
  }
  
  static void add_untracked(struct untracked_cache_dir *dir, const char *name)
@@@ -2245,7 -2305,7 +2376,7 @@@ static void add_path_to_appropriate_res
   * If 'stop_at_first_file' is specified, 'path_excluded' is returned
   * to signal that a file was found. This is the least significant value that
   * indicates that a file was encountered that does not depend on the order of
-  * whether an untracked or exluded path was encountered first.
+  * whether an untracked or excluded path was encountered first.
   *
   * Returns the most significant path_treatment value encountered in the scan.
   * If 'stop_at_first_file' is specified, `path_excluded` is the most
@@@ -2258,14 -2318,10 +2389,10 @@@ static enum path_treatment read_directo
        int stop_at_first_file, const struct pathspec *pathspec)
  {
        /*
-        * WARNING WARNING WARNING:
-        *
-        * Any updates to the traversal logic here may need corresponding
-        * updates in treat_leading_path().  See the commit message for the
-        * commit adding this warning as well as the commit preceding it
-        * for details.
+        * WARNING: Do NOT recurse unless path_recurse is returned from
+        *          treat_path().  Recursing on any other return value
+        *          can result in exponential slowdown.
         */
        struct cached_dir cdir;
        enum path_treatment state, subdir_state, dir_state = path_none;
        struct strbuf path = STRBUF_INIT;
                        dir_state = state;
  
                /* recurse into subdir if instructed by treat_path */
-               if ((state == path_recurse) ||
-                       ((state == path_untracked) &&
-                        (resolve_dtype(cdir.d_type, istate, path.buf, path.len) == DT_DIR) &&
-                        ((dir->flags & DIR_SHOW_IGNORED_TOO) ||
-                         (pathspec &&
-                          do_match_pathspec(istate, pathspec, path.buf, path.len,
-                                            baselen, NULL, DO_MATCH_LEADING_PATHSPEC) == MATCHED_RECURSIVELY_LEADING_PATHSPEC)))) {
+               if (state == path_recurse) {
                        struct untracked_cache_dir *ud;
                        ud = lookup_untracked(dir->untracked, untracked,
                                              path.buf + baselen,
                                        add_untracked(untracked, path.buf + baselen);
                                break;
                        }
-                       /* skip the dir_add_* part */
+                       /* skip the add_path_to_appropriate_result_list() */
                        continue;
                }
  
@@@ -2377,15 -2427,6 +2498,6 @@@ static int treat_leading_path(struct di
                              const char *path, int len,
                              const struct pathspec *pathspec)
  {
-       /*
-        * WARNING WARNING WARNING:
-        *
-        * Any updates to the traversal logic here may need corresponding
-        * updates in read_directory_recursive().  See 777b420347 (dir:
-        * synchronize treat_leading_path() and read_directory_recursive(),
-        * 2019-12-19) and its parent commit for details.
-        */
        struct strbuf sb = STRBUF_INIT;
        struct strbuf subdir = STRBUF_INIT;
        int prevlen, baselen;
                strbuf_reset(&subdir);
                strbuf_add(&subdir, path+prevlen, baselen-prevlen);
                cdir.d_name = subdir.buf;
-               state = treat_path(dir, NULL, &cdir, istate, &sb, prevlen,
-                                   pathspec);
-               if (state == path_untracked &&
-                   resolve_dtype(cdir.d_type, istate, sb.buf, sb.len) == DT_DIR &&
-                   (dir->flags & DIR_SHOW_IGNORED_TOO ||
-                    do_match_pathspec(istate, pathspec, sb.buf, sb.len,
-                                      baselen, NULL, DO_MATCH_LEADING_PATHSPEC) == MATCHED_RECURSIVELY_LEADING_PATHSPEC)) {
-                       if (!match_pathspec(istate, pathspec, sb.buf, sb.len,
-                                           0 /* prefix */, NULL,
-                                           0 /* do NOT special case dirs */))
-                               state = path_none;
-                       add_path_to_appropriate_result_list(dir, NULL, &cdir,
-                                                           istate,
-                                                           &sb, baselen,
-                                                           pathspec, state);
-                       state = path_recurse;
-               }
+               state = treat_path(dir, NULL, &cdir, istate, &sb, prevlen, pathspec);
  
                if (state != path_recurse)
                        break; /* do not recurse into it */
@@@ -2652,28 -2677,6 +2748,6 @@@ int read_directory(struct dir_struct *d
        QSORT(dir->entries, dir->nr, cmp_dir_entry);
        QSORT(dir->ignored, dir->ignored_nr, cmp_dir_entry);
  
-       /*
-        * If DIR_SHOW_IGNORED_TOO is set, read_directory_recursive() will
-        * also pick up untracked contents of untracked dirs; by default
-        * we discard these, but given DIR_KEEP_UNTRACKED_CONTENTS we do not.
-        */
-       if ((dir->flags & DIR_SHOW_IGNORED_TOO) &&
-                    !(dir->flags & DIR_KEEP_UNTRACKED_CONTENTS)) {
-               int i, j;
-               /* remove from dir->entries untracked contents of untracked dirs */
-               for (i = j = 0; j < dir->nr; j++) {
-                       if (i &&
-                           check_dir_entry_contains(dir->entries[i - 1], dir->entries[j])) {
-                               FREE_AND_NULL(dir->entries[j]);
-                       } else {
-                               dir->entries[i++] = dir->entries[j];
-                       }
-               }
-               dir->nr = i;
-       }
        trace_performance_leave("read directory %.*s", len, path);
        if (dir->untracked) {
                static int force_untracked_cache = -1;
index 6738497ea7a851b2395eb422fa8686dc6b4da717,69c39ff2e490a49bc1cc4a88c567d9cfc8532d05..428cff9cf3f5bfc6947dcb84df4c53ce3b14037b
@@@ -18,7 -18,7 +18,7 @@@ GIT_FORCE_UNTRACKED_CACHE=tru
  export GIT_FORCE_UNTRACKED_CACHE
  
  sync_mtime () {
 -      find . -type d -ls >/dev/null
 +      find . -type d -exec ls -ld {} + >/dev/null
  }
  
  avoid_racy() {
@@@ -30,6 -30,30 +30,30 @@@ status_is_clean() 
        test_must_be_empty ../status.actual
  }
  
+ # Ignore_Untracked_Cache, abbreviated to 3 letters because then people can
+ # compare commands side-by-side, e.g.
+ #    iuc status --porcelain >expect &&
+ #    git status --porcelain >actual &&
+ #    test_cmp expect actual
+ iuc () {
+       git ls-files -s >../current-index-entries
+       git ls-files -t | sed -ne s/^S.//p >../current-sparse-entries
+       GIT_INDEX_FILE=.git/tmp_index
+       export GIT_INDEX_FILE
+       git update-index --index-info <../current-index-entries
+       git update-index --skip-worktree $(cat ../current-sparse-entries)
+       git -c core.untrackedCache=false "$@"
+       ret=$?
+       rm ../current-index-entries
+       rm $GIT_INDEX_FILE
+       unset GIT_INDEX_FILE
+       return $ret
+ }
  test_lazy_prereq UNTRACKED_CACHE '
        { git update-index --test-untracked-cache; ret=$?; } &&
        test $ret -ne 1
@@@ -95,6 -119,8 +119,8 @@@ test_expect_success 'status first time 
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../actual &&
+       iuc status --porcelain >../status.iuc &&
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../actual &&
        cat >../trace.expect <<EOF &&
  node creation: 3
@@@ -115,6 -141,8 +141,8 @@@ test_expect_success 'status second tim
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../actual &&
+       iuc status --porcelain >../status.iuc &&
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -136,6 -164,7 +164,7 @@@ test_expect_success 'modify in root dir
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
  A  done/one
  A  one
@@@ -145,6 -174,7 +174,7 @@@ A  tw
  ?? four
  ?? three
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -183,6 -213,7 +213,7 @@@ test_expect_success 'new .gitignore inv
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
  A  done/one
  A  one
@@@ -192,6 -223,7 +223,7 @@@ A  tw
  ?? dtwo/
  ?? three
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -230,6 -262,7 +262,7 @@@ test_expect_success 'new info/exclude i
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
  A  done/one
  A  one
@@@ -237,6 -270,7 +270,7 @@@ A  tw
  ?? .gitignore
  ?? dtwo/
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -286,6 -320,7 +320,7 @@@ test_expect_success 'status after the m
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
  A  done/one
  A  one
  ?? dtwo/
  ?? two
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -343,6 -379,7 +379,7 @@@ test_expect_success 'status after the m
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
  A  done/one
  A  one
@@@ -350,6 -387,7 +387,7 @@@ A  tw
  ?? .gitignore
  ?? dtwo/
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -390,10 -428,12 +428,12 @@@ test_expect_success 'status after commi
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
  ?? .gitignore
  ?? dtwo/
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -447,12 -487,14 +487,14 @@@ test_expect_success 'test sparse statu
        avoid_racy &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../status.actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
   M done/two
  ?? .gitignore
  ?? done/five
  ?? dtwo/
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../status.actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -487,12 -529,14 +529,14 @@@ test_expect_success 'test sparse statu
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../status.actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
   M done/two
  ?? .gitignore
  ?? done/five
  ?? dtwo/
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../status.actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -514,6 -558,7 +558,7 @@@ test_expect_success 'test sparse statu
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../status.actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
   M done/two
  ?? .gitignore
  ?? done/sub/
  ?? dtwo/
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../status.actual &&
        cat >../trace.expect <<EOF &&
  node creation: 2
@@@ -560,6 -606,8 +606,8 @@@ test_expect_success 'test sparse statu
        : >../trace &&
        GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
        git status --porcelain >../status.actual &&
+       iuc status --porcelain >../status.iuc &&
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../status.actual &&
        cat >../trace.expect <<EOF &&
  node creation: 0
@@@ -573,6 -621,7 +621,7 @@@ EO
  test_expect_success 'move entry in subdir from untracked to cached' '
        git add dtwo/two &&
        git status --porcelain >../status.actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
   M done/two
  A  dtwo/two
  ?? done/five
  ?? done/sub/
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../status.actual
  '
  
  test_expect_success 'move entry in subdir from cached to untracked' '
        git rm --cached dtwo/two &&
        git status --porcelain >../status.actual &&
+       iuc status --porcelain >../status.iuc &&
        cat >../status.expect <<EOF &&
   M done/two
  ?? .gitignore
  ?? done/sub/
  ?? dtwo/
  EOF
+       test_cmp ../status.expect ../status.iuc &&
        test_cmp ../status.expect ../status.actual
  '
  
diff --combined t/t9902-completion.sh
index 5505e5aa249e43b88455b0565f0f9348546d5e4c,d9a6425671f17a63b98865cff9a5d8074a06aea2..3c44af69401594545082c47b16d5a98262c5302f
@@@ -1363,63 -1363,6 +1363,63 @@@ test_expect_success 'teardown after pat
               BS\\dir '$'separators\034in\035dir''
  '
  
 +test_expect_success '__git_find_on_cmdline - single match' '
 +      echo list >expect &&
 +      (
 +              words=(git command --opt list) &&
 +              cword=${#words[@]} &&
 +              __git_find_on_cmdline "add list remove" >actual
 +      ) &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success '__git_find_on_cmdline - multiple matches' '
 +      echo remove >expect &&
 +      (
 +              words=(git command -o --opt remove list add) &&
 +              cword=${#words[@]} &&
 +              __git_find_on_cmdline "add list remove" >actual
 +      ) &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success '__git_find_on_cmdline - no match' '
 +      (
 +              words=(git command --opt branch) &&
 +              cword=${#words[@]} &&
 +              __git_find_on_cmdline "add list remove" >actual
 +      ) &&
 +      test_must_be_empty actual
 +'
 +
 +test_expect_success '__git_find_on_cmdline - single match with index' '
 +      echo "3 list" >expect &&
 +      (
 +              words=(git command --opt list) &&
 +              cword=${#words[@]} &&
 +              __git_find_on_cmdline --show-idx "add list remove" >actual
 +      ) &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success '__git_find_on_cmdline - multiple matches with index' '
 +      echo "4 remove" >expect &&
 +      (
 +              words=(git command -o --opt remove list add) &&
 +              cword=${#words[@]} &&
 +              __git_find_on_cmdline --show-idx "add list remove" >actual
 +      ) &&
 +      test_cmp expect actual
 +'
 +
 +test_expect_success '__git_find_on_cmdline - no match with index' '
 +      (
 +              words=(git command --opt branch) &&
 +              cword=${#words[@]} &&
 +              __git_find_on_cmdline --show-idx "add list remove" >actual
 +      ) &&
 +      test_must_be_empty actual
 +'
  
  test_expect_success '__git_get_config_variables' '
        cat >expect <<-EOF &&
@@@ -1638,6 -1581,11 +1638,11 @@@ test_expect_success 'complete files' 
        echo modify > modified &&
        test_completion "git add " "modified" &&
  
+       mkdir -p some/deep &&
+       touch some/deep/path &&
+       test_completion "git add some/" "some/deep" &&
+       git clean -f some &&
        touch untracked &&
  
        : TODO .gitignore should not be here &&