Coverage Report

Created: 2026-03-31 06:24

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 "gettext.h"
5
#include "hook.h"
6
#include "path.h"
7
#include "parse.h"
8
#include "run-command.h"
9
#include "config.h"
10
#include "strbuf.h"
11
#include "strmap.h"
12
#include "environment.h"
13
#include "setup.h"
14
15
const char *find_hook(struct repository *r, const char *name)
16
0
{
17
0
  static struct strbuf path = STRBUF_INIT;
18
19
0
  int found_hook;
20
21
0
  if (!r || !r->gitdir)
22
0
    return NULL;
23
24
0
  repo_git_path_replace(r, &path, "hooks/%s", name);
25
0
  found_hook = access(path.buf, X_OK) >= 0;
26
#ifdef STRIP_EXTENSION
27
  if (!found_hook) {
28
    int err = errno;
29
30
    strbuf_addstr(&path, STRIP_EXTENSION);
31
    found_hook = access(path.buf, X_OK) >= 0;
32
    if (!found_hook)
33
      errno = err;
34
  }
35
#endif
36
37
0
  if (!found_hook) {
38
0
    if (errno == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) {
39
0
      static struct string_list advise_given = STRING_LIST_INIT_DUP;
40
41
0
      if (!string_list_lookup(&advise_given, name)) {
42
0
        string_list_insert(&advise_given, name);
43
0
        advise(_("The '%s' hook was ignored because "
44
0
           "it's not set as executable.\n"
45
0
           "You can disable this warning with "
46
0
           "`git config set advice.ignoredHook false`."),
47
0
               path.buf);
48
0
      }
49
0
    }
50
0
    return NULL;
51
0
  }
52
0
  return path.buf;
53
0
}
54
55
static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)
56
0
{
57
0
  if (!h)
58
0
    return;
59
60
0
  if (h->kind == HOOK_TRADITIONAL)
61
0
    free((void *)h->u.traditional.path);
62
0
  else if (h->kind == HOOK_CONFIGURED) {
63
0
    free((void *)h->u.configured.friendly_name);
64
0
    free((void *)h->u.configured.command);
65
0
  }
66
67
0
  if (cb_data_free)
68
0
    cb_data_free(h->feed_pipe_cb_data);
69
70
0
  free(h);
71
0
}
72
73
void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free)
74
0
{
75
0
  struct string_list_item *item;
76
77
0
  for_each_string_list_item(item, hooks)
78
0
    hook_clear(item->util, cb_data_free);
79
80
0
  string_list_clear(hooks, 0);
81
0
}
82
83
/* Helper to detect and add default "traditional" hooks from the hookdir. */
84
static void list_hooks_add_default(struct repository *r, const char *hookname,
85
           struct string_list *hook_list,
86
           struct run_hooks_opt *options)
87
0
{
88
0
  const char *hook_path = find_hook(r, hookname);
89
0
  struct hook *h;
90
91
0
  if (!hook_path)
92
0
    return;
93
94
0
  h = xcalloc(1, sizeof(struct hook));
95
96
  /*
97
   * If the hook is to run in a specific dir, a relative path can
98
   * become invalid in that dir, so convert to an absolute path.
99
   */
100
0
  if (options && options->dir)
101
0
    hook_path = absolute_path(hook_path);
102
103
  /* Setup per-hook internal state cb data */
104
0
  if (options && options->feed_pipe_cb_data_alloc)
105
0
    h->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);
106
107
0
  h->kind = HOOK_TRADITIONAL;
108
0
  h->u.traditional.path = xstrdup(hook_path);
109
110
0
  string_list_append(hook_list, hook_path)->util = h;
111
0
}
112
113
static void unsorted_string_list_remove(struct string_list *list,
114
          const char *str)
115
0
{
116
0
  struct string_list_item *item = unsorted_string_list_lookup(list, str);
117
0
  if (item)
118
0
    unsorted_string_list_delete_item(list, item - list->items, 0);
119
0
}
120
121
/*
122
 * Callback struct to collect all hook.* keys in a single config pass.
123
 * commands: friendly-name to command map.
124
 * event_hooks: event-name to list of friendly-names map.
125
 * disabled_hooks: set of friendly-names with hook.name.enabled = false.
126
 */
