aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2026-03-19 09:54:56 -0700
committerJunio C Hamano <gitster@pobox.com>2026-03-19 09:54:56 -0700
commitaccd0e107bb36cbcdb158f8bb19d5bae589294a9 (patch)
tree9c127dab3844830d92aaf884a0f1037d11d5a3c3
parenta7a079c2c4bc7b269229a6ea6c147b6b2d5b2684 (diff)
parente4f9d6b0ab2e1903765258991a6265599d0007ce (diff)
downloadgit-accd0e107bb36cbcdb158f8bb19d5bae589294a9.tar.xz
Merge branch 'lc/rebase-trailer'
"git rebase" learns "--trailer" command to drive the interpret-trailers machinery. * lc/rebase-trailer: rebase: support --trailer commit, tag: parse --trailer with OPT_STRVEC trailer: append trailers without fork/exec trailer: libify a couple of functions interpret-trailers: refactor create_in_place_tempfile() interpret-trailers: factor trailer rewriting
-rw-r--r--Documentation/git-rebase.adoc8
-rw-r--r--builtin/commit.c6
-rw-r--r--builtin/interpret-trailers.c87
-rw-r--r--builtin/rebase.c19
-rw-r--r--builtin/tag.c7
-rw-r--r--sequencer.c52
-rw-r--r--sequencer.h3
-rw-r--r--t/meson.build1
-rwxr-xr-xt/t3440-rebase-trailer.sh147
-rw-r--r--trailer.c180
-rw-r--r--trailer.h36
11 files changed, 459 insertions, 87 deletions
diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc
index e177808004..f6c22d1598 100644
--- a/Documentation/git-rebase.adoc
+++ b/Documentation/git-rebase.adoc
@@ -497,6 +497,13 @@ See also INCOMPATIBLE OPTIONS below.
+
See also INCOMPATIBLE OPTIONS below.
+--trailer=<trailer>::
+ Append the given trailer to every rebased commit message, processed
+ via linkgit:git-interpret-trailers[1]. This option implies
+ `--force-rebase`.
++
+See also INCOMPATIBLE OPTIONS below.
+
-i::
--interactive::
Make a list of the commits which are about to be rebased. Let the
@@ -653,6 +660,7 @@ are incompatible with the following options:
* --[no-]reapply-cherry-picks when used without --keep-base
* --update-refs
* --root when used without --onto
+ * --trailer
In addition, the following pairs of options are incompatible:
diff --git a/builtin/commit.c b/builtin/commit.c
index 7b23c1f883..02a04f68be 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -1720,7 +1720,8 @@ int cmd_commit(int argc,
OPT_STRING(0, "fixup", &fixup_message, N_("[(amend|reword):]commit"), N_("use autosquash formatted message to fixup or amend/reword specified commit")),
OPT_STRING(0, "squash", &squash_message, N_("commit"), N_("use autosquash formatted message to squash specified commit")),
OPT_BOOL(0, "reset-author", &renew_authorship, N_("the commit is authored by me now (used with -C/-c/--amend)")),
- OPT_PASSTHRU_ARGV(0, "trailer", &trailer_args, N_("trailer"), N_("add custom trailer(s)"), PARSE_OPT_NONEG),
+ OPT_STRVEC(0, "trailer", &trailer_args, N_("trailer"),
+ N_("add custom trailer(s)")),
OPT_BOOL('s', "signoff", &signoff, N_("add a Signed-off-by trailer")),
OPT_FILENAME('t', "template", &template_file, N_("use specified template file")),
OPT_BOOL('e', "edit", &edit_flag, N_("force edit of commit")),
@@ -1820,6 +1821,9 @@ int cmd_commit(int argc,
argc = parse_and_validate_options(argc, argv, builtin_commit_options,
builtin_commit_usage,
prefix, current_head, &s);
+ if (trailer_args.nr)
+ trailer_config_init();
+
if (verbose == -1)
verbose = (config_commit_verbose < 0) ? 0 : config_commit_verbose;
diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c
index 41b0750e5a..acaf42b2d9 100644
--- a/builtin/interpret-trailers.c
+++ b/builtin/interpret-trailers.c
@@ -93,37 +93,6 @@ static int parse_opt_parse(const struct option *opt, const char *arg,
return 0;
}
-static struct tempfile *trailers_tempfile;
-
-static FILE *create_in_place_tempfile(const char *file)
-{
- struct stat st;
- struct strbuf filename_template = STRBUF_INIT;
- const char *tail;
- FILE *outfile;
-
- if (stat(file, &st))
- die_errno(_("could not stat %s"), file);
- if (!S_ISREG(st.st_mode))
- die(_("file %s is not a regular file"), file);
- if (!(st.st_mode & S_IWUSR))
- die(_("file %s is not writable by user"), file);
-
- /* Create temporary file in the same directory as the original */
- tail = strrchr(file, '/');
- if (tail)
- strbuf_add(&filename_template, file, tail - file + 1);
- strbuf_addstr(&filename_template, "git-interpret-trailers-XXXXXX");
-
- trailers_tempfile = xmks_tempfile_m(filename_template.buf, st.st_mode);
- strbuf_release(&filename_template);
- outfile = fdopen_tempfile(trailers_tempfile, "w");
- if (!outfile)
- die_errno(_("could not open temporary file"));
-
- return outfile;
-}
-
static void read_input_file(struct strbuf *sb, const char *file)
{
if (file) {
@@ -140,55 +109,31 @@ static void interpret_trailers(const struct process_trailer_options *opts,
struct list_head *new_trailer_head,
const char *file)
{
- LIST_HEAD(head);
- struct strbuf sb = STRBUF_INIT;
- struct strbuf trailer_block_sb = STRBUF_INIT;
- struct trailer_block *trailer_block;
- FILE *outfile = stdout;
+ struct strbuf input = STRBUF_INIT;
+ struct strbuf out = STRBUF_INIT;
+ struct tempfile *tempfile = NULL;
+ int fd = 1;
trailer_config_init();
- read_input_file(&sb, file);
+ read_input_file(&input, file);
- if (opts->in_place)
- outfile = create_in_place_tempfile(file);
-
- trailer_block = parse_trailers(opts, sb.buf, &head);
-
- /* Print the lines before the trailer block */
- if (!opts->only_trailers)
- fwrite(sb.buf, 1, trailer_block_start(trailer_block), outfile);
-
- if (!opts->only_trailers && !blank_line_before_trailer_block(trailer_block))
- fprintf(outfile, "\n");
-
-
- if (!opts->only_input) {
- LIST_HEAD(config_head);
- LIST_HEAD(arg_head);
- parse_trailers_from_config(&config_head);
- parse_trailers_from_command_line_args(&arg_head, new_trailer_head);
- list_splice(&config_head, &arg_head);
- process_trailers_lists(&head, &arg_head);
+ if (opts->in_place) {
+ tempfile = trailer_create_in_place_tempfile(file);
+ if (!tempfile)
+ die(NULL);
+ fd = tempfile->fd;
}
+ process_trailers(opts, new_trailer_head, &input, &out);
- /* Print trailer block. */
- format_trailers(opts, &head, &trailer_block_sb);
- free_trailers(&head);
- fwrite(trailer_block_sb.buf, 1, trailer_block_sb.len, outfile);
- strbuf_release(&trailer_block_sb);
-
- /* Print the lines after the trailer block as is. */
- if (!opts->only_trailers)
- fwrite(sb.buf + trailer_block_end(trailer_block), 1,
- sb.len - trailer_block_end(trailer_block), outfile);
- trailer_block_release(trailer_block);
-
+ if (write_in_full(fd, out.buf, out.len) < 0)
+ die_errno(_("could not write to temporary file '%s'"), file);
if (opts->in_place)
- if (rename_tempfile(&trailers_tempfile, file))
+ if (rename_tempfile(&tempfile, file))
die_errno(_("could not rename temporary file to %s"), file);
- strbuf_release(&sb);
+ strbuf_release(&input);
+ strbuf_release(&out);
}
int cmd_interpret_trailers(int argc,
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 8c1316db38..a1c7d78196 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -36,6 +36,7 @@
#include "reset.h"
#include "trace2.h"
#include "hook.h"
+#include "trailer.h"
static char const * const builtin_rebase_usage[] = {
N_("git rebase [-i] [options] [--exec <cmd>] "
@@ -113,6 +114,7 @@ struct rebase_options {
enum action action;
char *reflog_action;
int signoff;
+ struct strvec trailer_args;
int allow_rerere_autoupdate;
int keep_empty;
int autosquash;
@@ -143,6 +145,7 @@ struct rebase_options {
.flags = REBASE_NO_QUIET, \
.git_am_opts = STRVEC_INIT, \
.exec = STRING_LIST_INIT_NODUP, \
+ .trailer_args = STRVEC_INIT, \
.git_format_patch_opt = STRBUF_INIT, \
.fork_point = -1, \
.reapply_cherry_picks = -1, \
@@ -166,6 +169,7 @@ static void rebase_options_release(struct rebase_options *opts)
free(opts->strategy);
string_list_clear(&opts->strategy_opts, 0);
strbuf_release(&opts->git_format_patch_opt);
+ strvec_clear(&opts->trailer_args);
}
static struct replay_opts get_replay_opts(const struct rebase_options *opts)
@@ -177,6 +181,10 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts)
sequencer_init_config(&replay);
replay.signoff = opts->signoff;
+
+ for (size_t i = 0; i < opts->trailer_args.nr; i++)
+ strvec_push(&replay.trailer_args, opts->trailer_args.v[i]);
+
replay.allow_ff = !(opts->flags & REBASE_FORCE);
if (opts->allow_rerere_autoupdate)
replay.allow_rerere_auto = opts->allow_rerere_autoupdate;
@@ -1134,6 +1142,8 @@ int cmd_rebase(int argc,
.flags = PARSE_OPT_NOARG,
.defval = REBASE_DIFFSTAT,
},
+ OPT_STRVEC(0, "trailer", &options.trailer_args, N_("trailer"),
+ N_("add custom trailer(s)")),
OPT_BOOL(0, "signoff", &options.signoff,
N_("add a Signed-off-by trailer to each commit")),
OPT_BOOL(0, "committer-date-is-author-date",
@@ -1287,6 +1297,12 @@ int cmd_rebase(int argc,
builtin_rebase_options,
builtin_rebase_usage, 0);
+ if (options.trailer_args.nr) {
+ if (validate_trailer_args(&options.trailer_args))
+ die(NULL);
+ options.flags |= REBASE_FORCE;
+ }
+
if (preserve_merges_selected)
die(_("--preserve-merges was replaced by --rebase-merges\n"
"Note: Your `pull.rebase` configuration may also be set to 'preserve',\n"
@@ -1544,6 +1560,9 @@ int cmd_rebase(int argc,
if (options.root && !options.onto_name)
imply_merge(&options, "--root without --onto");
+ if (options.trailer_args.nr)
+ imply_merge(&options, "--trailer");
+
if (isatty(2) && options.flags & REBASE_NO_QUIET)
strbuf_addstr(&options.git_format_patch_opt, " --progress");
diff --git a/builtin/tag.c b/builtin/tag.c
index aeb04c487f..e0f05f94fd 100644
--- a/builtin/tag.c
+++ b/builtin/tag.c
@@ -499,8 +499,8 @@ int cmd_tag(int argc,
OPT_CALLBACK_F('m', "message", &msg, N_("message"),
N_("tag message"), PARSE_OPT_NONEG, parse_msg_arg),
OPT_FILENAME('F', "file", &msgfile, N_("read message from file")),
- OPT_PASSTHRU_ARGV(0, "trailer", &trailer_args, N_("trailer"),
- N_("add custom trailer(s)"), PARSE_OPT_NONEG),
+ OPT_STRVEC(0, "trailer", &trailer_args, N_("trailer"),
+ N_("add custom trailer(s)")),
OPT_BOOL('e', "edit", &edit_flag, N_("force edit of tag message")),
OPT_BOOL('s', "sign", &opt.sign, N_("annotated and GPG-signed tag")),
OPT_CLEANUP(&cleanup_arg),
@@ -568,6 +568,9 @@ int cmd_tag(int argc,
if (cmdmode == 'l')
setup_auto_pager("tag", 1);
+ if (trailer_args.nr)
+ trailer_config_init();
+
if (opt.sign == -1)
opt.sign = cmdmode ? 0 : config_sign_tag > 0;
diff --git a/sequencer.c b/sequencer.c
index aafd0bc959..e5af49cecd 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -209,6 +209,7 @@ static GIT_PATH_FUNC(rebase_path_reschedule_failed_exec, "rebase-merge/reschedul
static GIT_PATH_FUNC(rebase_path_no_reschedule_failed_exec, "rebase-merge/no-reschedule-failed-exec")
static GIT_PATH_FUNC(rebase_path_drop_redundant_commits, "rebase-merge/drop_redundant_commits")
static GIT_PATH_FUNC(rebase_path_keep_redundant_commits, "rebase-merge/keep_redundant_commits")
+static GIT_PATH_FUNC(rebase_path_trailer, "rebase-merge/trailer")
/*
* A 'struct replay_ctx' represents the private state of the sequencer.
@@ -420,6 +421,7 @@ void replay_opts_release(struct replay_opts *opts)
if (opts->revs)
release_revisions(opts->revs);
free(opts->revs);
+ strvec_clear(&opts->trailer_args);
replay_ctx_release(ctx);
free(opts->ctx);
}
@@ -2027,12 +2029,15 @@ static int append_squash_message(struct strbuf *buf, const char *body,
if (is_fixup_flag(command, flag) && !seen_squash(ctx)) {
/*
* We're replacing the commit message so we need to
- * append the Signed-off-by: trailer if the user
- * requested '--signoff'.
+ * append any trailers if the user requested
+ * '--signoff' or '--trailer'.
*/
if (opts->signoff)
append_signoff(buf, 0, 0);
+ if (opts->trailer_args.nr)
+ amend_strbuf_with_trailers(buf, &opts->trailer_args);
+
if ((command == TODO_FIXUP) &&
(flag & TODO_REPLACE_FIXUP_MSG) &&
(file_exists(rebase_path_fixup_msg()) ||
@@ -2451,6 +2456,9 @@ static int do_pick_commit(struct repository *r,
if (opts->signoff && !is_fixup(command))
append_signoff(&ctx->message, 0, 0);
+ if (opts->trailer_args.nr && !is_fixup(command))
+ amend_strbuf_with_trailers(&ctx->message, &opts->trailer_args);
+
if (is_rebase_i(opts) && write_author_script(msg.message) < 0)
res = -1;
else if (!opts->strategy ||
@@ -3180,6 +3188,33 @@ static void read_strategy_opts(struct replay_opts *opts, struct strbuf *buf)
parse_strategy_opts(opts, buf->buf);
}
+static int read_trailers(struct replay_opts *opts, struct strbuf *buf)
+{
+ ssize_t len;
+
+ strbuf_reset(buf);
+ len = strbuf_read_file(buf, rebase_path_trailer(), 0);
+ if (len > 0) {
+ char *p = buf->buf, *nl;
+
+ trailer_config_init();
+
+ while ((nl = strchr(p, '\n'))) {
+ *nl = '\0';
+ if (!*p)
+ return error(_("trailers file contains empty line"));
+ strvec_push(&opts->trailer_args, p);
+ p = nl + 1;
+ }
+ } else if (!len) {
+ return error(_("trailers file is empty"));
+ } else if (errno != ENOENT) {
+ return error(_("cannot read trailers files"));
+ }
+
+ return 0;
+}
+
static int read_populate_opts(struct replay_opts *opts)
{
struct replay_ctx *ctx = opts->ctx;
@@ -3241,6 +3276,11 @@ static int read_populate_opts(struct replay_opts *opts)
opts->keep_redundant_commits = 1;
read_strategy_opts(opts, &buf);
+
+ if (read_trailers(opts, &buf)) {
+ ret = -1;
+ goto done_rebase_i;
+ }
strbuf_reset(&buf);
if (read_oneliner(&ctx->current_fixups,
@@ -3336,6 +3376,14 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,
write_file(rebase_path_reschedule_failed_exec(), "%s", "");
else
write_file(rebase_path_no_reschedule_failed_exec(), "%s", "");
+ if (opts->trailer_args.nr) {
+ struct strbuf buf = STRBUF_INIT;
+
+ for (size_t i = 0; i < opts->trailer_args.nr; i++)
+ strbuf_addf(&buf, "%s\n", opts->trailer_args.v[i]);
+ write_file(rebase_path_trailer(), "%s", buf.buf);
+ strbuf_release(&buf);
+ }
return 0;
}
diff --git a/sequencer.h b/sequencer.h
index 719684c8a9..bea20da085 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -57,6 +57,8 @@ struct replay_opts {
int ignore_date;
int commit_use_reference;
+ struct strvec trailer_args;
+
int mainline;
char *gpg_sign;
@@ -84,6 +86,7 @@ struct replay_opts {
#define REPLAY_OPTS_INIT { \
.edit = -1, \
.action = -1, \
+ .trailer_args = STRVEC_INIT, \
.xopts = STRVEC_INIT, \
.ctx = replay_ctx_new(), \
}
diff --git a/t/meson.build b/t/meson.build
index 9b2fa4dee8..e539117ca4 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -393,6 +393,7 @@ integration_tests = [
't3436-rebase-more-options.sh',
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
+ 't3440-rebase-trailer.sh',
't3450-history.sh',
't3451-history-reword.sh',
't3500-cherry.sh',
diff --git a/t/t3440-rebase-trailer.sh b/t/t3440-rebase-trailer.sh
new file mode 100755
index 0000000000..8b47579566
--- /dev/null
+++ b/t/t3440-rebase-trailer.sh
@@ -0,0 +1,147 @@
+#!/bin/sh
+#
+
+test_description='git rebase --trailer integration tests
+We verify that --trailer works with the merge backend,
+and that it is rejected early when the apply backend is requested.'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh # test_commit_message, helpers
+
+REVIEWED_BY_TRAILER="Reviewed-by: Dev <dev@example.com>"
+SP=" "
+
+test_expect_success 'setup repo with a small history' '
+ git commit --allow-empty -m "Initial empty commit" &&
+ test_commit first file a &&
+ test_commit second file &&
+ git checkout -b conflict-branch first &&
+ test_commit file-2 file-2 &&
+ test_commit conflict file &&
+ test_commit third file &&
+ git checkout main
+'
+
+test_expect_success 'apply backend is rejected with --trailer' '
+ git checkout -B apply-backend third &&
+ test_expect_code 128 \
+ git rebase --apply --trailer "$REVIEWED_BY_TRAILER" HEAD^ 2>err &&
+ test_grep "fatal: --trailer requires the merge backend" err
+'
+
+test_expect_success 'reject empty --trailer argument' '
+ git checkout -B empty-trailer third &&
+ test_expect_code 128 git rebase --trailer "" HEAD^ 2>err &&
+ test_grep "empty --trailer" err
+'
+
+test_expect_success 'reject trailer with missing key before separator' '
+ git checkout -B missing-key third &&
+ test_expect_code 128 git rebase --trailer ": no-key" HEAD^ 2>err &&
+ test_grep "missing key before separator" err
+'
+
+test_expect_success 'allow trailer with missing value after separator' '
+ git checkout -B missing-value third &&
+ git rebase --trailer "Acked-by:" HEAD^ &&
+ test_commit_message HEAD <<-EOF
+ third
+
+ Acked-by:${SP}
+ EOF
+'
+
+test_expect_success 'CLI trailer duplicates allowed; replace policy keeps last' '
+ git checkout -B replace-policy third &&
+ git -c trailer.Bug.ifexists=replace -c trailer.Bug.ifmissing=add \
+ rebase --trailer "Bug: 123" --trailer "Bug: 456" HEAD^ &&
+ test_commit_message HEAD <<-EOF
+ third
+
+ Bug: 456
+ EOF
+'
+
+test_expect_success 'multiple Signed-off-by trailers all preserved' '
+ git checkout -B multiple-signoff third &&
+ git rebase --trailer "Signed-off-by: Dev A <a@example.com>" \
+ --trailer "Signed-off-by: Dev B <b@example.com>" HEAD^ &&
+ test_commit_message HEAD <<-EOF
+ third
+
+ Signed-off-by: Dev A <a@example.com>
+ Signed-off-by: Dev B <b@example.com>
+ EOF
+'
+
+test_expect_success 'rebase --trailer adds trailer after conflicts' '
+ git checkout -B trailer-conflict third &&
+ test_commit fourth file &&
+ test_must_fail git rebase --trailer "$REVIEWED_BY_TRAILER" second &&
+ git checkout --theirs file &&
+ git add file &&
+ git rebase --continue &&
+ test_commit_message HEAD <<-EOF &&
+ fourth
+
+ $REVIEWED_BY_TRAILER
+ EOF
+ test_commit_message HEAD^ <<-EOF
+ third
+
+ $REVIEWED_BY_TRAILER
+ EOF
+'
+
+test_expect_success '--trailer handles fixup commands in todo list' '
+ git checkout -B fixup-trailer third &&
+ test_commit fixup-base base &&
+ test_commit fixup-second second &&
+ cat >todo <<-\EOF &&
+ pick fixup-base fixup-base
+ fixup fixup-second fixup-second
+ EOF
+ (
+ set_replace_editor todo &&
+ git rebase -i --trailer "$REVIEWED_BY_TRAILER" HEAD~2
+ ) &&
+ test_commit_message HEAD <<-EOF &&
+ fixup-base
+
+ $REVIEWED_BY_TRAILER
+ EOF
+ git reset --hard fixup-second &&
+ cat >todo <<-\EOF &&
+ pick fixup-base fixup-base
+ fixup -C fixup-second fixup-second
+ EOF
+ (
+ set_replace_editor todo &&
+ git rebase -i --trailer "$REVIEWED_BY_TRAILER" HEAD~2
+ ) &&
+ test_commit_message HEAD <<-EOF
+ fixup-second
+
+ $REVIEWED_BY_TRAILER
+ EOF
+'
+
+test_expect_success 'rebase --root honors trailer.<name>.key' '
+ git checkout -B root-trailer first &&
+ git -c trailer.review.key=Reviewed-by rebase --root \
+ --trailer=review="Dev <dev@example.com>" &&
+ test_commit_message HEAD <<-EOF &&
+ first
+
+ Reviewed-by: Dev <dev@example.com>
+ EOF
+ test_commit_message HEAD^ <<-EOF
+ Initial empty commit
+
+ Reviewed-by: Dev <dev@example.com>
+ EOF
+'
+test_done
diff --git a/trailer.c b/trailer.c
index 911a81ed99..ca8abd1882 100644
--- a/trailer.c
+++ b/trailer.c
@@ -7,8 +7,11 @@
#include "string-list.h"
#include "run-command.h"
#include "commit.h"
+#include "strvec.h"
#include "trailer.h"
#include "list.h"
+#include "tempfile.h"
+
/*
* Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org>
*/
@@ -772,6 +775,35 @@ void parse_trailers_from_command_line_args(struct list_head *arg_head,
free(cl_separators);
}
+int validate_trailer_args(const struct strvec *cli_args)
+{
+ char *cl_separators;
+ int ret = 0;
+
+ trailer_config_init();
+
+ cl_separators = xstrfmt("=%s", separators);
+
+ for (size_t i = 0; i < cli_args->nr; i++) {
+ const char *txt = cli_args->v[i];
+ ssize_t separator_pos;
+
+ if (!*txt) {
+ ret = error(_("empty --trailer argument"));
+ goto out;
+ }
+ separator_pos = find_separator(txt, cl_separators);
+ if (separator_pos == 0) {
+ ret = error(_("invalid trailer '%s': missing key before separator"),
+ txt);
+ goto out;
+ }
+ }
+out:
+ free(cl_separators);
+ return ret;
+}
+
static const char *next_line(const char *str)
{
const char *nl = strchrnul(str, '\n');
@@ -1224,14 +1256,146 @@ void trailer_iterator_release(struct trailer_iterator *iter)
strbuf_release(&iter->key);
}
-int amend_file_with_trailers(const char *path, const struct strvec *trailer_args)
+struct tempfile *trailer_create_in_place_tempfile(const char *file)
+{
+ struct tempfile *tempfile = NULL;
+ struct stat st;
+ struct strbuf filename_template = STRBUF_INIT;
+ const char *tail;
+
+ if (stat(file, &st)) {
+ error_errno(_("could not stat %s"), file);
+ return NULL;
+ }
+ if (!S_ISREG(st.st_mode)) {
+ error(_("file %s is not a regular file"), file);
+ return NULL;
+ }
+ if (!(st.st_mode & S_IWUSR)) {
+ error(_("file %s is not writable by user"), file);
+ return NULL;
+ }
+ /* Create temporary file in the same directory as the original */
+ tail = find_last_dir_sep(file);
+ if (tail)
+ strbuf_add(&filename_template, file, tail - file + 1);
+ strbuf_addstr(&filename_template, "git-interpret-trailers-XXXXXX");
+
+ tempfile = mks_tempfile_m(filename_template.buf, st.st_mode);
+
+ strbuf_release(&filename_template);
+
+ return tempfile;
+}
+
+int amend_strbuf_with_trailers(struct strbuf *buf,
+ const struct strvec *trailer_args)
{
- struct child_process run_trailer = CHILD_PROCESS_INIT;
+ struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
+ LIST_HEAD(new_trailer_head);
+ struct strbuf out = STRBUF_INIT;
+ size_t i;
+ int ret = 0;
+
+ opts.no_divider = 1;
+
+ for (i = 0; i < trailer_args->nr; i++) {
+ const char *text = trailer_args->v[i];
+ struct new_trailer_item *item;
+
+ if (!*text) {
+ ret = error(_("empty --trailer argument"));
+ goto out;
+ }
+ item = xcalloc(1, sizeof(*item));
+ item->text = xstrdup(text);
+ list_add_tail(&item->list, &new_trailer_head);
+ }
- run_trailer.git_cmd = 1;
- strvec_pushl(&run_trailer.args, "interpret-trailers",
- "--in-place", "--no-divider",
- path, NULL);
- strvec_pushv(&run_trailer.args, trailer_args->v);
- return run_command(&run_trailer);
+ process_trailers(&opts, &new_trailer_head, buf, &out);
+
+ strbuf_swap(buf, &out);
+out:
+ strbuf_release(&out);
+ free_trailers(&new_trailer_head);
+
+ return ret;
+}
+
+static int write_file_in_place(const char *path, const struct strbuf *buf)
+{
+ struct tempfile *tempfile = trailer_create_in_place_tempfile(path);
+ if (!tempfile)
+ return -1;
+
+ if (write_in_full(tempfile->fd, buf->buf, buf->len) < 0)
+ return error_errno(_("could not write to temporary file"));
+
+ if (rename_tempfile(&tempfile, path))
+ return error_errno(_("could not rename temporary file to %s"), path);
+
+ return 0;
+}
+
+int amend_file_with_trailers(const char *path,
+ const struct strvec *trailer_args)
+{
+ struct strbuf buf = STRBUF_INIT;
+ int ret = 0;
+
+ if (!trailer_args)
+ BUG("amend_file_with_trailers called with NULL trailer_args");
+ if (!trailer_args->nr)
+ return 0;
+
+ if (validate_trailer_args(trailer_args)) {
+ ret = -1;
+ goto out;
+ }
+ if (strbuf_read_file(&buf, path, 0) < 0)
+ ret = error_errno(_("could not read '%s'"), path);
+ else
+ amend_strbuf_with_trailers(&buf, trailer_args);
+
+ if (!ret)
+ ret = write_file_in_place(path, &buf);
+out:
+ strbuf_release(&buf);
+ return ret;
+}
+
+void process_trailers(const struct process_trailer_options *opts,
+ struct list_head *new_trailer_head,
+ struct strbuf *input, struct strbuf *out)
+{
+ LIST_HEAD(head);
+ struct trailer_block *trailer_block;
+
+ trailer_block = parse_trailers(opts, input->buf, &head);
+
+ /* Print the lines before the trailer block */
+ if (!opts->only_trailers)
+ strbuf_add(out, input->buf, trailer_block_start(trailer_block));
+
+ if (!opts->only_trailers && !blank_line_before_trailer_block(trailer_block))
+ strbuf_addch(out, '\n');
+
+ if (!opts->only_input) {
+ LIST_HEAD(config_head);
+ LIST_HEAD(arg_head);
+ parse_trailers_from_config(&config_head);
+ parse_trailers_from_command_line_args(&arg_head, new_trailer_head);
+ list_splice(&config_head, &arg_head);
+ process_trailers_lists(&head, &arg_head);
+ }
+
+ /* Print trailer block. */
+ format_trailers(opts, &head, out);
+ free_trailers(&head);
+
+ /* Print the lines after the trailer block as is. */
+ if (!opts->only_trailers)
+ strbuf_add(out, input->buf + trailer_block_end(trailer_block),
+ input->len - trailer_block_end(trailer_block));
+ trailer_block_release(trailer_block);
}
diff --git a/trailer.h b/trailer.h
index 4740549586..b49338858c 100644
--- a/trailer.h
+++ b/trailer.h
@@ -68,6 +68,8 @@ void parse_trailers_from_config(struct list_head *config_head);
void parse_trailers_from_command_line_args(struct list_head *arg_head,
struct list_head *new_trailer_head);
+int validate_trailer_args(const struct strvec *cli_args);
+
void process_trailers_lists(struct list_head *head,
struct list_head *arg_head);
@@ -196,10 +198,38 @@ int trailer_iterator_advance(struct trailer_iterator *iter);
void trailer_iterator_release(struct trailer_iterator *iter);
/*
- * Augment a file to add trailers to it by running git-interpret-trailers.
- * This calls run_command() and its return value is the same (i.e. 0 for
- * success, various non-zero for other errors). See run-command.h.
+ * Append trailers specified in trailer_args to buf in-place.
+ *
+ * Each element of trailer_args should be in the same format as the value
+ * accepted by --trailer=<trailer> (i.e., without the --trailer= prefix).
+ */
+int amend_strbuf_with_trailers(struct strbuf *buf,
+ const struct strvec *trailer_args);
+
+/*
+ * Augment a file by appending trailers specified in trailer_args.
+ *
+ * Each element of trailer_args should be in the same format as the value
+ * accepted by --trailer=<trailer> (i.e., without the --trailer= prefix).
+ *
+ * Returns 0 on success or a non-zero error code on failure.
*/
int amend_file_with_trailers(const char *path, const struct strvec *trailer_args);
+/*
+ * Create a tempfile ""git-interpret-trailers-XXXXXX" in the same
+ * directory as file.
+ */
+struct tempfile *trailer_create_in_place_tempfile(const char *file);
+
+/*
+ * Rewrite the contents of input by processing its trailer block according to
+ * opts and (optionally) appending trailers from new_trailer_head.
+ *
+ * The rewritten message is appended to out (callers should strbuf_reset()
+ * first if needed).
+ */
+void process_trailers(const struct process_trailer_options *opts,
+ struct list_head *new_trailer_head,
+ struct strbuf *input, struct strbuf *out);
#endif /* TRAILER_H */