Coverage Report

Created: 2026-11-12 06:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/git/hook.c
Line
Count
Source
1
#include "git-compat-util.h"
2
#include "abspath.h"
3
#include "advice.h"
4
#include "config.h"
5
#include "environment.h"
6
#include "gettext.h"
7
#include "hook.h"
8
#include "hook-list.h"
9
#include "parse.h"
10
#include "path.h"
11
#include "run-command.h"
12
#include "setup.h"
13
#include "strbuf.h"
14
#include "strmap.h"
15
#include "thread-utils.h"
16
17
bool is_known_hook(const char *name)
18
0
{
19
0
  const char **h;
20
0
  for (h = hook_name_list; *h; h++)
21
0
    if (!strcmp(*h, name))
22
0
      return true;
23
0
  return false;
24
0
}
25
26
const char *find_hook(struct repository *r, const char *name)
27
0
{
28
0
  static struct strbuf path = STRBUF_INIT;
29
30
0
  int found_hook;
31
32
0
  if (!r || !r->gitdir)
33
0
    return NULL;
34
35
0
  repo_git_path_replace(r, &path, "hooks/%s", name);
36
0
  found_hook = access(path.buf, X_OK) >= 0;
37
#ifdef STRIP_EXTENSION
38
  if (!found_hook) {
39
    int err = errno;
40
41
    strbuf_addstr(&path, STRIP_EXTENSION);
42
    found_hook = access(path.buf, X_OK) >= 0;
43
    if (!found_hook)
44
      errno = err;
45
  }
46
#endif
47
48
0
  if (!found_hook) {
49
0
    if (errno == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) {
50
0
      static struct string_list advise_given = STRING_LIST_INIT_DUP;
51
52
0
      if (!string_list_lookup(&advise_given, name)) {
53
0
        string_list_insert(&advise_given, name);
54
0
        advise(_("The '%s' hook was ignored because "
55
0
           "it's not set as executable.\n"
56
0
           "You can disable this warning with "
57
0
           "`git config set advice.ignoredHook false`."),
58
0
               path.buf);
59
0
      }
60
0
    }
61
0
    return NULL;
62
0
  }
63
0
  return path.buf;
64
0
}
65
66
void hook_free(void *p, const char *str UNUSED)
67
0
{
68
0
  struct hook *h = p;
69
70
0
  if (!h)
71
0
    return;
72
73
0
  if (h->kind == HOOK_TRADITIONAL) {
74
0
    free((void *)h->u.traditional.path);
75
0
  } else if (h->kind == HOOK_CONFIGURED) {
76
0
    free((void *)h->u.configured.friendly_name);
77
0
    free((void *)h->u.configured.command);
78
0
  }
79
80
0
  if (h->data_free && h->feed_pipe_cb_data)
81
0
    h->data_free(h->feed_pipe_cb_data);
82
83
0
  free(h);
84
0
}
85
86
/* Helper to detect and add default "traditional" hooks from the hookdir. */
87
static void list_hooks_add_default(struct repository *r, const char *hookname,
88
           struct string_list *hook_list,
89
           struct run_hooks_opt *options)
90
0
{
91
0
  const char *hook_path = find_hook(r, hookname);
92
0
  struct hook *h;
93
94
0
  if (!hook_path)
95
0
    return;
96
97
0
  CALLOC_ARRAY(h, 1);
98
99
  /*
100
   * If the hook is to run in a specific dir, a relative path can
101
   * become invalid in that dir, so convert to an absolute path.
102
   */
103
0
  if (options && options->dir)
104
0
    hook_path = absolute_path(hook_path);
105
106
  /*
107
   * Setup per-hook internal state callback data.
108
   * When provided, the alloc/free callbacks are always provided
109
   * together, so use them to alloc/free the internal hook state.
110
   */
111
0
  if (options && options->feed_pipe_cb_data_alloc) {
112
0
    h->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);
113
0
    h->data_free = options->feed_pipe_cb_data_free;
114
0
  }
115
116
0
  h->kind = HOOK_TRADITIONAL;
117
0
  h->u.traditional.path = xstrdup(hook_path);
118
  /* list_hooks_add_configured() has loaded r->disabled_events */
119
0
  h->event_disabled = !!unsorted_string_list_lookup(&r->disabled_events,
120
0
                hookname);
121
122
0
  string_list_append(hook_list, hook_path)->util = h;