127
struct hook_all_config_cb {
128
  struct strmap commands;
129
  struct strmap event_hooks;
130
  struct string_list disabled_hooks;
131
};
132
133
/* repo_config() callback that collects all hook.* configuration in one pass. */
134
static int hook_config_lookup_all(const char *key, const char *value,
135
          const struct config_context *ctx UNUSED,
136
          void *cb_data)
137
0
{
138
0
  struct hook_all_config_cb *data = cb_data;
139
0
  const char *name, *subkey;
140
0
  char *hook_name;
141
0
  size_t name_len = 0;
142
143
0
  if (parse_config_key(key, "hook", &name, &name_len, &subkey))
144
0
    return 0;
145
146
0
  if (!value)
147
0
    return config_error_nonbool(key);
148
149
  /* Extract name, ensuring it is null-terminated. */
150
0
  hook_name = xmemdupz(name, name_len);
151
152
0
  if (!strcmp(subkey, "event")) {
153
0
    if (!*value) {
154
      /* Empty values reset previous events for this hook. */
155
0
      struct hashmap_iter iter;
156
0
      struct strmap_entry *e;
157
158
0
      strmap_for_each_entry(&data->event_hooks, &iter, e)
159
0
        unsorted_string_list_remove(e->value, hook_name);
160
0
    } else {
161
0
      struct string_list *hooks =
162
0
        strmap_get(&data->event_hooks, value);
163
164
0
      if (!hooks) {
165
0
        hooks = xcalloc(1, sizeof(*hooks));
166
0
        string_list_init_dup(hooks);
167
0
        strmap_put(&data->event_hooks, value, hooks);
168
0
      }
169
170
      /* Re-insert if necessary to preserve last-seen order. */
171
0
      unsorted_string_list_remove(hooks, hook_name);
172
0
      string_list_append(hooks, hook_name);
173
0
    }
174
0
  } else if (!strcmp(subkey, "command")) {
175
    /* Store command overwriting the old value */
176
0
    char *old = strmap_put(&data->commands, hook_name,
177
0
               xstrdup(value));
178
0
    free(old);
179
0
  } else if (!strcmp(subkey, "enabled")) {
180
0
    switch (git_parse_maybe_bool(value)) {
181
0
    case 0: /* disabled */
182
0
      if (!unsorted_string_list_lookup(&data->disabled_hooks,
183
0
               hook_name))
184
0
        string_list_append(&data->disabled_hooks,
185
0
               hook_name);
186
0
      break;
187
0
    case 1: /* enabled: undo a prior disabled entry */
188
0
      unsorted_string_list_remove(&data->disabled_hooks,
189
0
                hook_name);
190
0
      break;
191
0
    default:
192
0
      break; /* ignore unrecognised values */
193
0
    }
194
0
  }
195
196
0
  free(hook_name);
197
0
  return 0;
198
0
}
199
200
/*
201
 * The hook config cache maps each hook event name to a string_list where
202
 * every item's string is the hook's friendly-name and its util pointer is
203
 * the corresponding command string. Both strings are owned by the map.
204
 *
205
 * Disabled hooks and hooks missing a command are already filtered out at
206
 * parse time, so callers can iterate the list directly.
207
 */
