trailer: append trailers without fork/exec

Introduce amend_strbuf_with_trailers() to apply trailer additions to a message buffer via process_trailers(), avoiding the need to run git interpret-trailers as a child process. Update amend_file_with_trailers() to use the in-process helper and rewrite the target file via tempfile+rename, preserving the previous in-place semantics. As the trailers are no longer added in a separate process and trailer_config_init() die()s on missing config values it is called early on in cmd_commit() and cmd_tag() so that they die() early before writing the message file. The trailer arguments are now also sanity checked. Keep existing callers unchanged by continuing to accept argv-style --trailer=<trailer> entries and stripping the prefix before feeding the in-process implementation. Signed-off-by: Li Chen <me@linux.beauty> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Li Chen committed Mar 6, 2026 at 14:53 UTC 6b2243fdd45f0596fc640823faaa6a1aec05a420
4 files changed +147 -12
builtin/commit.c
+3
@@ -1820,6 +1820,9 @@ int cmd_commit(int argc,
1820 argc = parse_and_validate_options(argc, argv, builtin_commit_options,
1821 builtin_commit_usage,
1822 prefix, current_head, &s);
1823 + if (trailer_args.nr)
1824 + trailer_config_init();
1825 +
1826 if (verbose == -1)
1827 verbose = (config_commit_verbose < 0) ? 0 : config_commit_verbose;
1828
builtin/tag.c
+3
@@ -568,6 +568,9 @@ int cmd_tag(int argc,
568 if (cmdmode == 'l')
569 setup_auto_pager("tag", 1);
570
571 + if (trailer_args.nr)
572 + trailer_config_init();
573 +
574 if (opt.sign == -1)
575 opt.sign = cmdmode ? 0 : config_sign_tag > 0;
576
trailer.c
+124 -9
@@ -7,6 +7,7 @@
7 #include "string-list.h"
8 #include "run-command.h"
9 #include "commit.h"
10 +#include "strvec.h"
11 #include "trailer.h"
12 #include "list.h"
13 #include "tempfile.h"
@@ -774,6 +775,35 @@ void parse_trailers_from_command_line_args(struct list_head *arg_head,
775 free(cl_separators);
776 }
777
778 +int validate_trailer_args(const struct strvec *cli_args)
779 +{
780 + char *cl_separators;
781 + int ret = 0;
782 +
783 + trailer_config_init();
784 +
785 + cl_separators = xstrfmt("=%s", separators);
786 +
787 + for (size_t i = 0; i < cli_args->nr; i++) {
788 + const char *txt = cli_args->v[i];
789 + ssize_t separator_pos;
790 +
791 + if (!*txt) {
792 + ret = error(_("empty --trailer argument"));
793 + goto out;
794 + }
795 + separator_pos = find_separator(txt, cl_separators);
796 + if (separator_pos == 0) {
797 + ret = error(_("invalid trailer '%s': missing key before separator"),
798 + txt);
799 + goto out;
800 + }
801 + }
802 +out:
803 + free(cl_separators);
804 + return ret;
805 +}
806 +
807 static const char *next_line(const char *str)
808 {
809 const char *nl = strchrnul(str, '\n');
@@ -1258,16 +1288,101 @@ struct tempfile *trailer_create_in_place_tempfile(const char *file)
1288 return tempfile;
1289 }
1290
1261 -int amend_file_with_trailers(const char *path, const struct strvec *trailer_args)
1291 +int amend_strbuf_with_trailers(struct strbuf *buf,
1292 + const struct strvec *trailer_args)
1293 {
1263 - struct child_process run_trailer = CHILD_PROCESS_INIT;
1264 -
1265 - run_trailer.git_cmd = 1;
1266 - strvec_pushl(&run_trailer.args, "interpret-trailers",
1267 - "--in-place", "--no-divider",
1268 - path, NULL);
1269 - strvec_pushv(&run_trailer.args, trailer_args->v);
1270 - return run_command(&run_trailer);
1294 + struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
1295 + LIST_HEAD(new_trailer_head);
1296 + struct strbuf out = STRBUF_INIT;
1297 + size_t i;
1298 + int ret = 0;
1299 +
1300 + opts.no_divider = 1;
1301 +
1302 + for (i = 0; i < trailer_args->nr; i++) {
1303 + const char *text = trailer_args->v[i];
1304 + struct new_trailer_item *item;
1305 +
1306 + if (!*text) {
1307 + ret = error(_("empty --trailer argument"));
1308 + goto out;
1309 + }
1310 + item = xcalloc(1, sizeof(*item));
1311 + item->text = xstrdup(text);
1312 + list_add_tail(&item->list, &new_trailer_head);
1313 + }
1314 +
1315 + process_trailers(&opts, &new_trailer_head, buf, &out);
1316 +
1317 + strbuf_swap(buf, &out);
1318 +out:
1319 + strbuf_release(&out);
1320 + free_trailers(&new_trailer_head);
1321 +
1322 + return ret;
1323 +}
1324 +
1325 +static int write_file_in_place(const char *path, const struct strbuf *buf)
1326 +{
1327 + struct tempfile *tempfile = trailer_create_in_place_tempfile(path);
1328 + if (!tempfile)
1329 + return -1;
1330 +
1331 + if (write_in_full(tempfile->fd, buf->buf, buf->len) < 0)
1332 + return error_errno(_("could not write to temporary file"));
1333 +
1334 + if (rename_tempfile(&tempfile, path))
1335 + return error_errno(_("could not rename temporary file to %s"), path);
1336 +
1337 + return 0;
1338 +}
1339 +
1340 +int amend_file_with_trailers(const char *path,
1341 + const struct strvec *trailer_args)
1342 +{
1343 + struct strbuf buf = STRBUF_INIT;
1344 + struct strvec stripped_trailer_args = STRVEC_INIT;
1345 + int ret = 0;
1346 + size_t i;
1347 +
1348 + if (!trailer_args)
1349 + BUG("amend_file_with_trailers called with NULL trailer_args");
1350 + if (!trailer_args->nr)
1351 + return 0;
1352 +
1353 + for (i = 0; i < trailer_args->nr; i++) {
1354 + const char *txt = trailer_args->v[i];
1355 +
1356 + /*
1357 + * Historically amend_file_with_trailers() passed its arguments
1358 + * to "git interpret-trailers", which expected argv entries in
1359 + * "--trailer=<trailer>" form. Continue to accept those for
1360 + * existing callers, but pass only the value portion to the
1361 + * in-process implementation.
1362 + */
1363 + skip_prefix(txt, "--trailer=", &txt);
1364 + if (!*txt) {
1365 + ret = error(_("empty --trailer argument"));
1366 + goto out;
1367 + }
1368 + strvec_push(&stripped_trailer_args, txt);
1369 + }
1370 +
1371 + if (validate_trailer_args(&stripped_trailer_args)) {
1372 + ret = -1;
1373 + goto out;
1374 + }
1375 + if (strbuf_read_file(&buf, path, 0) < 0)
1376 + ret = error_errno(_("could not read '%s'"), path);
1377 + else
1378 + amend_strbuf_with_trailers(&buf, &stripped_trailer_args);
1379 +
1380 + if (!ret)
1381 + ret = write_file_in_place(path, &buf);
1382 +out:
1383 + strvec_clear(&stripped_trailer_args);
1384 + strbuf_release(&buf);
1385 + return ret;
1386 }
1387
1388 void process_trailers(const struct process_trailer_options *opts,
trailer.h
+17 -3
@@ -68,6 +68,8 @@ void parse_trailers_from_config(struct list_head *config_head);
68 void parse_trailers_from_command_line_args(struct list_head *arg_head,
69 struct list_head *new_trailer_head);
70
71 +int validate_trailer_args(const struct strvec *cli_args);
72 +
73 void process_trailers_lists(struct list_head *head,
74 struct list_head *arg_head);
75
@@ -196,9 +198,21 @@ int trailer_iterator_advance(struct trailer_iterator *iter);
198 void trailer_iterator_release(struct trailer_iterator *iter);
199
200 /*
199 - * Augment a file to add trailers to it by running git-interpret-trailers.
200 - * This calls run_command() and its return value is the same (i.e. 0 for
201 - * success, various non-zero for other errors). See run-command.h.
201 + * Append trailers specified in trailer_args to buf in-place.
202 + *
203 + * Each element of trailer_args should be in the same format as the value
204 + * accepted by --trailer=<trailer> (i.e., without the --trailer= prefix).
205 + */
206 +int amend_strbuf_with_trailers(struct strbuf *buf,
207 + const struct strvec *trailer_args);
208 +
209 +/*
210 + * Augment a file by appending trailers specified in trailer_args.
211 + *
212 + * Each element of trailer_args should be an argv-style --trailer=<trailer>
213 + * option (i.e., including the --trailer= prefix).
214 + *
215 + * Returns 0 on success or a non-zero error code on failure.
216 */
217 int amend_file_with_trailers(const char *path, const struct strvec *trailer_args);
218