123
0
}
124
125
/*
126
 * Cache entry stored as the .util pointer of string_list items inside the
127
 * hook config cache.
128
 */
129
struct hook_config_cache_entry {
130
  char *command;
131
  enum config_scope scope;
132
  bool disabled;
133
  bool parallel;
134
};
135
136
/*
137
 * Callback struct to collect all hook.* keys in a single config pass.
138
 * commands: friendly-name to command map.
139
 * event_hooks: event-name to list of friendly-names map.
140
 * disabled_hooks: set of all names with hook.<name>.enabled = false; after
141
 *                 parsing, names that are not friendly-names become event-level
142
 *                 disables stored in r->disabled_events. This collects all.
143
 * parallel_hooks: friendly-name to parallel flag.
144
 * event_jobs: event-name to per-event jobs count (stored as uintptr_t, NULL == unset).
145
 * jobs: value of the global hook.jobs key. Defaults to 0 if unset (stored in r->hook_jobs).
146
 */
147
struct hook_all_config_cb {
148
  struct strmap commands;
149
  struct strmap event_hooks;
150
  struct string_list disabled_hooks;
151
  struct strmap parallel_hooks;
152
  struct strmap event_jobs;
153
  unsigned int jobs;
154
};
155
156
/* repo_config() callback that collects all hook.* configuration in one pass. */
157
static int hook_config_lookup_all(const char *key, const char *value,
158
          const struct config_context *ctx,
159
          void *cb_data)