208
void hook_cache_clear(struct strmap *cache)
209
0
{
210
0
  struct hashmap_iter iter;
211
0
  struct strmap_entry *e;
212
213
0
  strmap_for_each_entry(cache, &iter, e) {
214
0
    struct string_list *hooks = e->value;
215
0
    string_list_clear(hooks, 1); /* free util (command) pointers */
216
0
    free(hooks);
217
0
  }
218
0
  strmap_clear(cache, 0);
219
0
}
220
221
/* Populate `cache` with the complete hook configuration */
222
static void build_hook_config_map(struct repository *r, struct strmap *cache)
223
0
{
224
0
  struct hook_all_config_cb cb_data;
225
0
  struct hashmap_iter iter;
226
0
  struct strmap_entry *e;
227
228
0
  strmap_init(&cb_data.commands);
229
0
  strmap_init(&cb_data.event_hooks);
230
0
  string_list_init_dup(&cb_data.disabled_hooks);
231
232
  /* Parse all configs in one run. */
233
0
  repo_config(r, hook_config_lookup_all, &cb_data);
234
235
  /* Construct the cache from parsed configs. */
236
0
  strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
237
0
    struct string_list *hook_names = e->value;
238
0
    struct string_list *hooks = xcalloc(1, sizeof(*hooks));
239
240
0
    string_list_init_dup(hooks);
241
242
0
    for (size_t i = 0; i < hook_names->nr; i++) {
243
0
      const char *hname = hook_names->items[i].string;
244
0
      char *command;
245
246
      /* filter out disabled hooks */
247
0
      if (unsorted_string_list_lookup(&cb_data.disabled_hooks,
248
0
              hname))
249
0
        continue;
250
251
0
      command = strmap_get(&cb_data.commands, hname);
252
0
      if (!command)
253
0
        die(_("'hook.%s.command' must be configured or "
254
0
              "'hook.%s.event' must be removed;"
255
0
              " aborting."), hname, hname);
256
257
      /* util stores the command; owned by the cache. */
258
0
      string_list_append(hooks, hname)->util =
259
0
        xstrdup(command);
260
0
    }
261
262
0
    strmap_put(cache, e->key, hooks);
263
0
  }
264
265
0
  strmap_clear(&cb_data.commands, 1);
266
0
  string_list_clear(&cb_data.disabled_hooks, 0);
267
0
  strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
268
0
    string_list_clear(e->value, 0);
269
0
    free(e->value);
270
0
  }
271
0
  strmap_clear(&cb_data.event_hooks, 0);
272
0
}
273
274
/*
275
 * Return the hook config map for `r`, populating it first if needed.
276
 *
277
 * Out-of-repo calls (r->gitdir == NULL) allocate and return a temporary
278
 * cache map; the caller is responsible for freeing it with
279
 * hook_cache_clear() + free().
280
 */
281
static struct strmap *get_hook_config_cache(struct repository *r)
282
0
{
283
0
  struct strmap *cache = NULL;
284
285
0
  if (r && r->gitdir) {
286
    /*
287
     * For in-repo calls, the map is stored in r->hook_config_cache,
288
     * so repeated invocations don't parse the configs, so allocate
289
     * it just once on the first call.
290
     */
291
0
    if (!r->hook_config_cache) {
292
0
      r->hook_config_cache = xcalloc(1, sizeof(*cache));
293
0
      strmap_init(r->hook_config_cache);
294
0
      build_hook_config_map(r, r->hook_config_cache);
295
0
    }
296
0
    cache = r->hook_config_cache;
297
0
  } else {
298
    /*
299
     * Out-of-repo calls (no gitdir) allocate and return a temporary
300
     * map cache which gets free'd immediately by the caller.
301
     */
302
0
    cache = xcalloc(1, sizeof(*cache));
303
0
    strmap_init(cache);
304
0
    build_hook_config_map(r, cache);
305
0
  }
306
307
0
  return cache;
308
0
}
309
310
static void list_hooks_add_configured(struct repository *r,
311
              const char *hookname,
312
              struct string_list *list,
313
              struct run_hooks_opt *options)
