+
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
* --[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:
#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>] "
enum action action;
char *reflog_action;
int signoff;
+ struct strvec trailer_args;
int allow_rerere_autoupdate;
int keep_empty;
int autosquash;
.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, \
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)
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;
.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",
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"
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");
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.
if (opts->revs)
release_revisions(opts->revs);
free(opts->revs);
+ strvec_clear(&opts->trailer_args);
replay_ctx_release(ctx);
free(opts->ctx);
}
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()) ||
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 ||
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;
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,
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;
}
int ignore_date;
int commit_use_reference;
+ struct strvec trailer_args;
+
int mainline;
char *gpg_sign;
#define REPLAY_OPTS_INIT { \
.edit = -1, \
.action = -1, \
+ .trailer_args = STRVEC_INIT, \
.xopts = STRVEC_INIT, \
.ctx = replay_ctx_new(), \
}
'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',
--- /dev/null
+#!/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