From: Li Chen <chenl311@xxxxxxxxxxxxxxx> Implement a new `--trailer <text>` option for `git rebase` (support merge backend only now), which appends arbitrary trailer lines to each rebased commit message. Reject early if used with the apply backend (git am) since it lacks message‑filter/trailer hook. Automatically set REBASE_FORCE when any trailer is supplied. Reviewed-by: Li Chen <chenl311@xxxxxxxxxxxxxxx> --- Documentation/git-rebase.adoc | 8 +++ builtin/rebase.c | 70 ++++++++++++++++++++++ sequencer.c | 13 ++++ sequencer.h | 2 + t/meson.build | 1 + t/t3440-rebase-trailer.sh | 108 ++++++++++++++++++++++++++++++++++ 6 files changed, 202 insertions(+) create mode 100755 t/t3440-rebase-trailer.sh diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc index 1f3d152035..757016e529 100644 --- a/Documentation/git-rebase.adoc +++ b/Documentation/git-rebase.adoc @@ -525,6 +525,14 @@ See also INCOMPATIBLE OPTIONS below. Append a `Reviewed-by:` trailer whose value is taken from the current committer identity, exactly like `--signoff` appends a Signed-off-by:` trailer. + +--trailer <trailer>:: + Append the given trailer line(s) to every rebased commit + message, processed via linkgit:git-interpret-trailers[1]. + When this option is present *rebase automatically enables* + `--force-rebase` so that fast‑forwarded commits are also + rewritten. + See also INCOMPATIBLE OPTIONS below. -i:: diff --git a/builtin/rebase.c b/builtin/rebase.c index b288aedfb1..df65a1e040 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -36,6 +36,9 @@ #include "reset.h" #include "trace2.h" #include "hook.h" +#include "trailer.h" + +static const char trailer_state_name[] = "trailer"; static char const * const builtin_rebase_usage[] = { N_("git rebase [-i] [options] [--exec <cmd>] " @@ -46,6 +49,8 @@ static char const * const builtin_rebase_usage[] = { NULL }; +static struct strvec trailer_args = STRVEC_INIT; + static GIT_PATH_FUNC(path_squash_onto, "rebase-merge/squash-onto") static GIT_PATH_FUNC(path_interactive, "rebase-merge/interactive") static GIT_PATH_FUNC(apply_dir, "rebase-apply") @@ -114,6 +119,7 @@ struct rebase_options { char *reflog_action; int signoff; int reviewby; + struct strvec trailer_args; int allow_rerere_autoupdate; int keep_empty; int autosquash; @@ -144,6 +150,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, \ @@ -167,6 +174,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) @@ -179,6 +187,10 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts) replay.signoff = opts->signoff; replay.reviewby = opts->reviewby; + + 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; @@ -437,6 +449,8 @@ static int read_basic_state(struct rebase_options *opts) struct strbuf head_name = STRBUF_INIT; struct strbuf buf = STRBUF_INIT; struct object_id oid; + struct strbuf t = STRBUF_INIT, one = STRBUF_INIT; + const char *path = state_dir_path(trailer_state_name, opts); if (!read_oneliner(&head_name, state_dir_path("head-name", opts), READ_ONELINER_WARN_MISSING) || @@ -509,6 +523,22 @@ static int read_basic_state(struct rebase_options *opts) strbuf_release(&buf); + if (strbuf_read_file(&t, path, 0) >= 0) { + const char *p = t.buf, *end = t.buf + t.len; + + while (p < end) { + const char *nl = memchr(p, '\n', end - p); + strbuf_reset(&one); + strbuf_add(&one, p, nl ? nl - p : end - p); + if (one.len) /* skip empty line */ + strvec_push(&opts->trailer_args, + strbuf_detach(&one, NULL)); + p = nl ? nl + 1 : end; + } + strbuf_release(&one); + } + strbuf_release(&t); + return 0; } @@ -537,6 +567,28 @@ static int rebase_write_basic_state(struct rebase_options *opts) if (opts->reviewby) write_file(state_dir_path("reviewby", opts), "--reviewby"); + /* + * save opts->trailer_args into state_dir/trailer + */ + if (opts->trailer_args.nr) { + struct strbuf buf = STRBUF_INIT; + size_t i; + + for (i = 0; i < opts->trailer_args.nr; i++) { + strbuf_addstr(&buf, opts->trailer_args.v[i]); + strbuf_addch(&buf, '\n'); + } + write_file(state_dir_path(trailer_state_name, opts), + "%s", buf.buf); + strbuf_release(&buf); + } else { + /* + * but if rebase doesn't pass any --trailer, + * and state dir still have residual files,let's delete it。 + */ + unlink_or_warn(state_dir_path(trailer_state_name, opts)); + } + return 0; } @@ -1140,6 +1192,7 @@ int cmd_rebase(int argc, .flags = PARSE_OPT_NOARG, .defval = REBASE_DIFFSTAT, }, + OPT_PASSTHRU_ARGV(0, "trailer", &trailer_args, N_("trailer"), N_("add custom trailer(s)"), PARSE_OPT_NONEG), OPT_BOOL(0, "signoff", &options.signoff, N_("add a Signed-off-by trailer to each commit")), OPT_BOOL(0, "reviewby", &options.reviewby, @@ -1292,6 +1345,13 @@ int cmd_rebase(int argc, builtin_rebase_options, builtin_rebase_usage, 0); + for (i = 0; i < trailer_args.nr; i++) + strvec_push(&options.trailer_args, trailer_args.v[i]); + + /* if add --trailer,force rebase */ + if (options.trailer_args.nr) + 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" @@ -1549,6 +1609,16 @@ int cmd_rebase(int argc, if (options.root && !options.onto_name) imply_merge(&options, "--root without --onto"); + /* + * The apply‑based backend (git am) cannot append trailers because + * it lacks a message‑filter facility. Reject early, before any + * state (index, HEAD, etc.) is modified. + */ + if (options.type == REBASE_APPLY && options.trailer_args.nr) + die(_("the --apply backend (git am) cannot currently handle " + "--trailer; please omit --apply or use " + "the merge/interactive backend")); + if (isatty(2) && options.flags & REBASE_NO_QUIET) strbuf_addstr(&options.git_format_patch_opt, " --progress"); diff --git a/sequencer.c b/sequencer.c index 01933882ed..b61c668c39 100644 --- a/sequencer.c +++ b/sequencer.c @@ -429,6 +429,7 @@ void replay_opts_release(struct replay_opts *opts) free(opts->revs); replay_ctx_release(ctx); free(opts->ctx); + strvec_clear(&opts->trailer_args); } int sequencer_remove_state(struct replay_opts *opts) @@ -2506,6 +2507,18 @@ static int do_pick_commit(struct repository *r, _("dropping %s %s -- patch contents already upstream\n"), oid_to_hex(&commit->object.oid), msg.subject); } /* else allow == 0 and there's nothing special to do */ + + if (!res && opts->trailer_args.nr && !drop_commit) { + const char *trailer_file = + msg_file ? msg_file : git_path_merge_msg(r); + + if (amend_file_with_trailers(trailer_file, + &opts->trailer_args)) { + res = error(_("unable to add trailers to commit message")); + goto leave; + } + } + if (!opts->no_commit && !drop_commit) { if (author || command == TODO_REVERT || (flags & AMEND_MSG)) res = do_commit(r, msg_file, author, opts, flags, diff --git a/sequencer.h b/sequencer.h index 82b79fd1e8..4f5ea2d818 100644 --- a/sequencer.h +++ b/sequencer.h @@ -46,6 +46,7 @@ struct replay_opts { int no_commit; int signoff; int reviewby; + struct strvec trailer_args; int allow_ff; int allow_rerere_auto; int allow_empty; @@ -88,6 +89,7 @@ struct replay_opts { .action = -1, \ .xopts = STRVEC_INIT, \ .ctx = replay_ctx_new(), \ + .trailer_args = STRVEC_INIT, \ } /* diff --git a/t/meson.build b/t/meson.build index 327fa461fd..fc4b64fca1 100644 --- a/t/meson.build +++ b/t/meson.build @@ -393,6 +393,7 @@ integration_tests = [ 't3437-rebase-fixup-options.sh', 't3438-rebase-broken-files.sh', 't3439-rebase-reviewby.sh', + 't3440-rebase-trailer.sh', 't3500-cherry.sh', 't3501-revert-cherry-pick.sh', 't3502-cherry-pick-merge.sh', diff --git a/t/t3440-rebase-trailer.sh b/t/t3440-rebase-trailer.sh new file mode 100755 index 0000000000..6dc08e9633 --- /dev/null +++ b/t/t3440-rebase-trailer.sh @@ -0,0 +1,108 @@ +#!/bin/sh +# + +test_description='git rebase --trailer integration tests +We verify that --trailer on the merge/interactive/exec/root backends, +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 + +############################################################################ +# 1. repository setup +############################################################################ + +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 && + ident="$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" +' + +create_expect () { + cat >"$1" <<-EOF + $2 + + Reviewed-by: Dev <dev@xxxxxxxxxxx> + EOF +} +# golden files: +create_expect initial-signed "Initial empty commit" +create_expect first-signed "first" +create_expect second-signed "second" +create_expect file2-signed "file-2" +create_expect third-signed "third" +create_expect conflict-signed "conflict" + +############################################################################ +# 2. explicitly reject --apply + --trailer +############################################################################ + +test_expect_success 'apply backend is rejected when --trailer is given' ' + git reset --hard third && + git tag before-apply && + test_expect_code 128 \ + git rebase --apply --trailer "Reviewed-by: Dev <dev@xxxxxxxxxxx>" \ + HEAD^ && + git diff --quiet before-apply..HEAD # prove nothing moved +' + +############################################################################ +# 3. --no‑op: same range, no changes +############################################################################ + +test_expect_success '--trailer without range change is a no‑op' ' + git checkout main && + git tag before-noop && + git rebase --trailer "Reviewed-by: Dev <dev@xxxxxxxxxxx>" HEAD && + git diff --quiet before-noop..HEAD +' + +############################################################################ +# 4. merge backend (-m), conflict resolution path +############################################################################ + +test_expect_success 'rebase -m --trailer adds trailer after conflicts' ' + git reset --hard third && + test_must_fail git rebase -m \ + --trailer "Reviewed-by: Dev <dev@xxxxxxxxxxx>" \ + second third && + git checkout --theirs file && + git add file && + GIT_EDITOR=: git rebase --continue && + git show --no-patch --pretty=format:%B HEAD~2 >actual && + test_cmp file2-signed actual +' + +############################################################################ +# 5. --exec path +############################################################################ + +test_expect_success 'rebase --exec --trailer adds trailer' ' + test_when_finished "rm -f touched" && + git rebase --exec "touch touched" \ + --trailer "Reviewed-by: Dev <dev@xxxxxxxxxxx>" \ + first^ first && + test_path_is_file touched && + test_commit_message HEAD first-signed +' + +############################################################################ +# 6. --root path +############################################################################ + +test_expect_success 'rebase --root --trailer updates every commit' ' + git checkout first && + git rebase --root --keep-empty \ + --trailer "Reviewed-by: Dev <dev@xxxxxxxxxxx>" && + test_commit_message HEAD first-signed && + test_commit_message HEAD^ initial-signed +' +test_done -- 2.49.0