314
0
{
315
0
  struct strmap *cache = get_hook_config_cache(r);
316
0
  struct string_list *configured_hooks = strmap_get(cache, hookname);
317
318
  /* Iterate through configured hooks and initialize internal states */
319
0
  for (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {
320
0
    const char *friendly_name = configured_hooks->items[i].string;
321
0
    const char *command = configured_hooks->items[i].util;
322
0
    struct hook *hook = xcalloc(1, sizeof(struct hook));
323
324
0
    if (options && options->feed_pipe_cb_data_alloc)
325
0
      hook->feed_pipe_cb_data =
326
0
        options->feed_pipe_cb_data_alloc(
327
0
          options->feed_pipe_ctx);
328
329
0
    hook->kind = HOOK_CONFIGURED;
330
0
    hook->u.configured.friendly_name = xstrdup(friendly_name);
331
0
    hook->u.configured.command = xstrdup(command);
332
333
0
    string_list_append(list, friendly_name)->util = hook;
334
0
  }
335
336
  /*
337
   * Cleanup temporary cache for out-of-repo calls since they can't be
338
   * stored persistently. Next out-of-repo calls will have to re-parse.
339
   */
340
0
  if (!r || !r->gitdir) {
341
0
    hook_cache_clear(cache);
342
0
    free(cache);
343
0
  }
344
0
}
345
346
struct string_list *list_hooks(struct repository *r, const char *hookname,
347
             struct run_hooks_opt *options)
348
0
{
349
0
  struct string_list *hook_head;
350
351
0
  if (!hookname)
352
0
    BUG("null hookname was provided to hook_list()!");
353
354
0
  hook_head = xmalloc(sizeof(struct string_list));
355
0
  string_list_init_dup(hook_head);
356
357
  /* Add hooks from the config, e.g. hook.myhook.event = pre-commit */
358
0
  list_hooks_add_configured(r, hookname, hook_head, options);
359
360
  /* Add the default "traditional" hooks from hookdir. */
361
0
  list_hooks_add_default(r, hookname, hook_head, options);
362
363
0
  return hook_head;
364
0
}
365
366
int hook_exists(struct repository *r, const char *name)
367
0
{
368
0
  struct string_list *hooks = list_hooks(r, name, NULL);
369
0
  int exists = hooks->nr > 0;
370
0
  hook_list_clear(hooks, NULL);
371
0
  free(hooks);
372
0
  return exists;
373
0
}
374
375
static int pick_next_hook(struct child_process *cp,
376
        struct strbuf *out UNUSED,
377
        void *pp_cb,
378
        void **pp_task_cb)
379
0
{
380
0
  struct hook_cb_data *hook_cb = pp_cb;
381
0
  struct string_list *hook_list = hook_cb->hook_command_list;
382
0
  struct hook *h;
383
384
0
  if (hook_cb->hook_to_run_index >= hook_list->nr)
385
0
    return 0;
386
387
0
  h = hook_list->items[hook_cb->hook_to_run_index++].util;
388
389
0
  cp->no_stdin = 1;
390
0
  strvec_pushv(&cp->env, hook_cb->options->env.v);
391
392
0
  if (hook_cb->options->path_to_stdin && hook_cb->options->feed_pipe)
393
0
    BUG("options path_to_stdin and feed_pipe are mutually exclusive");
394
395
  /* reopen the file for stdin; run_command closes it. */
396
0
  if (hook_cb->options->path_to_stdin) {
397
0
    cp->no_stdin = 0;
398
0
    cp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);
399
0
  }
400
401
0
  if (hook_cb->options->feed_pipe) {
402
0
    cp->no_stdin = 0;
403
    /* start_command() will allocate a pipe / stdin fd for us */
404
0
    cp->in = -1;
405
0
  }
406
407
0
  cp->stdout_to_stderr = hook_cb->options->stdout_to_stderr;
408
0
  cp->trace2_hook_name = hook_cb->hook_name;
409
0
  cp->dir = hook_cb->options->dir;
410
411
  /* Add hook exec paths or commands */
412
0
  if (h->kind == HOOK_TRADITIONAL) {
413
0
    strvec_push(&cp->args, h->u.traditional.path);
414
0
  } else if (h->kind == HOOK_CONFIGURED) {
415
    /* to enable oneliners, let config-specified hooks run in shell. */
416
0
    cp->use_shell = true;
417
0
    strvec_push(&cp->args, h->u.configured.command);
418
0
  }
419
420
0
  if (!cp->args.nr)
421
0
    BUG("hook must have at least one command or exec path");
422
423
0
  strvec_pushv(&cp->args, hook_cb->options->args.v);
424
425
  /*
426
   * Provide per-hook internal state via task_cb for easy access, so
427
   * hook callbacks don't have to go through hook_cb->options.
428
   */
429
0
  *pp_task_cb = h->feed_pipe_cb_data;
430
431
0
  return 1;
432
0
}
433
434
static int notify_start_failure(struct strbuf *out UNUSED,
435
        void *pp_cb,
436
        void *pp_task_cp UNUSED)