160
0
{
161
0
  struct hook_all_config_cb *data = cb_data;
162
0
  const char *name, *subkey;
163
0
  char *hook_name;
164
0
  size_t name_len = 0;
165
166
0
  if (parse_config_key(key, "hook", &name, &name_len, &subkey))
167
0
    return 0;
168
169
  /* Handle plain hook.<key> entries that have no hook name component. */
170
0
  if (!name) {
171
0
    if (!strcmp(subkey, "jobs") && value) {
172
0
      int v;
173
0
      if (!git_parse_int(value, &v))
174
0
        warning(_("hook.jobs must be an integer, ignoring: '%s'"), value);
175
0
      else if (v == -1)
176
0
        data->jobs = online_cpus();
177
0
      else if (v > 0)
178
0
        data->jobs = v;
179
0
      else
180
0
        warning(_("hook.jobs must be a positive integer"
181
0
            " or -1, ignoring: '%s'"),
182
0
          value);
183
0
    }
184
0
    return 0;
185
0
  }
186
187
  /* .enabled and .parallel are bools, where a missing value means true */
188
0
  if (!value && strcmp(subkey, "enabled") && strcmp(subkey, "parallel"))
189
0
    return config_error_nonbool(key);
190
191
  /* Extract name, ensuring it is null-terminated. */
192
0
  hook_name = xmemdupz(name, name_len);
193
194
0
  if (!strcmp(subkey, "event")) {
195
0
    if (!*value) {
196
      /* Empty values reset previous events for this hook. */
197
0
      struct hashmap_iter iter;
198
0
      struct strmap_entry *e;
199
200
0
      strmap_for_each_entry(&data->event_hooks, &iter, e)
201
0
        unsorted_string_list_remove(e->value, hook_name, 0);
202
0
    } else {
203
0
      struct string_list *hooks;
204
205
0
      if (is_known_hook(hook_name))
206
0
        die(_("hook friendly-name '%s' collides with "
207
0
              "a known event name; please choose a "
208
0
              "different friendly-name"),
209
0
            hook_name);
210
211
0
      if (!strcmp(hook_name, value))
212
0
        warning(_("hook friendly-name '%s' is the "
213
0
            "same as its event; this may cause "
214
0
            "ambiguity with hook.%s.enabled"),
215
0
          hook_name, hook_name);
216
217
0
      hooks = strmap_get(&data->event_hooks, value);
218
219
0
      if (!hooks) {
220
0
        CALLOC_ARRAY(hooks, 1);
221
0
        string_list_init_dup(hooks);
222
0
        strmap_put(&data->event_hooks, value, hooks);
223
0
      }
224
225
      /* Re-insert if necessary to preserve last-seen order. */
226
0
      unsorted_string_list_remove(hooks, hook_name, 0);
227
228
0
      if (!ctx->kvi)
229
0
        BUG("hook config callback called without key-value info");
230
231
      /*
232
       * Stash the config scope in the util pointer for
233
       * later retrieval in build_hook_config_map(). This
234
       * intermediate struct is transient and never leaves
235
       * that function, so we pack the enum value into the
236
       * pointer rather than heap-allocating a wrapper.
237
       */
238
0
      string_list_append(hooks, hook_name)->util =
239
0
        (void *)(uintptr_t)ctx->kvi->scope;
240
0
    }
241
0
  } else if (!strcmp(subkey, "command")) {
242
    /* Store command overwriting the old value */
243
0
    char *old = strmap_put(&data->commands, hook_name,
244
0
               xstrdup(value));
245
0
    free(old);
246
0
  } else if (!strcmp(subkey, "enabled")) {
247
0
    switch (git_parse_maybe_bool(value)) {
248
0
    case 0: /* disabled */
249
0
      if (!unsorted_string_list_lookup(&data->disabled_hooks,
250
0
               hook_name))
251
0
        string_list_append(&data->disabled_hooks,
252
0
               hook_name);
253
0
      break;
254
0
    case 1: /* enabled: undo a prior disabled entry */
255
0
      unsorted_string_list_remove(&data->disabled_hooks,
256
0
                hook_name, 0);
257
0
      break;
258
0
    default:
259
0
      break; /* ignore unrecognised values */
260
0
    }
261
0
  } else if (!strcmp(subkey, "parallel")) {
262
0
    int v = git_parse_maybe_bool(value);
263
0
    if (v >= 0)
264
0
      strmap_put(&data->parallel_hooks, hook_name,
265
0
           (void *)(uintptr_t)v);
266
0
    else
267
0
      warning(_("hook.%s.parallel must be a boolean,"
268
0
          " ignoring: '%s'"),
269
0
        hook_name, value);
270
0
  } else if (!strcmp(subkey, "jobs")) {
271
0
    int v;
272
0
    if (!git_parse_int(value, &v))
273
0
      warning(_("hook.%s.jobs must be an integer,"
274
0
          " ignoring: '%s'"),
275
0
        hook_name, value);
276
0
    else if (v == -1)
277
0
      strmap_put(&data->event_jobs, hook_name,
278
0
           (void *)(uintptr_t)online_cpus());
279
0
    else if (v > 0)
280
0
      strmap_put(&data->event_jobs, hook_name,
281
0
           (void *)(uintptr_t)v);
282
0
    else
283
0
      warning(_("hook.%s.jobs must be a positive"
284
0
          " integer or -1, ignoring: '%s'"),
285
0
        hook_name, value);
286
0
  }
287
288
0
  free(hook_name);
289
0
  return 0;
290
0
}
291
292
/*
293
 * The hook config cache maps each hook event name to a string_list where
294
 * every item's string is the hook's friendly-name and its util pointer is
295
 * the corresponding command string. Both strings are owned by the map.
296
 *
297
 * Disabled hooks are kept in the cache with entry->disabled set, so that
298
 * "git hook list" can display them. A non-disabled hook missing a command
299
 * is fatal; a disabled hook missing a command emits a warning and is kept
300
 * in the cache with entry->command = NULL.
301
 */
302
void hook_cache_clear(struct strmap *cache)
303
0
{
304
0
  struct hashmap_iter iter;
305
0
  struct strmap_entry *e;
306
307
0
  strmap_for_each_entry(cache, &iter, e) {
308
0
    struct string_list *hooks = e->value;
309
0
    for (size_t i = 0; i < hooks->nr; i++) {
310
0
      struct hook_config_cache_entry *entry = hooks->items[i].util;
311
0
      free(entry->command);
312
0
      free(entry);
313
0
    }
314
0
    string_list_clear(hooks, 0);
315
0
    free(hooks);
316
0
  }
317
0
  strmap_clear(cache, 0);
318
0
}
319
320
/*
321
 * Return true if `name` is a hook friendly-name, i.e. it has at least one of
322
 * .command, .event, or .parallel configured. These are the reliable clues
323
 * that distinguish a friendly-name from an event name. Note: .enabled is
324
 * deliberately excluded because it can appear under both namespaces.
325
 */
