Skip to content

Commit e4f9d6b

Browse files
FirstLoveLifegitster
authored andcommitted
rebase: support --trailer
Add a new --trailer=<trailer> option to git rebase to append trailer lines to each rewritten commit message (merge backend only). Because the apply backend does not provide a commit-message filter, reject --trailer when --apply is in effect and require the merge backend instead. This option implies --force-rebase so that fast-forwarded commits are also rewritten. Validate trailer arguments early to avoid starting an interactive rebase with invalid input. Add integration tests covering error paths and trailer insertion across non-interactive and interactive rebases. 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>
1 parent 5e14869 commit e4f9d6b

File tree

6 files changed

+228
-2
lines changed

6 files changed

+228
-2
lines changed

Documentation/git-rebase.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,13 @@ See also INCOMPATIBLE OPTIONS below.
497497
+
498498
See also INCOMPATIBLE OPTIONS below.
499499

500+
--trailer=<trailer>::
501+
Append the given trailer to every rebased commit message, processed
502+
via linkgit:git-interpret-trailers[1]. This option implies
503+
`--force-rebase`.
504+
+
505+
See also INCOMPATIBLE OPTIONS below.
506+
500507
-i::
501508
--interactive::
502509
Make a list of the commits which are about to be rebased. Let the
@@ -653,6 +660,7 @@ are incompatible with the following options:
653660
* --[no-]reapply-cherry-picks when used without --keep-base
654661
* --update-refs
655662
* --root when used without --onto
663+
* --trailer
656664
657665
In addition, the following pairs of options are incompatible:
658666

builtin/rebase.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "reset.h"
3737
#include "trace2.h"
3838
#include "hook.h"
39+
#include "trailer.h"
3940

