]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Merge branch 'ab/config-based-hooks-2'
authorJunio C Hamano <gitster@pobox.com>
Wed, 9 Feb 2022 22:21:00 +0000 (14:21 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 9 Feb 2022 22:21:00 +0000 (14:21 -0800)
More "config-based hooks".

* ab/config-based-hooks-2:
  run-command: remove old run_hook_{le,ve}() hook API
  receive-pack: convert push-to-checkout hook to hook.h
  read-cache: convert post-index-change to use hook.h
  commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
  git-p4: use 'git hook' to run hooks
  send-email: use 'git hook run' for 'sendemail-validate'
  git hook run: add an --ignore-missing flag
  hooks: convert worktree 'post-checkout' hook to hook library
  hooks: convert non-worktree 'post-checkout' hook to hook library
  merge: convert post-merge to use hook.h
  am: convert applypatch-msg to use hook.h
  rebase: convert pre-rebase to use hook.h
  hook API: add a run_hooks_l() wrapper
  am: convert {pre,post}-applypatch to use hook.h
  gc: use hook library for pre-auto-gc hook
  hook API: add a run_hooks() wrapper
  hook: add 'run' subcommand

14 files changed:
1  2 
Makefile
builtin/am.c
builtin/checkout.c
builtin/clone.c
builtin/gc.c
builtin/merge.c
builtin/rebase.c
builtin/receive-pack.c
builtin/worktree.c
commit.c
git-p4.py
git.c
read-cache.c
run-command.c

diff --combined Makefile
index 5580859afdb45b44459798fa490f9b53e426079b,9a3cbc8c4147f03c3c5b022807ca44cd2d6036f5..8e07003840b70dc1c7cf5b83769a48b9d703a308
+++ b/Makefile
@@@ -1109,6 -1109,7 +1109,7 @@@ BUILTIN_OBJS += builtin/get-tar-commit-
  BUILTIN_OBJS += builtin/grep.o
  BUILTIN_OBJS += builtin/hash-object.o
  BUILTIN_OBJS += builtin/help.o
+ BUILTIN_OBJS += builtin/hook.o
  BUILTIN_OBJS += builtin/index-pack.o
  BUILTIN_OBJS += builtin/init-db.o
  BUILTIN_OBJS += builtin/interpret-trailers.o
@@@ -1881,7 -1882,7 +1882,7 @@@ ifdef GIT_TEST_CMP_USE_COPIED_CONTEX
  endif
  
  ifndef NO_MSGFMT_EXTENDED_OPTIONS
 -      MSGFMT += --check --statistics
 +      MSGFMT += --check
  endif
  
  ifdef HAVE_CLOCK_GETTIME
@@@ -2112,6 -2113,11 +2113,6 @@@ ifdef DEFAULT_HELP_FORMA
  BASIC_CFLAGS += -DDEFAULT_HELP_FORMAT='"$(DEFAULT_HELP_FORMAT)"'
  endif
  
 -PAGER_ENV_SQ = $(subst ','\'',$(PAGER_ENV))
 -PAGER_ENV_CQ = "$(subst ",\",$(subst \,\\,$(PAGER_ENV)))"
 -PAGER_ENV_CQ_SQ = $(subst ','\'',$(PAGER_ENV_CQ))
 -BASIC_CFLAGS += -DPAGER_ENV='$(PAGER_ENV_CQ_SQ)'
 -
  ALL_CFLAGS += $(BASIC_CFLAGS)
  ALL_LDFLAGS += $(BASIC_LDFLAGS)
  
@@@ -2218,20 -2224,14 +2219,20 @@@ git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS
                $(filter %.o,$^) $(LIBS)
  
  help.sp help.s help.o: command-list.h
 -hook.sp hook.s hook.o: hook-list.h
 +builtin/bugreport.sp builtin/bugreport.s builtin/bugreport.o: hook-list.h
  
 -builtin/help.sp builtin/help.s builtin/help.o: config-list.h hook-list.h GIT-PREFIX
 +builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX
  builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \
        '-DGIT_HTML_PATH="$(htmldir_relative_SQ)"' \
        '-DGIT_MAN_PATH="$(mandir_relative_SQ)"' \
        '-DGIT_INFO_PATH="$(infodir_relative_SQ)"'
  
 +PAGER_ENV_SQ = $(subst ','\'',$(PAGER_ENV))
 +PAGER_ENV_CQ = "$(subst ",\",$(subst \,\\,$(PAGER_ENV)))"
 +PAGER_ENV_CQ_SQ = $(subst ','\'',$(PAGER_ENV_CQ))
 +pager.sp pager.s pager.o: EXTRA_CPPFLAGS = \
 +      -DPAGER_ENV='$(PAGER_ENV_CQ_SQ)'
 +
  version.sp version.s version.o: GIT-VERSION-FILE GIT-USER-AGENT
  version.sp version.s version.o: EXTRA_CPPFLAGS = \
        '-DGIT_VERSION="$(GIT_VERSION)"' \
diff --combined builtin/am.c
index b6be1f1cb11e47dcb1012e1afe60fbae654f25cd,ae0c484dcbac98b89f65a230cfb2e8e82a0fa70e..7de2c89ef22c7f9a4292f34ddb3278547791ebeb
@@@ -87,12 -87,6 +87,12 @@@ enum show_patch_type 
        SHOW_PATCH_DIFF = 1,
  };
  
 +enum empty_action {
 +      STOP_ON_EMPTY_COMMIT = 0,  /* output errors and stop in the middle of an am session */
 +      DROP_EMPTY_COMMIT,         /* skip with a notice message, unless "--quiet" has been passed */
 +      KEEP_EMPTY_COMMIT,         /* keep recording as empty commits */
 +};
 +
  struct am_state {
        /* state directory path */
        char *dir;
        int message_id;
        int scissors; /* enum scissors_type */
        int quoted_cr; /* enum quoted_cr_action */
 +      int empty_type; /* enum empty_action */
        struct strvec git_apply_opts;
        const char *resolvemsg;
        int committer_date_is_author_date;
@@@ -185,25 -178,6 +185,25 @@@ static int am_option_parse_quoted_cr(co
        return 0;
  }
  
 +static int am_option_parse_empty(const struct option *opt,
 +                                   const char *arg, int unset)
 +{
 +      int *opt_value = opt->value;
 +
 +      BUG_ON_OPT_NEG(unset);
 +
 +      if (!strcmp(arg, "stop"))
 +              *opt_value = STOP_ON_EMPTY_COMMIT;
 +      else if (!strcmp(arg, "drop"))
 +              *opt_value = DROP_EMPTY_COMMIT;
 +      else if (!strcmp(arg, "keep"))
 +              *opt_value = KEEP_EMPTY_COMMIT;
 +      else
 +              return error(_("Invalid value for --empty: %s"), arg);
 +
 +      return 0;
 +}
 +
  /**
   * Returns path relative to the am_state directory.
   */
@@@ -474,7 -448,7 +474,7 @@@ static int run_applypatch_msg_hook(stru
        int ret;
  
        assert(state->msg);
-       ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
+       ret = run_hooks_l("applypatch-msg", am_path(state, "final-commit"), NULL);
  
        if (!ret) {
                FREE_AND_NULL(state->msg);
@@@ -1152,12 -1126,6 +1152,12 @@@ static void NORETURN die_user_resolve(c
  
                printf_ln(_("When you have resolved this problem, run \"%s --continue\"."), cmdline);
                printf_ln(_("If you prefer to skip this patch, run \"%s --skip\" instead."), cmdline);
 +
 +              if (advice_enabled(ADVICE_AM_WORK_DIR) &&
 +                  is_empty_or_missing_file(am_path(state, "patch")) &&
 +                  !repo_index_has_changes(the_repository, NULL, NULL))
 +                      printf_ln(_("To record the empty patch as an empty commit, run \"%s --allow-empty\"."), cmdline);
 +
                printf_ln(_("To restore the original branch and stop patching, run \"%s --abort\"."), cmdline);
        }
  
@@@ -1280,6 -1248,11 +1280,6 @@@ static int parse_mail(struct am_state *
                goto finish;
        }
  
 -      if (is_empty_or_missing_file(am_path(state, "patch"))) {
 -              printf_ln(_("Patch is empty."));
 -              die_user_resolve(state);
 -      }
 -
        strbuf_addstr(&msg, "\n\n");
        strbuf_addbuf(&msg, &mi.log_message);
        strbuf_stripspace(&msg, 0);
@@@ -1636,7 -1609,7 +1636,7 @@@ static void do_commit(const struct am_s
        const char *reflog_msg, *author, *committer = NULL;
        struct strbuf sb = STRBUF_INIT;
  
-       if (run_hook_le(NULL, "pre-applypatch", NULL))
+       if (run_hooks("pre-applypatch"))
                exit(1);
  
        if (write_cache_as_tree(&tree, 0, NULL))
                fclose(fp);
        }
  
-       run_hook_le(NULL, "post-applypatch", NULL);
+       run_hooks("post-applypatch");
  
        strbuf_release(&sb);
  }
@@@ -1790,7 -1763,6 +1790,7 @@@ static void am_run(struct am_state *sta
        while (state->cur <= state->last) {
                const char *mail = am_path(state, msgnum(state));
                int apply_status;
 +              int to_keep;
  
                reset_ident_date();
  
                if (state->interactive && do_interactive(state))
                        goto next;
  
 +              to_keep = 0;
 +              if (is_empty_or_missing_file(am_path(state, "patch"))) {
 +                      switch (state->empty_type) {
 +                      case DROP_EMPTY_COMMIT:
 +                              say(state, stdout, _("Skipping: %.*s"), linelen(state->msg), state->msg);
 +                              goto next;
 +                              break;
 +                      case KEEP_EMPTY_COMMIT:
 +                              to_keep = 1;
 +                              say(state, stdout, _("Creating an empty commit: %.*s"),
 +                                      linelen(state->msg), state->msg);
 +                              break;
 +                      case STOP_ON_EMPTY_COMMIT:
 +                              printf_ln(_("Patch is empty."));
 +                              die_user_resolve(state);
 +                              break;
 +                      }
 +              }
 +
                if (run_applypatch_msg_hook(state))
                        exit(1);
 +              if (to_keep)
 +                      goto commit;
  
                say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg);
  
                        die_user_resolve(state);
                }
  
 +commit:
                do_commit(state);
  
  next:
  /**
   * Resume the current am session after patch application failure. The user did
   * all the hard work, and we do not have to do any patch application. Just
 - * trust and commit what the user has in the index and working tree.
 + * trust and commit what the user has in the index and working tree. If `allow_empty`
 + * is true, commit as an empty commit when index has not changed and lacking a patch.
   */
 -static void am_resolve(struct am_state *state)
 +static void am_resolve(struct am_state *state, int allow_empty)
  {
        validate_resume_state(state);
  
        say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg);
  
        if (!repo_index_has_changes(the_repository, NULL, NULL)) {
 -              printf_ln(_("No changes - did you forget to use 'git add'?\n"
 -                      "If there is nothing left to stage, chances are that something else\n"
 -                      "already introduced the same changes; you might want to skip this patch."));
 -              die_user_resolve(state);
 +              if (allow_empty && is_empty_or_missing_file(am_path(state, "patch"))) {
 +                      printf_ln(_("No changes - recorded it as an empty commit."));
 +              } else {
 +                      printf_ln(_("No changes - did you forget to use 'git add'?\n"
 +                                  "If there is nothing left to stage, chances are that something else\n"
 +                                  "already introduced the same changes; you might want to skip this patch."));
 +                      die_user_resolve(state);
 +              }
        }
  
        if (unmerged_cache()) {
@@@ -2250,8 -2195,7 +2250,8 @@@ enum resume_type 
        RESUME_SKIP,
        RESUME_ABORT,
        RESUME_QUIT,
 -      RESUME_SHOW_PATCH
 +      RESUME_SHOW_PATCH,
 +      RESUME_ALLOW_EMPTY,
  };
  
  struct resume_mode {
@@@ -2286,9 -2230,9 +2286,9 @@@ static int parse_opt_show_current_patch
        }
  
        if (resume->mode == RESUME_SHOW_PATCH && new_value != resume->sub_mode)
 -              return error(_("--show-current-patch=%s is incompatible with "
 -                             "--show-current-patch=%s"),
 -                           arg, valid_modes[resume->sub_mode]);
 +              return error(_("options '%s=%s' and '%s=%s' "
 +                                         "cannot be used together"),
 +                                       "--show-current-patch", "--show-current-patch", arg, valid_modes[resume->sub_mode]);
  
        resume->mode = RESUME_SHOW_PATCH;
        resume->sub_mode = new_value;
@@@ -2404,9 -2348,6 +2404,9 @@@ int cmd_am(int argc, const char **argv
                  N_("show the patch being applied"),
                  PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,
                  parse_opt_show_current_patch, RESUME_SHOW_PATCH },
 +              OPT_CMDMODE(0, "allow-empty", &resume.mode,
 +                      N_("record the empty patch as an empty commit"),
 +                      RESUME_ALLOW_EMPTY),
                OPT_BOOL(0, "committer-date-is-author-date",
                        &state.committer_date_is_author_date,
                        N_("lie about committer date")),
                { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"),
                  N_("GPG-sign commits"),
                  PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
 +              OPT_CALLBACK_F(STOP_ON_EMPTY_COMMIT, "empty", &state.empty_type, "{stop,drop,keep}",
 +                N_("how to handle empty patches"),
 +                PARSE_OPT_NONEG, am_option_parse_empty),
                OPT_HIDDEN_BOOL(0, "rebasing", &state.rebasing,
                        N_("(internal use for git-rebase)")),
                OPT_END()
                am_run(&state, 1);
                break;
        case RESUME_RESOLVED:
 -              am_resolve(&state);
 +      case RESUME_ALLOW_EMPTY:
 +              am_resolve(&state, resume.mode == RESUME_ALLOW_EMPTY ? 1 : 0);
                break;
        case RESUME_SKIP:
                am_skip(&state);
diff --combined builtin/checkout.c
index cc804ba8e1e6cdb8748eabc7e91575ac2ee53a4a,e2e95445407cd3952e497b2a1afc8edc224f8589..8f010412a9cdc957ff8ceac841c7cb14b6d55b5a
@@@ -9,6 -9,7 +9,7 @@@
  #include "config.h"
  #include "diff.h"
  #include "dir.h"
+ #include "hook.h"
  #include "ll-merge.h"
  #include "lockfile.h"
  #include "merge-recursive.h"
@@@ -114,7 -115,7 +115,7 @@@ static void branch_info_release(struct 
  static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
                              int changed)
  {
-       return run_hook_le(NULL, "post-checkout",
+       return run_hooks_l("post-checkout",
                           oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
                           oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
                           changed ? "1" : "0", NULL);
@@@ -464,10 -465,10 +465,10 @@@ static int checkout_paths(const struct 
                die(_("'%s' cannot be used with updating paths"), "--detach");
  
        if (opts->merge && opts->patch_mode)
 -              die(_("'%s' cannot be used with %s"), "--merge", "--patch");
 +              die(_("options '%s' and '%s' cannot be used together"), "--merge", "--patch");
  
        if (opts->ignore_unmerged && opts->merge)
 -              die(_("'%s' cannot be used with %s"),
 +              die(_("options '%s' and '%s' cannot be used together"),
                    opts->ignore_unmerged_opt, "-m");
  
        if (opts->new_branch)
@@@ -1094,6 -1095,9 +1095,6 @@@ static int switch_branches(const struc
                const char *p;
                if (skip_prefix(old_branch_info.path, prefix, &p))
                        old_branch_info.name = xstrdup(p);
 -              else
 -                      BUG("should be able to skip past '%s' in '%s'!",
 -                          prefix, old_branch_info.path);
        }
  
        if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
@@@ -1546,10 -1550,8 +1547,10 @@@ static struct option *add_common_switch
  {
        struct option options[] = {
                OPT_BOOL('d', "detach", &opts->force_detach, N_("detach HEAD at named commit")),
 -              OPT_SET_INT('t', "track",  &opts->track, N_("set upstream info for new branch"),
 -                      BRANCH_TRACK_EXPLICIT),
 +              OPT_CALLBACK_F('t', "track",  &opts->track, "(direct|inherit)",
 +                      N_("set branch tracking configuration"),
 +                      PARSE_OPT_OPTARG,
 +                      parse_opt_tracking_mode),
                OPT__FORCE(&opts->force, N_("force checkout (throw away local modifications)"),
                           PARSE_OPT_NOCOMPLETE),
                OPT_STRING(0, "orphan", &opts->new_orphan_branch, N_("new-branch"), N_("new unparented branch")),
@@@ -1634,11 -1636,11 +1635,11 @@@ static int checkout_main(int argc, cons
        }
  
        if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1)
 -              die(_("-%c, -%c and --orphan are mutually exclusive"),
 -                              cb_option, toupper(cb_option));
 +              die(_("options '-%c', '-%c', and '%s' cannot be used together"),
 +                      cb_option, toupper(cb_option), "--orphan");
  
        if (opts->overlay_mode == 1 && opts->patch_mode)
 -              die(_("-p and --overlay are mutually exclusive"));
 +              die(_("options '%s' and '%s' cannot be used together"), "-p", "--overlay");
  
        if (opts->checkout_index >= 0 || opts->checkout_worktree >= 0) {
                if (opts->checkout_index < 0)
  
        if (opts->pathspec_from_file) {
                if (opts->pathspec.nr)
 -                      die(_("--pathspec-from-file is incompatible with pathspec arguments"));
 +                      die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file");
  
                if (opts->force_detach)
 -                      die(_("--pathspec-from-file is incompatible with --detach"));
 +                      die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--detach");
  
                if (opts->patch_mode)
 -                      die(_("--pathspec-from-file is incompatible with --patch"));
 +                      die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--patch");
  
                parse_pathspec_file(&opts->pathspec, 0,
                                    0,
                                    prefix, opts->pathspec_from_file, opts->pathspec_file_nul);
        } else if (opts->pathspec_file_nul) {
 -              die(_("--pathspec-file-nul requires --pathspec-from-file"));
 +              die(_("the option '%s' requires '%s'"), "--pathspec-file-nul", "--pathspec-from-file");
        }
  
        opts->pathspec.recursive = 1;
diff --combined builtin/clone.c
index 727e16e0aea435a1fe244c381d8bb7670e8e7a5a,ee27b9f8114bd7041183e2250c80ee47b2a9c78a..b19925f373409adfae5450b48e2f0c8c780c4b0a
@@@ -32,6 -32,7 +32,7 @@@
  #include "connected.h"
  #include "packfile.h"
  #include "list-objects-filter-options.h"
+ #include "hook.h"
  
  /*
   * Overall FIXMEs:
@@@ -633,7 -634,7 +634,7 @@@ static int git_sparse_checkout_init(con
  {
        struct strvec argv = STRVEC_INIT;
        int result = 0;
 -      strvec_pushl(&argv, "-C", repo, "sparse-checkout", "init", NULL);
 +      strvec_pushl(&argv, "-C", repo, "sparse-checkout", "set", NULL);
  
        /*
         * We must apply the setting in the current process
@@@ -705,7 -706,7 +706,7 @@@ static int checkout(int submodule_progr
        if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
                die(_("unable to write new index file"));
  
-       err |= run_hook_le(NULL, "post-checkout", oid_to_hex(null_oid()),
+       err |= run_hooks_l("post-checkout", oid_to_hex(null_oid()),
                           oid_to_hex(&oid), "1", NULL);
  
        if (!err && (option_recurse_submodules.nr > 0)) {
@@@ -900,10 -901,10 +901,10 @@@ int cmd_clone(int argc, const char **ar
  
        if (option_bare) {
                if (option_origin)
 -                      die(_("--bare and --origin %s options are incompatible."),
 -                          option_origin);
 +                      die(_("options '%s' and '%s %s' cannot be used together"),
 +                          "--bare", "--origin", option_origin);
                if (real_git_dir)
 -                      die(_("--bare and --separate-git-dir are incompatible."));
 +                      die(_("options '%s' and '%s' cannot be used together"), "--bare", "--separate-git-dir");
                option_no_checkout = 1;
        }
  
         */
        submodule_progress = transport->progress;
  
 -      transport_unlock_pack(transport);
 +      transport_unlock_pack(transport, 0);
        transport_disconnect(transport);
  
        if (option_dissociate) {
diff --combined builtin/gc.c
index 8e60ef1eaba426eae66c99d15d5dfdcbc26efac4,4bbc58aae5bb6126d2a17efb969eb0daba109ac6..ffaf0daf5d9b565e0be4c35e586ad1054f981061
@@@ -32,6 -32,7 +32,7 @@@
  #include "remote.h"
  #include "object-store.h"
  #include "exec-cmd.h"
+ #include "hook.h"
  
  #define FAILED_RUN "failed to run %s"
  
@@@ -394,7 -395,7 +395,7 @@@ static int need_to_gc(void
        else
                return 0;
  
-       if (run_hook_le(NULL, "pre-auto-gc", NULL))
+       if (run_hooks("pre-auto-gc"))
                return 0;
        return 1;
  }
@@@ -470,8 -471,7 +471,8 @@@ static const char *lock_repo_for_gc(in
  /*
   * Returns 0 if there was no previous error and gc can proceed, 1 if
   * gc should not proceed due to an error in the last run. Prints a
 - * message and returns -1 if an error occurred while reading gc.log
 + * message and returns with a non-[01] status code if an error occurred
 + * while reading gc.log
   */
  static int report_last_gc_error(void)
  {
                if (errno == ENOENT)
                        goto done;
  
 -              ret = error_errno(_("cannot stat '%s'"), gc_log_path);
 +              ret = die_message_errno(_("cannot stat '%s'"), gc_log_path);
                goto done;
        }
  
  
        len = strbuf_read_file(&sb, gc_log_path, 0);
        if (len < 0)
 -              ret = error_errno(_("cannot read '%s'"), gc_log_path);
 +              ret = die_message_errno(_("cannot read '%s'"), gc_log_path);
        else if (len > 0) {
                /*
                 * A previous gc failed.  Report the error, and don't
@@@ -612,13 -612,12 +613,13 @@@ int cmd_gc(int argc, const char **argv
                }
                if (detach_auto) {
                        int ret = report_last_gc_error();
 -                      if (ret < 0)
 -                              /* an I/O error occurred, already reported */
 -                              exit(128);
 +
                        if (ret == 1)
                                /* Last gc --auto failed. Skip this one. */
                                return 0;
 +                      else if (ret)
 +                              /* an I/O error occurred, already reported */
 +                              return ret;
  
                        if (lock_repo_for_gc(force, &pid))
                                return 0;
diff --combined builtin/merge.c
index 74e53cf20a776e23efaa36702dffec408e4d29fa,5be3009c2a24a54381174f65a855cc9d02a117e5..fed1ff401701ac4407d69c40f0e45af3e2b3717f
@@@ -87,7 -87,6 +87,7 @@@ static int signoff
  static const char *sign_commit;
  static int autostash;
  static int no_verify;
 +static char *into_name;
  
  static struct strategy all_strategy[] = {
        { "recursive",  NO_TRIVIAL },
@@@ -287,8 -286,6 +287,8 @@@ static struct option builtin_merge_opti
        { OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"),
                N_("read message from file"), PARSE_OPT_NONEG,
                NULL, 0, option_read_message },
 +      OPT_STRING(0, "into-name", &into_name, N_("name"),
 +                 N_("use <name> instead of the real target")),
        OPT__VERBOSITY(&verbosity),
        OPT_BOOL(0, "abort", &abort_current_merge,
                N_("abort the current in-progress merge")),
@@@ -490,7 -487,7 +490,7 @@@ static void finish(struct commit *head_
        }
  
        /* Run a post-merge hook */
-       run_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL);
+       run_hooks_l("post-merge", squash ? "1" : "0", NULL);
  
        apply_autostash(git_path_merge_autostash(the_repository));
        strbuf_release(&reflog_message);
@@@ -1124,7 -1121,6 +1124,7 @@@ static void prepare_merge_message(struc
        opts.add_title = !have_message;
        opts.shortlog_len = shortlog_len;
        opts.credit_people = (0 < option_edit);
 +      opts.into_name = into_name;
  
        fmt_merge_msg(merge_names, merge_msg, &opts);
        if (merge_msg->len)
@@@ -1400,9 -1396,9 +1400,9 @@@ int cmd_merge(int argc, const char **ar
  
        if (squash) {
                if (fast_forward == FF_NO)
 -                      die(_("You cannot combine --squash with --no-ff."));
 +                      die(_("options '%s' and '%s' cannot be used together"), "--squash", "--no-ff.");
                if (option_commit > 0)
 -                      die(_("You cannot combine --squash with --commit."));
 +                      die(_("options '%s' and '%s' cannot be used together"), "--squash", "--commit.");
                /*
                 * squash can now silently disable option_commit - this is not
                 * a problem as it is only overriding the default, not a user
diff --combined builtin/rebase.c
index 36490d06c8ac6ad4c10596b7e2a4556ec7aaa8c8,ac4120013a08fb2f578fd9274dc3c1bbd02dee34..2e6d8fa34ee9861187135653ba2cb9db3f03947b
@@@ -28,6 -28,7 +28,7 @@@
  #include "sequencer.h"
  #include "rebase-interactive.h"
  #include "reset.h"
+ #include "hook.h"
  
  #define DEFAULT_REFLOG_ACTION "rebase"
  
@@@ -1190,13 -1191,13 +1191,13 @@@ int cmd_rebase(int argc, const char **a
  
        if (keep_base) {
                if (options.onto_name)
 -                      die(_("cannot combine '--keep-base' with '--onto'"));
 +                      die(_("options '%s' and '%s' cannot be used together"), "--keep-base", "--onto");
                if (options.root)
 -                      die(_("cannot combine '--keep-base' with '--root'"));
 +                      die(_("options '%s' and '%s' cannot be used together"), "--keep-base", "--root");
        }
  
        if (options.root && options.fork_point > 0)
 -              die(_("cannot combine '--root' with '--fork-point'"));
 +              die(_("options '%s' and '%s' cannot be used together"), "--root", "--fork-point");
  
        if (action != ACTION_NONE && !in_progress)
                die(_("No rebase in progress?"));
  
                if (i >= 0) {
                        if (is_merge(&options))
 -                              die(_("cannot combine apply options with "
 -                                    "merge options"));
 +                              die(_("apply options and merge options "
 +                                        "cannot be used together"));
                        else
                                options.type = REBASE_APPLY;
                }
  
        /* If a hook exists, give it a chance to interrupt*/
        if (!ok_to_skip_pre_rebase &&
-           run_hook_le(NULL, "pre-rebase", options.upstream_arg,
+           run_hooks_l("pre-rebase", options.upstream_arg,
                        argc ? argv[0] : NULL, NULL))
                die(_("The pre-rebase hook refused to rebase."));
  
diff --combined builtin/receive-pack.c
index 5c2732a0d0785d9cffbe8503890c997a67a84c65,e99b1ecd103f41d8bed551040c1c03df4a880252..c427ca09aafa69810fa7213cebb6375bb6a21b92
@@@ -581,19 -581,32 +581,19 @@@ static char *prepare_push_cert_nonce(co
        return strbuf_detach(&buf, NULL);
  }
  
 -/*
 - * NEEDSWORK: reuse find_commit_header() from jk/commit-author-parsing
 - * after dropping "_commit" from its name and possibly moving it out
 - * of commit.c
 - */
  static char *find_header(const char *msg, size_t len, const char *key,
                         const char **next_line)
  {
 -      int key_len = strlen(key);
 -      const char *line = msg;
 -
 -      while (line && line < msg + len) {
 -              const char *eol = strchrnul(line, '\n');
 -
 -              if ((msg + len <= eol) || line == eol)
 -                      return NULL;
 -              if (line + key_len < eol &&
 -                  !memcmp(line, key, key_len) && line[key_len] == ' ') {
 -                      int offset = key_len + 1;
 -                      if (next_line)
 -                              *next_line = *eol ? eol + 1 : eol;
 -                      return xmemdupz(line + offset, (eol - line) - offset);
 -              }
 -              line = *eol ? eol + 1 : NULL;
 -      }
 -      return NULL;
 +      size_t out_len;
 +      const char *val = find_header_mem(msg, len, key, &out_len);
 +
 +      if (!val)
 +              return NULL;
 +
 +      if (next_line)
 +              *next_line = val + out_len + 1;
 +
 +      return xmemdupz(val, out_len);
  }
  
  /*
@@@ -1411,9 -1424,12 +1411,12 @@@ static const char *push_to_checkout(uns
                                    struct strvec *env,
                                    const char *work_tree)
  {
+       struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
        strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
-       if (run_hook_le(env->v, push_to_checkout_hook,
-                       hash_to_hex(hash), NULL))
+       strvec_pushv(&opt.env, env->v);
+       strvec_push(&opt.args, hash_to_hex(hash));
+       if (run_hooks_opt(push_to_checkout_hook, &opt))
                return "push-to-checkout hook declined";
        else
                return NULL;
@@@ -2193,7 -2209,7 +2196,7 @@@ static const char *unpack(int err_fd, s
                strvec_push(&child.args, alt_shallow_file);
        }
  
 -      tmp_objdir = tmp_objdir_create();
 +      tmp_objdir = tmp_objdir_create("incoming");
        if (!tmp_objdir) {
                if (err_fd > 0)
                        close(err_fd);
diff --combined builtin/worktree.c
index 2838254f7f2e10f780ea8608d591c6fd8e8f5521,6f03afad9740e75a9255cfe7535bdf8211638fab..0d0809276fe1b566140ce2f75a704c06cd0aa690
@@@ -382,21 -382,17 +382,17 @@@ done
         * is_junk is cleared, but do return appropriate code when hook fails.
         */
        if (!ret && opts->checkout) {
-               const char *hook = find_hook("post-checkout");
-               if (hook) {
-                       const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
-                       struct child_process cp = CHILD_PROCESS_INIT;
-                       cp.no_stdin = 1;
-                       cp.stdout_to_stderr = 1;
-                       cp.dir = path;
-                       strvec_pushv(&cp.env_array, env);
-                       cp.trace2_hook_name = "post-checkout";
-                       strvec_pushl(&cp.args, absolute_path(hook),
-                                    oid_to_hex(null_oid()),
-                                    oid_to_hex(&commit->object.oid),
-                                    "1", NULL);
-                       ret = run_command(&cp);
-               }
+               struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+               strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
+               strvec_pushl(&opt.args,
+                            oid_to_hex(null_oid()),
+                            oid_to_hex(&commit->object.oid),
+                            "1",
+                            NULL);
+               opt.dir = path;
+               ret = run_hooks_opt("post-checkout", &opt);
        }
  
        strvec_clear(&child_env);
@@@ -503,9 -499,9 +499,9 @@@ static int add(int ac, const char **av
        opts.checkout = 1;
        ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
        if (!!opts.detach + !!new_branch + !!new_branch_force > 1)
 -              die(_("-b, -B, and --detach are mutually exclusive"));
 +              die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach");
        if (lock_reason && !keep_locked)
 -              die(_("--reason requires --lock"));
 +              die(_("the option '%s' requires '%s'"), "--reason", "--lock");
        if (lock_reason)
                opts.keep_locked = lock_reason;
        else if (keep_locked)
@@@ -699,7 -695,7 +695,7 @@@ static int list(int ac, const char **av
        if (ac)
                usage_with_options(worktree_usage, options);
        else if (verbose && porcelain)
 -              die(_("--verbose and --porcelain are mutually exclusive"));
 +              die(_("options '%s' and '%s' cannot be used together"), "--verbose", "--porcelain");
        else {
                struct worktree **worktrees = get_worktrees();
                int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i;
diff --combined commit.c
index 28391c3468dc77b1b0f417c0eca9991273757be6,931f65d56d6451e2d59dc33beb6dcaad780b546e..d400f5dfa2b1f9016f90c5aee9665b1f08316048
+++ b/commit.c
@@@ -21,6 -21,7 +21,7 @@@
  #include "commit-reach.h"
  #include "run-command.h"
  #include "shallow.h"
+ #include "hook.h"
  
  static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
  
@@@ -1631,20 -1632,12 +1632,20 @@@ struct commit_list **commit_list_append
        return &new_commit->next;
  }
  
 -const char *find_commit_header(const char *msg, const char *key, size_t *out_len)
 +const char *find_header_mem(const char *msg, size_t len,
 +                      const char *key, size_t *out_len)
  {
        int key_len = strlen(key);
        const char *line = msg;
  
 -      while (line) {
 +      /*
 +       * NEEDSWORK: It's possible for strchrnul() to scan beyond the range
 +       * given by len. However, current callers are safe because they compute
 +       * len by scanning a NUL-terminated block of memory starting at msg.
 +       * Nonetheless, it would be better to ensure the function does not look
 +       * at msg beyond the len provided by the caller.
 +       */
 +      while (line && line < msg + len) {
                const char *eol = strchrnul(line, '\n');
  
                if (line == eol)
        return NULL;
  }
  
 +const char *find_commit_header(const char *msg, const char *key, size_t *out_len)
 +{
 +      return find_header_mem(msg, strlen(msg), key, out_len);
 +}
  /*
   * Inspect the given string and determine the true "end" of the log message, in
   * order to find where to put a new Signed-off-by trailer.  Ignored are
@@@ -1714,22 -1703,22 +1715,22 @@@ size_t ignore_non_trailer(const char *b
  int run_commit_hook(int editor_is_used, const char *index_file,
                    const char *name, ...)
  {
-       struct strvec hook_env = STRVEC_INIT;
+       struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
        va_list args;
-       int ret;
+       const char *arg;
  
-       strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
+       strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s", index_file);
  
        /*
         * Let the hook know that no editor will be launched.
         */
        if (!editor_is_used)
-               strvec_push(&hook_env, "GIT_EDITOR=:");
+               strvec_push(&opt.env, "GIT_EDITOR=:");
  
        va_start(args, name);
-       ret = run_hook_ve(hook_env.v, name, args);
+       while ((arg = va_arg(args, const char *)))
+               strvec_push(&opt.args, arg);
        va_end(args);
-       strvec_clear(&hook_env);
  
-       return ret;
+       return run_hooks_opt(name, &opt);
  }
diff --combined git-p4.py
index 47ad2d6409356ca49c4965fc1be0bdf4c46e56c7,3b54168eb4a59e6c325fb34877dd2a3fde73b99d..a9b1f9044108e4dce94b865f1f777039d8755613
+++ b/git-p4.py
@@@ -56,21 -56,6 +56,21 @@@ defaultBlockSize = 1<<2
  
  p4_access_checked = False
  
 +re_ko_keywords = re.compile(br'\$(Id|Header)(:[^$\n]+)?\$')
 +re_k_keywords = re.compile(br'\$(Id|Header|Author|Date|DateTime|Change|File|Revision)(:[^$\n]+)?\$')
 +
 +def format_size_human_readable(num):
 +    """ Returns a number of units (typically bytes) formatted as a human-readable
 +        string.
 +    """
 +    if num < 1024:
 +        return '{:d} B'.format(num)
 +    for unit in ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
 +        num /= 1024.0
 +        if num < 1024.0:
 +            return "{:3.1f} {}B".format(num, unit)
 +    return "{:.1f} YiB".format(num)
 +
  def p4_build_cmd(cmd):
      """Build a suitable p4 command line.
  
          # Provide a way to not pass this option by setting git-p4.retries to 0
          real_cmd += ["-r", str(retries)]
  
 -    if not isinstance(cmd, list):
 -        real_cmd = ' '.join(real_cmd) + ' ' + cmd
 -    else:
 -        real_cmd += cmd
 +    real_cmd += cmd
  
      # now check that we can actually talk to the server
      global p4_access_checked
@@@ -220,151 -208,93 +220,93 @@@ def decode_path(path)
  
  def run_git_hook(cmd, param=[]):
      """Execute a hook if the hook exists."""
-     if verbose:
-         sys.stderr.write("Looking for hook: %s\n" % cmd)
-         sys.stderr.flush()
-     hooks_path = gitConfig("core.hooksPath")
-     if len(hooks_path) <= 0:
-         hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
-     if not isinstance(param, list):
-         param=[param]
-     # resolve hook file name, OS depdenent
-     hook_file = os.path.join(hooks_path, cmd)
-     if platform.system() == 'Windows':
-         if not os.path.isfile(hook_file):
-             # look for the file with an extension
-             files = glob.glob(hook_file + ".*")
-             if not files:
-                 return True
-             files.sort()
-             hook_file = files.pop()
-             while hook_file.upper().endswith(".SAMPLE"):
-                 # The file is a sample hook. We don't want it
-                 if len(files) > 0:
-                     hook_file = files.pop()
-                 else:
-                     return True
-     if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
-         return True
-     return run_hook_command(hook_file, param) == 0
- def run_hook_command(cmd, param):
-     """Executes a git hook command
-        cmd = the command line file to be executed. This can be
-        a file that is run by OS association.
-        param = a list of parameters to pass to the cmd command
-        On windows, the extension is checked to see if it should
-        be run with the Git for Windows Bash shell.  If there
-        is no file extension, the file is deemed a bash shell
-        and will be handed off to sh.exe. Otherwise, Windows
-        will be called with the shell to handle the file assocation.
-        For non Windows operating systems, the file is called
-        as an executable.
-     """
-     cli = [cmd] + param
-     use_shell = False
-     if platform.system() == 'Windows':
-         (root,ext) = os.path.splitext(cmd)
-         if ext == "":
-             exe_path = os.environ.get("EXEPATH")
-             if exe_path is None:
-                 exe_path = ""
-             else:
-                 exe_path = os.path.join(exe_path, "bin")
-             cli = [os.path.join(exe_path, "SH.EXE")] + cli
-         else:
-             use_shell = True
-     return subprocess.call(cli, shell=use_shell)
+     args = ['git', 'hook', 'run', '--ignore-missing', cmd]
+     if param:
+         args.append("--")
+         for p in param:
+             args.append(p)
+     return subprocess.call(args) == 0
  
 -def write_pipe(c, stdin):
 +def write_pipe(c, stdin, *k, **kw):
      if verbose:
 -        sys.stderr.write('Writing pipe: %s\n' % str(c))
 +        sys.stderr.write('Writing pipe: {}\n'.format(' '.join(c)))
  
 -    expand = not isinstance(c, list)
 -    p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
 +    p = subprocess.Popen(c, stdin=subprocess.PIPE, *k, **kw)
      pipe = p.stdin
      val = pipe.write(stdin)
      pipe.close()
      if p.wait():
 -        die('Command failed: %s' % str(c))
 +        die('Command failed: {}'.format(' '.join(c)))
  
      return val
  
 -def p4_write_pipe(c, stdin):
 +def p4_write_pipe(c, stdin, *k, **kw):
      real_cmd = p4_build_cmd(c)
      if bytes is not str and isinstance(stdin, str):
          stdin = encode_text_stream(stdin)
 -    return write_pipe(real_cmd, stdin)
 +    return write_pipe(real_cmd, stdin, *k, **kw)
  
 -def read_pipe_full(c):
 +def read_pipe_full(c, *k, **kw):
      """ Read output from  command. Returns a tuple
          of the return status, stdout text and stderr
          text.
      """
      if verbose:
 -        sys.stderr.write('Reading pipe: %s\n' % str(c))
 +        sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c)))
  
 -    expand = not isinstance(c, list)
 -    p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
 +    p = subprocess.Popen(
 +        c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, *k, **kw)
      (out, err) = p.communicate()
      return (p.returncode, out, decode_text_stream(err))
  
 -def read_pipe(c, ignore_error=False, raw=False):
 +def read_pipe(c, ignore_error=False, raw=False, *k, **kw):
      """ Read output from  command. Returns the output text on
          success. On failure, terminates execution, unless
          ignore_error is True, when it returns an empty string.
  
          If raw is True, do not attempt to decode output text.
      """
 -    (retcode, out, err) = read_pipe_full(c)
 +    (retcode, out, err) = read_pipe_full(c, *k, **kw)
      if retcode != 0:
          if ignore_error:
              out = ""
          else:
 -            die('Command failed: %s\nError: %s' % (str(c), err))
 +            die('Command failed: {}\nError: {}'.format(' '.join(c), err))
      if not raw:
          out = decode_text_stream(out)
      return out
  
 -def read_pipe_text(c):
 +def read_pipe_text(c, *k, **kw):
      """ Read output from a command with trailing whitespace stripped.
          On error, returns None.
      """
 -    (retcode, out, err) = read_pipe_full(c)
 +    (retcode, out, err) = read_pipe_full(c, *k, **kw)
      if retcode != 0:
          return None
      else:
          return decode_text_stream(out).rstrip()
  
 -def p4_read_pipe(c, ignore_error=False, raw=False):
 +def p4_read_pipe(c, ignore_error=False, raw=False, *k, **kw):
      real_cmd = p4_build_cmd(c)
 -    return read_pipe(real_cmd, ignore_error, raw=raw)
 +    return read_pipe(real_cmd, ignore_error, raw=raw, *k, **kw)
  
 -def read_pipe_lines(c):
 +def read_pipe_lines(c, raw=False, *k, **kw):
      if verbose:
 -        sys.stderr.write('Reading pipe: %s\n' % str(c))
 +        sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c)))
  
 -    expand = not isinstance(c, list)
 -    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
 +    p = subprocess.Popen(c, stdout=subprocess.PIPE, *k, **kw)
      pipe = p.stdout
 -    val = [decode_text_stream(line) for line in pipe.readlines()]
 +    lines = pipe.readlines()
 +    if not raw:
 +        lines = [decode_text_stream(line) for line in lines]
      if pipe.close() or p.wait():
 -        die('Command failed: %s' % str(c))
 -    return val
 +        die('Command failed: {}'.format(' '.join(c)))
 +    return lines
  
 -def p4_read_pipe_lines(c):
 +def p4_read_pipe_lines(c, *k, **kw):
      """Specifically invoke p4 on the command supplied. """
      real_cmd = p4_build_cmd(c)
 -    return read_pipe_lines(real_cmd)
 +    return read_pipe_lines(real_cmd, *k, **kw)
  
  def p4_has_command(cmd):
      """Ask p4 for help on this command.  If it returns an error, the
@@@ -395,22 -325,23 +337,22 @@@ def p4_has_move_command()
      # assume it failed because @... was invalid changelist
      return True
  
 -def system(cmd, ignore_error=False):
 -    expand = not isinstance(cmd, list)
 +def system(cmd, ignore_error=False, *k, **kw):
      if verbose:
 -        sys.stderr.write("executing %s\n" % str(cmd))
 -    retcode = subprocess.call(cmd, shell=expand)
 +        sys.stderr.write("executing {}\n".format(
 +            ' '.join(cmd) if isinstance(cmd, list) else cmd))
 +    retcode = subprocess.call(cmd, *k, **kw)
      if retcode and not ignore_error:
 -        raise CalledProcessError(retcode, cmd)
 +        raise subprocess.CalledProcessError(retcode, cmd)
  
      return retcode
  
 -def p4_system(cmd):
 +def p4_system(cmd, *k, **kw):
      """Specifically invoke p4 as the system command. """
      real_cmd = p4_build_cmd(cmd)
 -    expand = not isinstance(real_cmd, list)
 -    retcode = subprocess.call(real_cmd, shell=expand)
 +    retcode = subprocess.call(real_cmd, *k, **kw)
      if retcode:
 -        raise CalledProcessError(retcode, real_cmd)
 +        raise subprocess.CalledProcessError(retcode, real_cmd)
  
  def die_bad_access(s):
      die("failure accessing depot: {0}".format(s.rstrip()))
@@@ -588,12 -519,20 +530,12 @@@ def p4_type(f)
  #
  def p4_keywords_regexp_for_type(base, type_mods):
      if base in ("text", "unicode", "binary"):
 -        kwords = None
          if "ko" in type_mods:
 -            kwords = 'Id|Header'
 +            return re_ko_keywords
          elif "k" in type_mods:
 -            kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
 +            return re_k_keywords
          else:
              return None
 -        pattern = r"""
 -            \$              # Starts with a dollar, followed by...
 -            (%s)            # one of the keywords, followed by...
 -            (:[^$\n]+)?     # possibly an old expansion, followed by...
 -            \$              # another dollar
 -            """ % kwords
 -        return pattern
      else:
          return None
  
@@@ -729,11 -668,18 +671,11 @@@ def isModeExecChanged(src_mode, dst_mod
      return isModeExec(src_mode) != isModeExec(dst_mode)
  
  def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
 -        errors_as_exceptions=False):
 +        errors_as_exceptions=False, *k, **kw):
  
 -    if not isinstance(cmd, list):
 -        cmd = "-G " + cmd
 -        expand = True
 -    else:
 -        cmd = ["-G"] + cmd
 -        expand = False
 -
 -    cmd = p4_build_cmd(cmd)
 +    cmd = p4_build_cmd(["-G"] + cmd)
      if verbose:
 -        sys.stderr.write("Opening pipe: %s\n" % str(cmd))
 +        sys.stderr.write("Opening pipe: {}\n".format(' '.join(cmd)))
  
      # Use a temporary file to avoid deadlocks without
      # subprocess.communicate(), which would put another copy
          stdin_file.flush()
          stdin_file.seek(0)
  
 -    p4 = subprocess.Popen(cmd,
 -                          shell=expand,
 -                          stdin=stdin_file,
 -                          stdout=subprocess.PIPE)
 +    p4 = subprocess.Popen(
 +        cmd, stdin=stdin_file, stdout=subprocess.PIPE, *k, **kw)
  
      result = []
      try:
  
      return result
  
 -def p4Cmd(cmd):
 -    list = p4CmdList(cmd)
 +def p4Cmd(cmd, *k, **kw):
 +    list = p4CmdList(cmd, *k, **kw)
      result = {}
      for entry in list:
          result.update(entry)
@@@ -854,7 -802,7 +796,7 @@@ def isValidGitDir(path)
      return git_dir(path) != None
  
  def parseRevision(ref):
 -    return read_pipe("git rev-parse %s" % ref).strip()
 +    return read_pipe(["git", "rev-parse", ref]).strip()
  
  def branchExists(ref):
      rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
@@@ -960,11 -908,11 +902,11 @@@ def p4BranchesInGit(branchesAreInRemote
  
      branches = {}
  
 -    cmdline = "git rev-parse --symbolic "
 +    cmdline = ["git", "rev-parse", "--symbolic"]
      if branchesAreInRemotes:
 -        cmdline += "--remotes"
 +        cmdline.append("--remotes")
      else:
 -        cmdline += "--branches"
 +        cmdline.append("--branches")
  
      for line in read_pipe_lines(cmdline):
          line = line.strip()
@@@ -1029,7 -977,7 +971,7 @@@ def createOrUpdateBranchesFromOrigin(lo
  
      originPrefix = "origin/p4/"
  
 -    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
 +    for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
          line = line.strip()
          if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
              continue
                                remoteHead, ','.join(settings['depot-paths'])))
  
          if update:
 -            system("git update-ref %s %s" % (remoteHead, originHead))
 +            system(["git", "update-ref", remoteHead, originHead])
  
  def originP4BranchesExist():
          return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
@@@ -1181,7 -1129,7 +1123,7 @@@ def getClientSpec()
      """Look at the p4 client spec, create a View() object that contains
         all the mappings, and return it."""
  
 -    specList = p4CmdList("client -o")
 +    specList = p4CmdList(["client", "-o"])
      if len(specList) != 1:
          die('Output from "client -o" is %d lines, expecting 1' %
              len(specList))
  def getClientRoot():
      """Grab the client directory."""
  
 -    output = p4CmdList("client -o")
 +    output = p4CmdList(["client", "-o"])
      if len(output) != 1:
          die('Output from "client -o" is %d lines, expecting 1' % len(output))
  
@@@ -1465,7 -1413,7 +1407,7 @@@ class P4UserMap
          if self.myP4UserId:
              return self.myP4UserId
  
 -        results = p4CmdList("user -o")
 +        results = p4CmdList(["user", "-o"])
          for r in results:
              if 'User' in r:
                  self.myP4UserId = r['User']
          self.users = {}
          self.emails = {}
  
 -        for output in p4CmdList("users"):
 +        for output in p4CmdList(["users"]):
              if "User" not in output:
                  continue
              self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
          except IOError:
              self.getUserMapFromPerforceServer()
  
 -class P4Debug(Command):
 -    def __init__(self):
 -        Command.__init__(self)
 -        self.options = []
 -        self.description = "A tool to debug the output of p4 -G."
 -        self.needsGit = False
 -
 -    def run(self, args):
 -        j = 0
 -        for output in p4CmdList(args):
 -            print('Element: %d' % j)
 -            j += 1
 -            print(output)
 -        return True
 -
 -class P4RollBack(Command):
 -    def __init__(self):
 -        Command.__init__(self)
 -        self.options = [
 -            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
 -        ]
 -        self.description = "A tool to debug the multi-branch import. Don't use :)"
 -        self.rollbackLocalBranches = False
 -
 -    def run(self, args):
 -        if len(args) != 1:
 -            return False
 -        maxChange = int(args[0])
 -
 -        if "p4ExitCode" in p4Cmd("changes -m 1"):
 -            die("Problems executing p4");
 -
 -        if self.rollbackLocalBranches:
 -            refPrefix = "refs/heads/"
 -            lines = read_pipe_lines("git rev-parse --symbolic --branches")
 -        else:
 -            refPrefix = "refs/remotes/"
 -            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
 -
 -        for line in lines:
 -            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
 -                line = line.strip()
 -                ref = refPrefix + line
 -                log = extractLogMessageFromGitCommit(ref)
 -                settings = extractSettingsGitLog(log)
 -
 -                depotPaths = settings['depot-paths']
 -                change = settings['change']
 -
 -                changed = False
 -
 -                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
 -                                                           for p in depotPaths]))) == 0:
 -                    print("Branch %s did not exist at change %s, deleting." % (ref, maxChange))
 -                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
 -                    continue
 -
 -                while change and int(change) > maxChange:
 -                    changed = True
 -                    if self.verbose:
 -                        print("%s is at %s ; rewinding towards %s" % (ref, change, maxChange))
 -                    system("git update-ref %s \"%s^\"" % (ref, ref))
 -                    log = extractLogMessageFromGitCommit(ref)
 -                    settings =  extractSettingsGitLog(log)
 -
 -
 -                    depotPaths = settings['depot-paths']
 -                    change = settings['change']
 -
 -                if changed:
 -                    print("%s rewound to %s" % (ref, change))
 -
 -        return True
 -
  class P4Submit(Command, P4UserMap):
  
      conflict_behavior_choices = ("ask", "skip", "quit")
              die("Large file system not supported for git-p4 submit command. Please remove it from config.")
  
      def check(self):
 -        if len(p4CmdList("opened ...")) > 0:
 +        if len(p4CmdList(["opened", "..."])) > 0:
              die("You have files opened with perforce! Close them before starting the sync.")
  
      def separate_jobs_from_description(self, message):
  
          return result
  
 -    def patchRCSKeywords(self, file, pattern):
 -        # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
 +    def patchRCSKeywords(self, file, regexp):
 +        # Attempt to zap the RCS keywords in a p4 controlled file matching the given regex
          (handle, outFileName) = tempfile.mkstemp(dir='.')
          try:
 -            outFile = os.fdopen(handle, "w+")
 -            inFile = open(file, "r")
 -            regexp = re.compile(pattern, re.VERBOSE)
 -            for line in inFile.readlines():
 -                line = regexp.sub(r'$\1$', line)
 -                outFile.write(line)
 -            inFile.close()
 -            outFile.close()
 +            with os.fdopen(handle, "wb") as outFile, open(file, "rb") as inFile:
 +                for line in inFile.readlines():
 +                    outFile.write(regexp.sub(br'$\1$', line))
              # Forcibly overwrite the original file
              os.unlink(file)
              shutil.move(outFileName, file)
          # then gets used to patch up the username in the change. If the same
          # client spec is being used by multiple processes then this might go
          # wrong.
 -        results = p4CmdList("client -o")        # find the current client
 +        results = p4CmdList(["client", "-o"])        # find the current client
          client = None
          for r in results:
              if 'Client' in r:
  
      def modifyChangelistUser(self, changelist, newUser):
          # fixup the user field of a changelist after it has been submitted.
 -        changes = p4CmdList("change -o %s" % changelist)
 +        changes = p4CmdList(["change", "-o", changelist])
          if len(changes) != 1:
              die("Bad output from p4 change modifying %s to user %s" %
                  (changelist, newUser))
          # p4 does not understand format version 3 and above
          input = marshal.dumps(c, 2)
  
 -        result = p4CmdList("change -f -i", stdin=input)
 +        result = p4CmdList(["change", "-f", "-i"], stdin=input)
          for r in result:
              if 'code' in r:
                  if r['code'] == 'error':
          if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
              editor = os.environ.get("P4EDITOR")
          else:
 -            editor = read_pipe("git var GIT_EDITOR").strip()
 +            editor = read_pipe(["git", "var", "GIT_EDITOR"]).strip()
          system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
  
          # If the file was not saved, prompt to see if this patch should
  
          (p4User, gitEmail) = self.p4UserForCommit(id)
  
 -        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
 +        diff = read_pipe_lines(
 +            ["git", "diff-tree", "-r"] + self.diffOpts + ["{}^".format(id), id])
          filesToAdd = set()
          filesToChangeType = set()
          filesToDelete = set()
              # the patch to see if that's possible.
              if gitConfigBool("git-p4.attemptRCSCleanup"):
                  file = None
 -                pattern = None
                  kwfiles = {}
                  for file in editedFiles | filesToDelete:
                      # did this file's delta contain RCS keywords?
 -                    pattern = p4_keywords_regexp_for_file(file)
 -
 -                    if pattern:
 +                    regexp = p4_keywords_regexp_for_file(file)
 +                    if regexp:
                          # this file is a possibility...look for RCS keywords.
 -                        regexp = re.compile(pattern, re.VERBOSE)
 -                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
 +                        for line in read_pipe_lines(
 +                            ["git", "diff", "%s^..%s" % (id, id), file],
 +                            raw=True):
                              if regexp.search(line):
                                  if verbose:
 -                                    print("got keyword match on %s in %s in %s" % (pattern, line, file))
 -                                kwfiles[file] = pattern
 +                                    print("got keyword match on %s in %s in %s" % (regex.pattern, line, file))
 +                                kwfiles[file] = regexp
                                  break
  
 -                for file in kwfiles:
 +                for file, regexp in kwfiles.items():
                      if verbose:
 -                        print("zapping %s with %s" % (line,pattern))
 +                        print("zapping %s with %s" % (line, regexp.pattern))
                      # File is being deleted, so not open in p4.  Must
                      # disable the read-only bit on windows.
                      if self.isWindows and file not in editedFiles:
          #
          # Apply the patch for real, and do add/delete/+x handling.
          #
 -        system(applyPatchCmd)
 +        system(applyPatchCmd, shell=True)
  
          for f in filesToChangeType:
              p4_edit(f, "-t", "auto")
          #
          if self.detectRenames:
              # command-line -M arg
 -            self.diffOpts = "-M"
 +            self.diffOpts = ["-M"]
          else:
              # If not explicitly set check the config variable
              detectRenames = gitConfig("git-p4.detectRenames")
  
              if detectRenames.lower() == "false" or detectRenames == "":
 -                self.diffOpts = ""
 +                self.diffOpts = []
              elif detectRenames.lower() == "true":
 -                self.diffOpts = "-M"
 +                self.diffOpts = ["-M"]
              else:
 -                self.diffOpts = "-M%s" % detectRenames
 +                self.diffOpts = ["-M{}".format(detectRenames)]
  
          # no command-line arg for -C or --find-copies-harder, just
          # config variables
          if detectCopies.lower() == "false" or detectCopies == "":
              pass
          elif detectCopies.lower() == "true":
 -            self.diffOpts += " -C"
 +            self.diffOpts.append("-C")
          else:
 -            self.diffOpts += " -C%s" % detectCopies
 +            self.diffOpts.append("-C{}".format(detectCopies))
  
          if gitConfigBool("git-p4.detectCopiesHarder"):
 -            self.diffOpts += " --find-copies-harder"
 +            self.diffOpts.append("--find-copies-harder")
  
          num_shelves = len(self.update_shelve)
          if num_shelves > 0 and num_shelves != len(commits):
@@@ -2881,8 -2908,7 +2823,8 @@@ class P4Sync(Command, P4UserMap)
                  size = int(self.stream_file['fileSize'])
              else:
                  size = 0 # deleted files don't get a fileSize apparently
 -            sys.stdout.write('\r%s --> %s (%i MB)\n' % (file_path, relPath, size/1024/1024))
 +            sys.stdout.write('\r%s --> %s (%s)\n' % (
 +                file_path, relPath, format_size_human_readable(size)))
              sys.stdout.flush()
  
          (type_base, type_mods) = split_p4_type(file["type"])
  
          # Note that we do not try to de-mangle keywords on utf16 files,
          # even though in theory somebody may want that.
 -        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
 -        if pattern:
 -            regexp = re.compile(pattern, re.VERBOSE)
 -            text = ''.join(decode_text_stream(c) for c in contents)
 -            text = regexp.sub(r'$\1$', text)
 -            contents = [ encode_text_stream(text) ]
 +        regexp = p4_keywords_regexp_for_type(type_base, type_mods)
 +        if regexp:
 +            contents = [regexp.sub(br'$\1$', c) for c in contents]
  
          if self.largeFileSystem:
              (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
          if not err and 'fileSize' in self.stream_file:
              required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
              if required_bytes > 0:
 -                err = 'Not enough space left on %s! Free at least %i MB.' % (
 -                    os.getcwd(), required_bytes/1024/1024
 -                )
 +                err = 'Not enough space left on %s! Free at least %s.' % (
 +                    os.getcwd(), format_size_human_readable(required_bytes))
  
          if err:
              f = None
              size = int(self.stream_file["fileSize"])
              if size > 0:
                  progress = 100*self.stream_file['streamContentSize']/size
 -                sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
 +                sys.stdout.write('\r%s %d%% (%s)' % (
 +                    self.stream_file['depotFile'], progress,
 +                    format_size_human_readable(size)))
                  sys.stdout.flush()
  
          self.stream_have_file_info = True
          lostAndFoundBranches = set()
  
          user = gitConfig("git-p4.branchUser")
 -        if len(user) > 0:
 -            command = "branches -u %s" % user
 -        else:
 -            command = "branches"
  
 -        for info in p4CmdList(command):
 +        for info in p4CmdList(
 +            ["branches"] + (["-u", user] if len(user) > 0 else [])):
              details = p4Cmd(["branch", "-o", info["branch"]])
              viewIdx = 0
              while "View%s" % viewIdx in details:
          while True:
              if self.verbose:
                  print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
 -            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
 +            next = read_pipe(["git", "rev-list", "--bisect",
 +                latestCommit, earliestCommit]).strip()
              if len(next) == 0:
                  if self.verbose:
                      print("argh")
              self.updateOptionDict(description)
  
              if not self.silent:
 -                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
 +                sys.stdout.write("\rImporting revision %s (%d%%)" % (
 +                    change, (cnt * 100) // len(changes)))
                  sys.stdout.flush()
              cnt = cnt + 1
  
              if self.hasOrigin:
                  if not self.silent:
                      print('Syncing with origin first, using "git fetch origin"')
 -                system("git fetch origin")
 +                system(["git", "fetch", "origin"])
  
      def importHeadRevision(self, revision):
          print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
          if len(self.branch) == 0:
              self.branch = self.refPrefix + "master"
              if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
 -                system("git update-ref %s refs/heads/p4" % self.branch)
 -                system("git branch -D p4")
 +                system(["git", "update-ref", self.branch, "refs/heads/p4"])
 +                system(["git", "branch", "-D", "p4"])
  
          # accept either the command-line option, or the configuration variable
          if self.useClientSpec:
          # Cleanup temporary branches created during import
          if self.tempBranches != []:
              for branch in self.tempBranches:
 -                read_pipe("git update-ref -d %s" % branch)
 +                read_pipe(["git", "update-ref", "-d", branch])
              os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
  
          # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
@@@ -4020,7 -4049,7 +3962,7 @@@ class P4Rebase(Command)
      def rebase(self):
          if os.system("git update-index --refresh") != 0:
              die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up to date or stash away all your changes with git stash.");
 -        if len(read_pipe("git diff-index HEAD --")) > 0:
 +        if len(read_pipe(["git", "diff-index", "HEAD", "--"])) > 0:
              die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
  
          [upstream, settings] = findUpstreamBranchPoint()
          upstream = re.sub("~[0-9]+$", "", upstream)
  
          print("Rebasing the current branch onto %s" % upstream)
 -        oldHead = read_pipe("git rev-parse HEAD").strip()
 -        system("git rebase %s" % upstream)
 -        system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
 +        oldHead = read_pipe(["git", "rev-parse", "HEAD"]).strip()
 +        system(["git", "rebase", upstream])
 +        system(["git", "diff-tree", "--stat", "--summary", "-M", oldHead,
 +            "HEAD", "--"])
          return True
  
  class P4Clone(P4Sync):
              init_cmd.append("--bare")
          retcode = subprocess.call(init_cmd)
          if retcode:
 -            raise CalledProcessError(retcode, init_cmd)
 +            raise subprocess.CalledProcessError(retcode, init_cmd)
  
          if not P4Sync.run(self, depotPaths):
              return False
  
          # auto-set this variable if invoked with --use-client-spec
          if self.useClientSpec_from_options:
 -            system("git config --bool git-p4.useclientspec true")
 +            system(["git", "config", "--bool", "git-p4.useclientspec", "true"])
  
          return True
  
@@@ -4242,7 -4270,10 +4184,7 @@@ class P4Branches(Command)
          if originP4BranchesExist():
              createOrUpdateBranchesFromOrigin()
  
 -        cmdline = "git rev-parse --symbolic "
 -        cmdline += " --remotes"
 -
 -        for line in read_pipe_lines(cmdline):
 +        for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
              line = line.strip()
  
              if not line.startswith('p4/') or line == "p4/HEAD":
@@@ -4274,11 -4305,13 +4216,11 @@@ def printUsage(commands)
      print("")
  
  commands = {
 -    "debug" : P4Debug,
      "submit" : P4Submit,
      "commit" : P4Submit,
      "sync" : P4Sync,
      "rebase" : P4Rebase,
      "clone" : P4Clone,
 -    "rollback" : P4RollBack,
      "branches" : P4Branches,
      "unshelve" : P4Unshelve,
  }
@@@ -4325,9 -4358,9 +4267,9 @@@ def main()
              cmd.gitdir = os.path.abspath(".git")
              if not isValidGitDir(cmd.gitdir):
                  # "rev-parse --git-dir" without arguments will try $PWD/.git
 -                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
 +                cmd.gitdir = read_pipe(["git", "rev-parse", "--git-dir"]).strip()
                  if os.path.exists(cmd.gitdir):
 -                    cdup = read_pipe("git rev-parse --show-cdup").strip()
 +                    cdup = read_pipe(["git", "rev-parse", "--show-cdup"]).strip()
                      if len(cdup) > 0:
                          chdir(cdup);
  
diff --combined git.c
index edda922ce6d423b8b734b47b979f4912b9a4d281,7030f1180b3739f7b1e7f0b42172b21be4e0b44f..340665d4a044dbdb71981b0699102efa97895556
--- 1/git.c
--- 2/git.c
+++ b/git.c
@@@ -185,7 -185,7 +185,7 @@@ static int handle_options(const char **
                                *envchanged = 1;
                } else if (!strcmp(cmd, "--git-dir")) {
                        if (*argc < 2) {
 -                              fprintf(stderr, _("no directory given for --git-dir\n" ));
 +                              fprintf(stderr, _("no directory given for '%s' option\n" ), "--git-dir");
                                usage(git_usage_string);
                        }
                        setenv(GIT_DIR_ENVIRONMENT, (*argv)[1], 1);
                                *envchanged = 1;
                } else if (!strcmp(cmd, "--work-tree")) {
                        if (*argc < 2) {
 -                              fprintf(stderr, _("no directory given for --work-tree\n" ));
 +                              fprintf(stderr, _("no directory given for '%s' option\n" ), "--work-tree");
                                usage(git_usage_string);
                        }
                        setenv(GIT_WORK_TREE_ENVIRONMENT, (*argv)[1], 1);
                                *envchanged = 1;
                } else if (!strcmp(cmd, "-C")) {
                        if (*argc < 2) {
 -                              fprintf(stderr, _("no directory given for -C\n" ));
 +                              fprintf(stderr, _("no directory given for '%s' option\n" ), "-C");
                                usage(git_usage_string);
                        }
                        if ((*argv)[1][0]) {
@@@ -541,6 -541,7 +541,7 @@@ static struct cmd_struct commands[] = 
        { "grep", cmd_grep, RUN_SETUP_GENTLY },
        { "hash-object", cmd_hash_object },
        { "help", cmd_help },
+       { "hook", cmd_hook, RUN_SETUP },
        { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
        { "init", cmd_init_db },
        { "init-db", cmd_init_db },
diff --combined read-cache.c
index ed297635a33160c6d61d0483fccb060ff719f507,a7fbf144fecd5ce60a5df9c3e036b85568671157..eeb1c9e8f75c9dbf5b7a2d999737e3d87fe63f2f
@@@ -28,6 -28,7 +28,7 @@@
  #include "sparse-index.h"
  #include "csum-file.h"
  #include "promisor-remote.h"
+ #include "hook.h"
  
  /* Mask for the name length in ce_flags in the on-disk index */
  
@@@ -2775,7 -2776,7 +2776,7 @@@ static int repo_verify_index(struct rep
        return verify_index_from(repo->index, repo->index_file);
  }
  
 -static int has_racy_timestamp(struct index_state *istate)
 +int has_racy_timestamp(struct index_state *istate)
  {
        int entries = istate->cache_nr;
        int i;
@@@ -3150,7 -3151,7 +3151,7 @@@ static int do_write_locked_index(struc
        else
                ret = close_lock_file_gently(lock);
  
-       run_hook_le(NULL, "post-index-change",
+       run_hooks_l("post-index-change",
                        istate->updated_workdir ? "1" : "0",
                        istate->updated_skipworktree ? "1" : "0", NULL);
        istate->updated_workdir = 0;
diff --combined run-command.c
index 69dde42f1e7f5e5886f2ea2d9caba02dd9ab8eff,f32e4fe1326649df7007ff1f8a1dd3b725276a6d..a8501e38cebe50f6a1fefb6d31d92ce049b96ac3
@@@ -340,6 -340,15 +340,6 @@@ static void child_close_pair(int fd[2]
        child_close(fd[1]);
  }
  
 -/*
 - * parent will make it look like the child spewed a fatal error and died
 - * this is needed to prevent changes to t0061.
 - */
 -static void fake_fatal(const char *err, va_list params)
 -{
 -      vreportf("fatal: ", err, params);
 -}
 -
  static void child_error_fn(const char *err, va_list params)
  {
        const char msg[] = "error() should not be called in child\n";
@@@ -363,10 -372,9 +363,10 @@@ static void NORETURN child_die_fn(cons
  static void child_err_spew(struct child_process *cmd, struct child_err *cerr)
  {
        static void (*old_errfn)(const char *err, va_list params);
 +      report_fn die_message_routine = get_die_message_routine();
  
        old_errfn = get_error_routine();
 -      set_error_routine(fake_fatal);
 +      set_error_routine(die_message_routine);
        errno = cerr->syserr;
  
        switch (cerr->err) {
@@@ -1064,9 -1072,7 +1064,9 @@@ static void *run_thread(void *data
  
  static NORETURN void die_async(const char *err, va_list params)
  {
 -      vreportf("fatal: ", err, params);
 +      report_fn die_message_fn = get_die_message_routine();
 +
 +      die_message_fn(err, params);
  
        if (in_async()) {
                struct async *async = pthread_getspecific(async_key);
@@@ -1307,39 -1313,6 +1307,6 @@@ int async_with_fork(void
  #endif
  }
  
- int run_hook_ve(const char *const *env, const char *name, va_list args)
- {
-       struct child_process hook = CHILD_PROCESS_INIT;
-       const char *p;
-       p = find_hook(name);
-       if (!p)
-               return 0;
-       strvec_push(&hook.args, p);
-       while ((p = va_arg(args, const char *)))
-               strvec_push(&hook.args, p);
-       if (env)
-               strvec_pushv(&hook.env_array, (const char **)env);
-       hook.no_stdin = 1;
-       hook.stdout_to_stderr = 1;
-       hook.trace2_hook_name = name;
-       return run_command(&hook);
- }
- int run_hook_le(const char *const *env, const char *name, ...)
- {
-       va_list args;
-       int ret;
-       va_start(args, name);
-       ret = run_hook_ve(env, name, args);
-       va_end(args);
-       return ret;
- }
  struct io_pump {
        /* initialized by caller */
        int fd;