326
static int is_friendly_name(struct hook_all_config_cb *cb, const char *name)
327
0
{
328
0
  struct hashmap_iter iter;
329
0
  struct strmap_entry *e;
330
331
0
  if (strmap_get(&cb->commands, name) || strmap_get(&cb->parallel_hooks, name))
332
0
    return 1;
333
334
0
  strmap_for_each_entry(&cb->event_hooks, &iter, e) {
335
0
    if (unsorted_string_list_lookup(e->value, name))
336
0
      return 1;
337
0
  }
338
339
0
  return 0;
340
0
}
341
342
/* Warn if any name in event_jobs is also a hook friendly-name. */
343
static void warn_jobs_on_friendly_names(struct hook_all_config_cb *cb_data)
344
0
{
345
0
  struct hashmap_iter iter;
346
0
  struct strmap_entry *e;
347
348
0
  strmap_for_each_entry(&cb_data->event_jobs, &iter, e) {
349
0
    if (is_friendly_name(cb_data, e->key))
350
0
      warning(_("hook.%s.jobs is set but '%s' looks like a "
351
0
          "hook friendly-name, not an event name; "
352
0
          "hook.<event>.jobs uses the event name "
353
0
          "(e.g. hook.post-receive.jobs), so this "
354
0
          "setting will be ignored"), e->key, e->key);
355
0
  }
356
0
}
357
358
/* Populate `cache` with the complete hook configuration */
359
static void build_hook_config_map(struct repository *r, struct strmap *cache)
360
0
{
361
0
  struct hook_all_config_cb cb_data = { 0 };
362
0
  struct hashmap_iter iter;
363
0
  struct strmap_entry *e;
364
365
0
  strmap_init(&cb_data.commands);
366
0
  strmap_init(&cb_data.event_hooks);
367
0
  string_list_init_dup(&cb_data.disabled_hooks);
368
0
  strmap_init(&cb_data.parallel_hooks);
369
0
  strmap_init(&cb_data.event_jobs);
370
371
  /* Parse all configs in one run, capturing hook.* including hook.jobs. */
372
0
  repo_config(r, hook_config_lookup_all, &cb_data);
373
374
0
  warn_jobs_on_friendly_names(&cb_data);
375
376
  /*
377
   * Populate disabled_events: names in disabled_hooks that are not
378
   * friendly-names are event-level switches (hook.<event>.enabled = false).
379
   * Names that are friendly-names are already handled per-hook via the
380
   * hook_config_cache_entry.disabled flag below.
381
   */
382
0
  if (r) {
383
0
    string_list_clear(&r->disabled_events, 0);
384
0
    string_list_init_dup(&r->disabled_events);
385
0
    for (size_t i = 0; i < cb_data.disabled_hooks.nr; i++) {
386
0
      const char *n = cb_data.disabled_hooks.items[i].string;
387
0
      if (!is_friendly_name(&cb_data, n))
388
0
        string_list_append(&r->disabled_events, n);
389
0
    }
390
0
  }
391
392
  /* Construct the cache from parsed configs. */
393
0
  strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
394
0
    struct string_list *hook_names = e->value;
395
0
    struct string_list *hooks;
396
397
0
    CALLOC_ARRAY(hooks, 1);
398
0
    string_list_init_dup(hooks);
399
400
0
    for (size_t i = 0; i < hook_names->nr; i++) {
401
0
      const char *hname = hook_names->items[i].string;
402
0
      enum config_scope scope =
403
0
        (enum config_scope)(uintptr_t)hook_names->items[i].util;
404
0
      struct hook_config_cache_entry *entry;
405
0
      char *command;
406
407
0
      bool is_par = !!strmap_get(&cb_data.parallel_hooks, hname);
408
0
      bool is_disabled =
409
0
        !!unsorted_string_list_lookup(
410
0
          &cb_data.disabled_hooks, hname);
411
412
0
      command = strmap_get(&cb_data.commands, hname);
413
0
      if (!command) {
414
0
        if (is_disabled)
415
0
          warning(_("disabled hook '%s' has no "
416
0
              "command configured"), hname);
417
0
        else
418
0
          die(_("'hook.%s.command' must be configured or "
419
0
                "'hook.%s.event' must be removed;"
420
0
                " aborting."), hname, hname);
421
0
      }
422
423
      /* util stores a cache entry; owned by the cache. */
424
0
      CALLOC_ARRAY(entry, 1);
425
0
      entry->command = xstrdup_or_null(command);
426
0
      entry->scope = scope;
427
0
      entry->disabled = is_disabled;
428
0
      entry->parallel = is_par;
429
0
      string_list_append(hooks, hname)->util = entry;
430
0
    }
431
432
0
    strmap_put(cache, e->key, hooks);
433
0
  }