4041
static char const * const builtin_rebase_usage[] = {
4142
N_("git rebase [-i] [options] [--exec <cmd>] "
@@ -113,6 +114,7 @@ struct rebase_options {
113114
enum action action;
114115
char *reflog_action;
115116
int signoff;
117+
struct strvec trailer_args;
116118
int allow_rerere_autoupdate;
117119
int keep_empty;
118120
int autosquash;
@@ -143,6 +145,7 @@ struct rebase_options {
143145
.flags = REBASE_NO_QUIET, \
144146
.git_am_opts = STRVEC_INIT, \
145147
.exec = STRING_LIST_INIT_NODUP, \
148+
.trailer_args = STRVEC_INIT, \
146149
.git_format_patch_opt = STRBUF_INIT, \
147150
.fork_point = -1, \
148151
.reapply_cherry_picks = -1, \
@@ -166,6 +169,7 @@ static void rebase_options_release(struct rebase_options *opts)
166169
free(opts->strategy);
167170
string_list_clear(&opts->strategy_opts, 0);
168171
strbuf_release(&opts->git_format_patch_opt);
172+
strvec_clear(&opts->trailer_args);
169173
}
170174

171175
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)
177181
sequencer_init_config(&replay);
178182

179183
replay.signoff = opts->signoff;
184+
185+
for (size_t i = 0; i < opts->trailer_args.nr; i++)
186+
strvec_push(&replay.trailer_args, opts->trailer_args.v[i]);
187+
180188
replay.allow_ff = !(opts->flags & REBASE_FORCE);
181189
if (opts->allow_rerere_autoupdate)
182190
replay.allow_rerere_auto = opts->allow_rerere_autoupdate;
@@ -1132,6 +1140,8 @@ int cmd_rebase(int argc,
11321140
.flags = PARSE_OPT_NOARG,
11331141
.defval = REBASE_DIFFSTAT,
11341142
},
1143+
OPT_STRVEC(0, "trailer", &options.trailer_args, N_("trailer"),
1144+
N_("add custom trailer(s)")),
11351145
OPT_BOOL(0, "signoff", &options.signoff,
11361146
N_("add a Signed-off-by trailer to each commit")),
11371147
OPT_BOOL(0, "committer-date-is-author-date",
@@ -1285,6 +1295,12 @@ int cmd_rebase(int argc,
12851295
builtin_rebase_options,
12861296
builtin_rebase_usage, 0);
12871297

1298+
if (options.trailer_args.nr) {
1299+
if (validate_trailer_args(&options.trailer_args))
1300+
die(NULL);
1301+
options.flags |= REBASE_FORCE;
1302+
}
1303+
12881304
if (preserve_merges_selected)
12891305
die(_("--preserve-merges was replaced by --rebase-merges\n"
12901306
"Note: Your `pull.rebase` configuration may also be set to 'preserve',\n"
@@ -1542,6 +1558,9 @@ int cmd_rebase(int argc,
15421558
if (options.root && !options.onto_name)
15431559
imply_merge(&options, "--root without --onto");
15441560

1561+
if (options.trailer_args.nr)
1562+
imply_merge(&options, "--trailer");
1563+
15451564
if (isatty(2) && options.flags & REBASE_NO_QUIET)
15461565
strbuf_addstr(&options.git_format_patch_opt, " --progress");
15471566

sequencer.c

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ static GIT_PATH_FUNC(rebase_path_reschedule_failed_exec, "rebase-merge/reschedul
209209
static GIT_PATH_FUNC(rebase_path_no_reschedule_failed_exec, "rebase-merge/no-reschedule-failed-exec")
210210
static GIT_PATH_FUNC(rebase_path_drop_redundant_commits, "rebase-merge/drop_redundant_commits")
211211
static GIT_PATH_FUNC(rebase_path_keep_redundant_commits, "rebase-merge/keep_redundant_commits")
212+
static GIT_PATH_FUNC(rebase_path_trailer, "rebase-merge/trailer")
212213

213214
/*
214215
* A 'struct replay_ctx' represents the private state of the sequencer.
@@ -420,6 +421,7 @@ void replay_opts_release(struct replay_opts *opts)
420421
if (opts->revs)
421422
release_revisions(opts->revs);
422423
free(opts->revs);
424+
strvec_clear(&opts->trailer_args);
423425
replay_ctx_release(ctx);
424426
free(opts->ctx);
425427
}
@@ -2019,12 +2021,15 @@ static int append_squash_message(struct strbuf *buf, const char *body,
20192021
if (is_fixup_flag(command, flag) && !seen_squash(ctx)) {
20202022
/*
20212023
* We're replacing the commit message so we need to
2022-
* append the Signed-off-by: trailer if the user
2023-
* requested '--signoff'.
2024+
* append any trailers if the user requested
2025+
* '--signoff' or '--trailer'.
20242026
*/
20252027
if (opts->signoff)
20262028
append_signoff(buf, 0, 0);
20272029

2030+
if (opts->trailer_args.nr)
2031+
amend_strbuf_with_trailers(buf, &opts->trailer_args);
2032+
20282033
if ((command == TODO_FIXUP) &&
20292034
(flag & TODO_REPLACE_FIXUP_MSG) &&
20302035
(file_exists(rebase_path_fixup_msg()) ||
@@ -2443,6 +2448,9 @@ static int do_pick_commit(struct repository *r,
24432448
if (opts->signoff && !is_fixup(command))
24442449
append_signoff(&ctx->message, 0, 0);
24452450

2451+
if (opts->trailer_args.nr && !is_fixup(command))
2452+
amend_strbuf_with_trailers(&ctx->message, &opts->trailer_args);
2453+
24462454
if (is_rebase_i(opts) && write_author_script(msg.message) < 0)
24472455
res = -1;
24482456
else if (!opts->strategy ||
@@ -3172,6 +3180,33 @@ static void read_strategy_opts(struct replay_opts *opts, struct strbuf *buf)
31723180
parse_strategy_opts(opts, buf->buf);
31733181
}
31743182

3183+
static int read_trailers(struct replay_opts *opts, struct strbuf *buf)
3184+
{
3185+
ssize_t len;
3186+
3187+
strbuf_reset(buf);
3188+
len = strbuf_read_file(buf, rebase_path_trailer(), 0);
3189+
if (len > 0) {
3190+
char *p = buf->buf, *nl;
3191+
3192+
trailer_config_init();
3193+
3194+
while ((nl = strchr(p, '\n'))) {
3195+
*nl = '\0';
3196+
if (!*p)
3197+
return error(_("trailers file contains empty line"));
3198+
strvec_push(&opts->trailer_args, p);
3199+
p = nl + 1;
3200+
}
3201+
} else if (!len) {
3202+
return error(_("trailers file is empty"));
3203+
} else if (errno != ENOENT) {
3204+
return error(_("cannot read trailers files"));
3205+
}
3206+
3207+
return 0;
3208+
}
3209+
31753210
static int read_populate_opts(struct replay_opts *opts)
31763211
{
31773212
struct replay_ctx *ctx = opts->ctx;
@@ -3233,6 +3268,11 @@ static int read_populate_opts(struct replay_opts *opts)
32333268
opts->keep_redundant_commits = 1;
32343269

32353270
read_strategy_opts(opts, &buf);
3271+
3272+
if (read_trailers(opts, &buf)) {
3273+
ret = -1;
3274+
goto done_rebase_i;
3275+
}
32363276
strbuf_reset(&buf);
32373277

32383278
if (read_oneliner(&ctx->current_fixups,
@@ -3328,6 +3368,14 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,
33283368
write_file(rebase_path_reschedule_failed_exec(), "%s", "");
33293369
else
33303370
write_file(rebase_path_no_reschedule_failed_exec(), "%s", "");
3371+
if (opts->trailer_args.nr) {
3372+
struct strbuf buf = STRBUF_INIT;
3373+
3374+
for (size_t i = 0; i < opts->trailer_args.nr; i++)
3375+
strbuf_addf(&buf, "%s\n", opts->trailer_args.v[i]);
3376+
write_file(rebase_path_trailer(), "%s", buf.buf);
3377+
strbuf_release(&buf);
3378+
}
33313379

33323380
return 0;
33333381
}

sequencer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ struct replay_opts {
5757
int ignore_date;
5858
int commit_use_reference;
5959

60+
struct strvec trailer_args;
61+
6062
int mainline;
6163

6264
char *gpg_sign;
@@ -84,6 +86,7 @@ struct replay_opts {
8486
#define REPLAY_OPTS_INIT { \
8587
.edit = -1, \
8688
.action = -1, \
89+
.trailer_args = STRVEC_INIT, \
8790
.xopts = STRVEC_INIT, \
8891
.ctx = replay_ctx_new(), \
8992
}

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ integration_tests = [
388388
't3436-rebase-more-options.sh',
389389
't3437-rebase-fixup-options.sh',
390390
't3438-rebase-broken-files.sh',
391+
't3440-rebase-trailer.sh',
391392
't3450-history.sh',
392393
't3451-history-reword.sh',
393394
't3500-cherry.sh',

t/t3440-rebase-trailer.sh

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/bin/sh
2+
#
3+
4+
test_description='git rebase --trailer integration tests
5+
We verify that --trailer works with the merge backend,
6+
and that it is rejected early when the apply backend is requested.'
7+
8+
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
9+
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
10+
11+
. ./test-lib.sh
12+
. "$TEST_DIRECTORY"/lib-rebase.sh # test_commit_message, helpers
13+
14+
REVIEWED_BY_TRAILER="Reviewed-by: Dev <dev@example.com>"
15+
SP=" "
16+
17+
test_expect_success 'setup repo with a small history' '
18+
git commit --allow-empty -m "Initial empty commit" &&
19+
test_commit first file a &&
20+
test_commit second file &&
21+
git checkout -b conflict-branch first &&
22+
test_commit file-2 file-2 &&
23+
test_commit conflict file &&
24+
test_commit third file &&
25+
git checkout main
26+
'
27+
28+
test_expect_success 'apply backend is rejected with --trailer' '
29+
git checkout -B apply-backend third &&
30+
test_expect_code 128 \
31+
git rebase --apply --trailer "$REVIEWED_BY_TRAILER" HEAD^ 2>err &&
32+
test_grep "fatal: --trailer requires the merge backend" err
33+
'
34+
35+
test_expect_success 'reject empty --trailer argument' '
36+
git checkout -B empty-trailer third &&
37+
test_expect_code 128 git rebase --trailer "" HEAD^ 2>err &&
38+
test_grep "empty --trailer" err
39+
'
40+
41+
test_expect_success 'reject trailer with missing key before separator' '
42+
git checkout -B missing-key third &&
43+
test_expect_code 128 git rebase --trailer ": no-key" HEAD^ 2>err &&
44+
test_grep "missing key before separator" err
45+
'
46+
47+
test_expect_success 'allow trailer with missing value after separator' '
48+
git checkout -B missing-value third &&
49+
git rebase --trailer "Acked-by:" HEAD^ &&
50+
test_commit_message HEAD <<-EOF
51+
third
52+
53+
Acked-by:${SP}
54+
EOF
55+
'
56+
57+
test_expect_success 'CLI trailer duplicates allowed; replace policy keeps last' '
58+
git checkout -B replace-policy third &&
59+
git -c trailer.Bug.ifexists=replace -c trailer.Bug.ifmissing=add \
60+
rebase --trailer "Bug: 123" --trailer "Bug: 456" HEAD^ &&
61+
test_commit_message HEAD <<-EOF
62+
third
63+
64+
Bug: 456
65+
EOF
66+
'
67+
68+
test_expect_success 'multiple Signed-off-by trailers all preserved' '
69+
git checkout -B multiple-signoff third &&
70+
git rebase --trailer "Signed-off-by: Dev A <a@example.com>" \
71+
--trailer "Signed-off-by: Dev B <b@example.com>" HEAD^ &&
72+
test_commit_message HEAD <<-EOF
73+
third
74+
75+
Signed-off-by: Dev A <a@example.com>
76+
Signed-off-by: Dev B <b@example.com>
77+
EOF
78+
'
79+
80+
test_expect_success 'rebase --trailer adds trailer after conflicts' '
81+
git checkout -B trailer-conflict third &&
82+
test_commit fourth file &&
83+
test_must_fail git rebase --trailer "$REVIEWED_BY_TRAILER" second &&
84+
git checkout --theirs file &&
85+
git add file &&
86+
git rebase --continue &&
87+
test_commit_message HEAD <<-EOF &&
88+
fourth
89+
90+
$REVIEWED_BY_TRAILER
91+
EOF
92+
test_commit_message HEAD^ <<-EOF
93+
third
94+
95+
$REVIEWED_BY_TRAILER
96+
EOF
97+
'
98+
99+
test_expect_success '--trailer handles fixup commands in todo list' '
100+
git checkout -B fixup-trailer third &&
101+
test_commit fixup-base base &&
102+
test_commit fixup-second second &&
103+
cat >todo <<-\EOF &&
104+
pick fixup-base fixup-base
105+
fixup fixup-second fixup-second
106+
EOF
107+
(
108+
set_replace_editor todo &&
109+
git rebase -i --trailer "$REVIEWED_BY_TRAILER" HEAD~2
110+
) &&
111+
test_commit_message HEAD <<-EOF &&
112+
fixup-base
113+
114+
$REVIEWED_BY_TRAILER
115+
EOF
116+
git reset --hard fixup-second &&
117+
cat >todo <<-\EOF &&
118+
pick fixup-base fixup-base
119+
fixup -C fixup-second fixup-second
120+
EOF
121+
(
122+
set_replace_editor todo &&
123+
git rebase -i --trailer "$REVIEWED_BY_TRAILER" HEAD~2
124+
) &&
125+
test_commit_message HEAD <<-EOF
126+
fixup-second
127+
128+
$REVIEWED_BY_TRAILER
129+
EOF
130+
'
131+
132+
test_expect_success 'rebase --root honors trailer.<name>.key' '
133+
git checkout -B root-trailer first &&
134+
git -c trailer.review.key=Reviewed-by rebase --root \
135+
--trailer=review="Dev <dev@example.com>" &&
136+
test_commit_message HEAD <<-EOF &&
137+
first
138+
139+
Reviewed-by: Dev <dev@example.com>
140+
EOF
141+
test_commit_message HEAD^ <<-EOF
142+
Initial empty commit
143+
144+
Reviewed-by: Dev <dev@example.com>
145+
EOF
146+
'
147+
test_done

0 commit comments

Comments
 (0)