437
0
{
438
0
  struct hook_cb_data *hook_cb = pp_cb;
439
440
0
  hook_cb->rc |= 1;
441
442
0
  return 1;
443
0
}
444
445
static int notify_hook_finished(int result,
446
        struct strbuf *out UNUSED,
447
        void *pp_cb,
448
        void *pp_task_cb UNUSED)
449
0
{
450
0
  struct hook_cb_data *hook_cb = pp_cb;
451
0
  struct run_hooks_opt *opt = hook_cb->options;
452
453
0
  hook_cb->rc |= result;
454
455
0
  if (opt->invoked_hook)
456
0
    *opt->invoked_hook = 1;
457
458
0
  return 0;
459
0
}
460
461
static void run_hooks_opt_clear(struct run_hooks_opt *options)
462
0
{
463
0
  strvec_clear(&options->env);
464
0
  strvec_clear(&options->args);
465
0
}
466
467
int run_hooks_opt(struct repository *r, const char *hook_name,
468
      struct run_hooks_opt *options)
469
0
{
470
0
  struct hook_cb_data cb_data = {
471
0
    .rc = 0,
472
0
    .hook_name = hook_name,
473
0
    .options = options,
474
0
  };
475
0
  int ret = 0;
476
0
  const struct run_process_parallel_opts opts = {
477
0
    .tr2_category = "hook",
478
0
    .tr2_label = hook_name,
479
480
0
    .processes = options->jobs,
481
0
    .ungroup = options->jobs == 1,
482
483
0
    .get_next_task = pick_next_hook,
484
0
    .start_failure = notify_start_failure,
485
0
    .feed_pipe = options->feed_pipe,
486
0
    .task_finished = notify_hook_finished,
487
488
0
    .data = &cb_data,
489
0
  };
490
491
0
  if (!options)
492
0
    BUG("a struct run_hooks_opt must be provided to run_hooks");
493
494
0
  if (options->path_to_stdin && options->feed_pipe)
495
0
    BUG("options path_to_stdin and feed_pipe are mutually exclusive");
496
497
0
  if (!options->jobs)
498
0
    BUG("run_hooks_opt must be called with options.jobs >= 1");
499
500
  /*
501
   * Ensure cb_data copy and free functions are either provided together,
502
   * or neither one is provided.
503
   */
504
0
  if ((options->feed_pipe_cb_data_alloc && !options->feed_pipe_cb_data_free) ||
505
0
      (!options->feed_pipe_cb_data_alloc && options->feed_pipe_cb_data_free))
506
0
    BUG("feed_pipe_cb_data_alloc and feed_pipe_cb_data_free must be set together");
507
508
0
  if (options->invoked_hook)
509
0
    *options->invoked_hook = 0;
510
511
0
  cb_data.hook_command_list = list_hooks(r, hook_name, options);
512
0
  if (!cb_data.hook_command_list->nr) {
513
0
    if (options->error_if_missing)
514
0
      ret = error("cannot find a hook named %s", hook_name);
515
0
    goto cleanup;
516
0
  }
517
518
0
  run_processes_parallel(&opts);
519
0
  ret = cb_data.rc;
520
0
cleanup:
521
0
  hook_list_clear(cb_data.hook_command_list, options->feed_pipe_cb_data_free);
522
0
  free(cb_data.hook_command_list);
523
0
  run_hooks_opt_clear(options);
524
0
  return ret;
525
0
}
526
527
int run_hooks(struct repository *r, const char *hook_name)
528
0
{
529
0
  struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
530
531
0
  return run_hooks_opt(r, hook_name, &opt);
532
0
}
533
534
int run_hooks_l(struct repository *r, const char *hook_name, ...)
535
0
{
536
0
  struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
537
0
  va_list ap;
538
0
  const char *arg;
539
540
0
  va_start(ap, hook_name);
541
0
  while ((arg = va_arg(ap, const char *)))
542
0
    strvec_push(&opt.args, arg);
543
0
  va_end(ap);
544
545
0
  return run_hooks_opt(r, hook_name, &opt);
546
0
}