434
435
0
  if (r) {
436
0
    r->hook_jobs = cb_data.jobs;
437
0
    r->event_jobs = cb_data.event_jobs;
438
0
  }
439
440
0
  strmap_clear(&cb_data.commands, 1);
441
0
  strmap_clear(&cb_data.parallel_hooks, 0); /* values are uintptr_t, not heap ptrs */
442
0
  string_list_clear(&cb_data.disabled_hooks, 0);
443
0
  strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
444
0
    string_list_clear(e->value, 0);
445
0
    free(e->value);
446
0
  }
447
0
  strmap_clear(&cb_data.event_hooks, 0);
448
0
}
449
450
/*
451
 * Return the hook config map for `r`, populating it first if needed.
452
 *
453
 * Out-of-repo calls (r->gitdir == NULL) allocate and return a temporary
454
 * cache map; the caller is responsible for freeing it with
455
 * hook_cache_clear() + free().
456
 */
457
static struct strmap *get_hook_config_cache(struct repository *r)
458
0
{
459
0
  struct strmap *cache = NULL;
460
461
0
  if (r && r->gitdir) {
462
    /*
463
     * For in-repo calls, the map is stored in r->hook_config_cache,
464
     * so repeated invocations don't parse the configs, so allocate
465
     * it just once on the first call.
466
     */
467
0
    if (!r->hook_config_cache) {
468
0
      CALLOC_ARRAY(r->hook_config_cache, 1);
469
0
      strmap_init(r->hook_config_cache);
470
0
      build_hook_config_map(r, r->hook_config_cache);
471
0
    }
472
0
    cache = r->hook_config_cache;
473
0
  } else {
474
    /*
475
     * Out-of-repo calls (no gitdir) allocate and return a temporary
476
     * cache which gets freed immediately by the caller.
477
     */
478
0
    CALLOC_ARRAY(cache, 1);
479
0
    strmap_init(cache);
480
0
    build_hook_config_map(r, cache);
481
0
  }
482
483
0
  return cache;
484
0
}
485
486
static void list_hooks_add_configured(struct repository *r,
487
              const char *hookname,
488
              struct string_list *list,
489
              struct run_hooks_opt *options)
490
0
{
491
0
  struct strmap *cache = get_hook_config_cache(r);
492
0
  struct string_list *configured_hooks = strmap_get(cache, hookname);
493
0
  bool event_is_disabled = r ? !!unsorted_string_list_lookup(&r->disabled_events,
494
0
                   hookname) : 0;
495
496
  /* Iterate through configured hooks and initialize internal states */
497
0
  for (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {
498
0
    const char *friendly_name = configured_hooks->items[i].string;
499
0
    struct hook_config_cache_entry *entry = configured_hooks->items[i].util;
500
0
    struct hook *hook;
501
502
0
    CALLOC_ARRAY(hook, 1);
503
504
    /*
505
     * When provided, the alloc/free callbacks are always provided
506
     * together, so use them to alloc/free the internal hook state.
507
     */
508
0
    if (options && options->feed_pipe_cb_data_alloc) {
509
0
      hook->feed_pipe_cb_data =
510
0
        options->feed_pipe_cb_data_alloc(
511
0
          options->feed_pipe_ctx);
512
0
      hook->data_free = options->feed_pipe_cb_data_free;
513
0
    }
514
515
0
    hook->kind = HOOK_CONFIGURED;
516
0
    hook->u.configured.friendly_name = xstrdup(friendly_name);
517
0
    hook->u.configured.command =
518
0
      entry->command ? xstrdup(entry->command) : NULL;
519
0
    hook->u.configured.scope = entry->scope;
520
0
    hook->u.configured.disabled = entry->disabled;
521
0
    hook->event_disabled = event_is_disabled;
522
0
    hook->parallel = entry->parallel;
523
524
0
    string_list_append(list, friendly_name)->util = hook;
525
0
  }
526
527
  /*
528
   * Cleanup temporary cache for out-of-repo calls since they can't be
529
   * stored persistently. Next out-of-repo calls will have to re-parse.
530
   */
531
0
  if (!r || !r->gitdir) {
532
0
    hook_cache_clear(cache);
533
0
    free(cache);
534
0
    if (r)
535
0
      string_list_clear(&r->disabled_events, 0);
536
0
  }
537
0
}
538
539
struct string_list *list_hooks(struct repository *r, const char *hookname,
540
             struct run_hooks_opt *options)
541
0
{
542
0
  struct string_list *hook_head;
543
544
0
  if (!hookname)
545
0
    BUG("null hookname was provided to hook_list()!");
546
547
0
  CALLOC_ARRAY(hook_head, 1);
548
0
  string_list_init_dup(hook_head);
549
550
  /* Add hooks from the config, e.g. hook.myhook.event = pre-commit */
551
0
  list_hooks_add_configured(r, hookname, hook_head, options);
552
553
  /* Add the default "traditional" hooks from hookdir. */
554
0
  list_hooks_add_default(r, hookname, hook_head, options);
555
556
0
  return hook_head;
557
0
}
558
559
int hook_exists(struct repository *r, const char *name)
560
0
{
561
0
  struct string_list *hooks = list_hooks(r, name, NULL);
562
0
  int exists = 0;
563
564
0
  for (size_t i = 0; i < hooks->nr; i++) {
565
0
    struct hook *h = hooks->items[i].util;
566
0
    if (!h->event_disabled &&
567
0
        (h->kind == HOOK_TRADITIONAL ||
568
0
         !h->u.configured.disabled)) {
569
0
      exists = 1;
570
0
      break;
571
0
    }
572
0
  }
573
0
  string_list_clear_func(hooks, hook_free);
574
0
  free(hooks);
575
0
  return exists;
576
0
}
577
578
static int pick_next_hook(struct child_process *cp,
579
        struct strbuf *out UNUSED,
580
        void *pp_cb,
581
        void **pp_task_cb)
582
0
{
583
0
  struct hook_cb_data *hook_cb = pp_cb;
584
0
  struct string_list *hook_list = hook_cb->hook_command_list;
585
0
  struct hook *h;
586
587
0
  do {
588
0
    if (hook_cb->hook_to_run_index >= hook_list->nr)
589
0
      return 0;
590
0
    h = hook_list->items[hook_cb->hook_to_run_index++].util;
591
0
  } while (h->event_disabled ||
592
0
     (h->kind == HOOK_CONFIGURED && h->u.configured.disabled));
593
594
0
  cp->no_stdin = 1;
595
0
  strvec_pushv(&cp->env, hook_cb->options->env.v);
596
597
0
  if (hook_cb->options->path_to_stdin && hook_cb->options->feed_pipe)
598
0
    BUG("options path_to_stdin and feed_pipe are mutually exclusive");
599
600
  /* reopen the file for stdin; run_command closes it. */
601
0
  if (hook_cb->options->path_to_stdin) {
602
0
    cp->no_stdin = 0;
603
0
    cp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);
604
0
  }
605
606
0
  if (hook_cb->options->feed_pipe) {
607
0
    cp->no_stdin = 0;
608
    /* start_command() will allocate a pipe / stdin fd for us */
609
0
    cp->in = -1;
610
0
  }
611
612
0
  cp->stdout_to_stderr = hook_cb->options->stdout_to_stderr;
613
0
  cp->trace2_hook_name = hook_cb->hook_name;
614
0
  cp->dir = hook_cb->options->dir;
615
616
  /* Add hook exec paths or commands */
617
0
  if (h->kind == HOOK_TRADITIONAL) {
618
0
    strvec_push(&cp->args, h->u.traditional.path);
619
0
  } else if (h->kind == HOOK_CONFIGURED) {
620
    /* to enable oneliners, let config-specified hooks run in shell. */
621
0
    cp->use_shell = true;
622
0
    if (!h->u.configured.command)
623
0
      BUG("non-disabled HOOK_CONFIGURED hook has no command");
624
0
    strvec_push(&cp->args, h->u.configured.command);
625
0
  } else {
626
0
    BUG("unknown hook kind");
627
0
  }
628
629
0
  if (!cp->args.nr)
630
0
    BUG("hook must have at least one command or exec path");
631
632
0
  strvec_pushv(&cp->args, hook_cb->options->args.v);
633
634
  /*
635
   * Provide per-hook internal state via task_cb for easy access, so
636
   * hook callbacks don't have to go through hook_cb->options.
637
   */
638
0
  *pp_task_cb = h->feed_pipe_cb_data;
639
640
0
  return 1;
641
0
}
642
643
static int notify_start_failure(struct strbuf *out UNUSED,
644
        void *pp_cb,
645
        void *pp_task_cp UNUSED)
646
0
{
647
0
  struct hook_cb_data *hook_cb = pp_cb;
648
649
0
  hook_cb->rc |= 1;
650
651
0
  return 1;
652
0
}
653
654
static int notify_hook_finished(int result,
655
        struct strbuf *out UNUSED,
656
        void *pp_cb,
657
        void *pp_task_cb UNUSED)
658
0
{
659
0
  struct hook_cb_data *hook_cb = pp_cb;
660
0
  struct run_hooks_opt *opt = hook_cb->options;
661
662
0
  hook_cb->rc |= result;
663
664
0
  if (opt->invoked_hook)
665
0
    *opt->invoked_hook = 1;
666
667
0
  return 0;
668
0
}
669
670
static void run_hooks_opt_clear(struct run_hooks_opt *options)
671
0
{
672
0
  strvec_clear(&options->env);
673
0
  strvec_clear(&options->args);
674
0
}
675
676
/*
677
 * When running in parallel, stdout must be merged into stderr so
678
 * run-command can buffer and de-interleave outputs correctly. This
679
 * applies even to hooks like pre-push that normally keep stdout and
680
 * stderr separate: the user has opted into parallelism, so the output
681
 * stream behavior changes accordingly.
682
 */
683
static void merge_output_if_parallel(struct run_hooks_opt *options)
684
0
{
685
0
  if (options->jobs > 1)
686
0
    options->stdout_to_stderr = 1;
687
0
}
688
689
static void warn_non_parallel_hooks_override(unsigned int jobs,
690
               struct string_list *hook_list)
691
0
{
692
  /* Don't warn for hooks running sequentially. */
693
0
  if (jobs == 1)
694
0
    return;
695
696
0
  for (size_t i = 0; i < hook_list->nr; i++) {
697
0
    struct hook *h = hook_list->items[i].util;
698
0
    if (h->kind == HOOK_CONFIGURED && !h->parallel)
699
0
      warning(_("hook '%s' is not marked as parallel=true, "
700
0
          "running in parallel anyway due to -j%u"),
701
0
        h->u.configured.friendly_name, jobs);
702
0
  }
703
0
}
704
705
/* Resolve a hook.jobs config key, handling -1 as online_cpus(). */
706
static void resolve_hook_config_jobs(struct repository *r,
707
             const char *key,
708
             unsigned int *jobs)
709
0
{
710
0
  int v;
711
712
0
  if (repo_config_get_int(r, key, &v))
713
0
    return;
714
715
0
  if (v == -1)
716
0
    *jobs = online_cpus();
717
0
  else if (v > 0)
718
0
    *jobs = v;
719
0
  else
720
0
    warning(_("%s must be a positive integer or -1,"
721
0
        " ignoring: %d"), key, v);
722
0
}
723
724
/* Determine how many jobs to use for hook execution. */
725
static unsigned int get_hook_jobs(struct repository *r,
726
          struct run_hooks_opt *options,
727
          const char *hook_name,
728
          struct string_list *hook_list)
729
0
{
730
  /*
731
   * An explicit job count overrides everything else: this covers both
732
   * FORCE_SERIAL callers (for hooks that must never run in parallel)
733
   * and the -j flag from the CLI. The CLI override is intentional: users
734
   * may want to serialize hooks declared parallel or to parallelize more
735
   * aggressively than the default.
736
   */
737
0
  if (options->jobs)
738
0
    goto cleanup;
739
740
  /*
741
   * Use hook.jobs from the already-parsed config cache (in-repo), or
742
   * fallback to a direct config lookup (out-of-repo).
743
   * Default to 1 (serial execution) on failure.
744
   */
745
0
  options->jobs = 1;
746
0
  if (r) {
747
0
    if (r->gitdir && r->hook_config_cache) {
748
0
      void *event_jobs;
749
750
0
      if (r->hook_jobs)
751
0
        options->jobs = r->hook_jobs;
752
753
0
      event_jobs = strmap_get(&r->event_jobs, hook_name);
754
0
      if (event_jobs)
755
0
        options->jobs = (unsigned int)(uintptr_t)event_jobs;
756
0
    } else {
757
0
      char *key;
758
759
0
      resolve_hook_config_jobs(r, "hook.jobs", &options->jobs);
760
761
0
      key = xstrfmt("hook.%s.jobs", hook_name);
762
0
      resolve_hook_config_jobs(r, key, &options->jobs);
763
0
      free(key);
764
0
    }
765
0
  }
766
767
  /*
768
   * Cap to serial any configured hook not marked as parallel = true.
769
   * This enforces the parallel = false default, even for "traditional"
770
   * hooks from the hookdir which cannot be marked parallel = true.
771
   * The same restriction applies whether jobs came from hook.jobs or
772
   * hook.<event>.jobs.
773
   */
774
0
  for (size_t i = 0; i < hook_list->nr; i++) {
775
0
    struct hook *h = hook_list->items[i].util;
776
0
    if (h->kind == HOOK_CONFIGURED && !h->parallel) {
777
0
      options->jobs = 1;
778
0
      break;
779
0
    }
780
0
  }
781
782
0
cleanup:
783
0
  merge_output_if_parallel(options);
784
0
  warn_non_parallel_hooks_override(options->jobs, hook_list);
785
0
  return options->jobs;
786
0
}
787
788
int run_hooks_opt(struct repository *r, const char *hook_name,
789
      struct run_hooks_opt *options)
790
0
{
791
0
  struct string_list *hook_list = list_hooks(r, hook_name, options);
792
0
  struct hook_cb_data cb_data = {
793
0
    .rc = 0,
794
0
    .hook_name = hook_name,
795
0
    .hook_command_list = hook_list,
796
0
    .options = options,
797
0
  };
798
0
  int ret = 0;
799
0
  unsigned int jobs = get_hook_jobs(r, options, hook_name, hook_list);
800
0
  const struct run_process_parallel_opts opts = {
801
0
    .tr2_category = "hook",
802
0
    .tr2_label = hook_name,
803
804
0
    .processes = jobs,
805
0
    .ungroup = jobs == 1,
806
807
0
    .get_next_task = pick_next_hook,
808
0
    .start_failure = notify_start_failure,
809
0
    .feed_pipe = options->feed_pipe,
810
0
    .task_finished = notify_hook_finished,
811
812
0
    .data = &cb_data,
813
0
  };
814
815
0
  if (!options)
816
0
    BUG("a struct run_hooks_opt must be provided to run_hooks");
817
818
0
  if (options->path_to_stdin && options->feed_pipe)
819
0
    BUG("options path_to_stdin and feed_pipe are mutually exclusive");
820
821
  /*
822
   * Ensure cb_data copy and free functions are either provided together,
823
   * or neither one is provided.
824
   */
825
0
  if (!options->feed_pipe_cb_data_alloc != !options->feed_pipe_cb_data_free)
826
0
    BUG("feed_pipe_cb_data_alloc and feed_pipe_cb_data_free must be set together");
827
828
0
  if (options->invoked_hook)
829
0
    *options->invoked_hook = 0;
830
831
0
  if (!cb_data.hook_command_list->nr) {
832
0
    if (options->error_if_missing)
833
0
      ret = error("cannot find a hook named %s", hook_name);
834
0
    goto cleanup;
835
0
  }
836
837
0
  run_processes_parallel(&opts);
838
0
  ret = cb_data.rc;
839
0
cleanup:
840
0
  string_list_clear_func(cb_data.hook_command_list, hook_free);
841
0
  free(cb_data.hook_command_list);
842
0
  run_hooks_opt_clear(options);
843
0
  return ret;
844
0
}
845
846
int run_hooks(struct repository *r, const char *hook_name)
847
0
{
848
0
  struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
849
850
0
  return run_hooks_opt(r, hook_name, &opt);
851
0
}
852
853
int run_hooks_l(struct repository *r, const char *hook_name, ...)
854
0
{
855
0
  struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
856
0
  va_list ap;
857
0
  const char *arg;
858
859
0
  va_start(ap, hook_name);
860
0
  while ((arg = va_arg(ap, const char *)))
861
0
    strvec_push(&opt.args, arg);
862
0
  va_end(ap);
863
864
0
  return run_hooks_opt(r, hook_name, &opt);
865
0
}