Skip to content

Commit 036e2d4

Browse files
FirstLoveLifegitster
authored andcommitted
rebase: support --trailer
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 it if the user passes an option that requires the apply backend (git am) since it lacks message‑filter/trailer hook. otherwise we can just use the merge backend. Automatically set REBASE_FORCE when any trailer is supplied. And reject invalid input before user edits the interactive file. Signed-off-by: Li Chen <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 534a87d commit 036e2d4

File tree

8 files changed

+262
-4
lines changed

8 files changed

+262
-4
lines changed

Documentation/git-rebase.adoc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,9 +487,16 @@ See also INCOMPATIBLE OPTIONS below.
487487
Add a `Signed-off-by` trailer to all the rebased commits. Note
488488
that if `--interactive` is given then only commits marked to be
489489
picked, edited or reworded will have the trailer added.
490-
+
490+
491491
See also INCOMPATIBLE OPTIONS below.
492492

493+
--trailer=<trailer>::
494+
Append the given trailer line(s) to every rebased commit
495+
message, processed via linkgit:git-interpret-trailers[1].
496+
When this option is present *rebase automatically implies*
497+
`--force-rebase` so that fast‑forwarded commits are also
498+
rewritten.
499+
493500
-i::
494501
--interactive::
495502
Make a list of the commits which are about to be rebased. Let the

builtin/rebase.c

Lines changed: 50 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;
@@ -500,6 +508,23 @@ static int read_basic_state(struct rebase_options *opts)
500508
opts->gpg_sign_opt = xstrdup(buf.buf);
501509
}
502510

511+
strbuf_reset(&buf);
512+
513+
if (strbuf_read_file(&buf, state_dir_path("trailer", opts), 0) >= 0) {
514+
const char *p = buf.buf, *end = buf.buf + buf.len;
515+
516+
while (p < end) {
517+
char *nl = memchr(p, '\n', end - p);
518+
if (!nl)
519+
die("nl shouldn't be NULL");
520+
*nl = '\0';
521+
522+
if (*p)
523+
strvec_push(&opts->trailer_args, p);
524+
525+
p = nl + 1;
526+
}
527+
}
503528
strbuf_release(&buf);
504529

505530
return 0;
@@ -528,6 +553,21 @@ static int rebase_write_basic_state(struct rebase_options *opts)
528553
if (opts->signoff)
529554
write_file(state_dir_path("signoff", opts), "--signoff");
530555

556+
/*
557+
* save opts->trailer_args into state_dir/trailer
558+
*/
559+
if (opts->trailer_args.nr) {
560+
struct strbuf buf = STRBUF_INIT;
561+
562+
for (size_t i = 0; i < opts->trailer_args.nr; i++) {
563+
strbuf_addstr(&buf, opts->trailer_args.v[i]);
564+
strbuf_addch(&buf, '\n');
565+
}
566+
write_file(state_dir_path("trailer", opts),
567+
"%s", buf.buf);
568+
strbuf_release(&buf);
569+
}
570+
531571
return 0;
532572
}
533573

@@ -1132,6 +1172,8 @@ int cmd_rebase(int argc,
11321172
.flags = PARSE_OPT_NOARG,
11331173
.defval = REBASE_DIFFSTAT,
11341174
},
1175+
OPT_STRVEC(0, "trailer", &options.trailer_args, N_("trailer"),
1176+
N_("add custom trailer(s)")),
11351177
OPT_BOOL(0, "signoff", &options.signoff,
11361178
N_("add a Signed-off-by trailer to each commit")),
11371179
OPT_BOOL(0, "committer-date-is-author-date",
@@ -1285,6 +1327,11 @@ int cmd_rebase(int argc,
12851327
builtin_rebase_options,
12861328
builtin_rebase_usage, 0);
12871329

1330+
if (options.trailer_args.nr) {
1331+
validate_trailer_args_after_config(&options.trailer_args);
1332+
options.flags |= REBASE_FORCE;
1333+
}
1334+
12881335
if (preserve_merges_selected)
12891336
die(_("--preserve-merges was replaced by --rebase-merges\n"
12901337
"Note: Your `pull.rebase` configuration may also be set to 'preserve',\n"
@@ -1542,6 +1589,9 @@ int cmd_rebase(int argc,
15421589
if (options.root && !options.onto_name)
15431590
imply_merge(&options, "--root without --onto");
15441591

1592+
if (options.trailer_args.nr)
1593+
imply_merge(&options, "--trailer");
1594+
15451595
if (isatty(2) && options.flags & REBASE_NO_QUIET)
15461596
strbuf_addstr(&options.git_format_patch_opt, " --progress");
15471597

sequencer.c

Lines changed: 34 additions & 0 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
}
@@ -2025,6 +2027,10 @@ static int append_squash_message(struct strbuf *buf, const char *body,
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+
return error(_("unable to add trailers to commit message"));
2033+
20282034
if ((command == TODO_FIXUP) &&
20292035
(flag & TODO_REPLACE_FIXUP_MSG) &&
20302036
(file_exists(rebase_path_fixup_msg()) ||
@@ -2443,6 +2449,14 @@ static int do_pick_commit(struct repository *r,
24432449
if (opts->signoff && !is_fixup(command))
24442450
append_signoff(&ctx->message, 0, 0);
24452451

2452+
if (opts->trailer_args.nr && !is_fixup(command)) {
2453+
if (amend_strbuf_with_trailers(&ctx->message,
2454+
&opts->trailer_args)) {
2455+
res = error(_("unable to add trailers to commit message"));
2456+
goto leave;
2457+
}
2458+
}
2459+
24462460
if (is_rebase_i(opts) && write_author_script(msg.message) < 0)
24472461
res = -1;
24482462
else if (!opts->strategy ||
@@ -2517,6 +2531,7 @@ static int do_pick_commit(struct repository *r,
25172531
_("dropping %s %s -- patch contents already upstream\n"),
25182532
oid_to_hex(&commit->object.oid), msg.subject);
25192533
} /* else allow == 0 and there's nothing special to do */
2534+
25202535
if (!opts->no_commit && !drop_commit) {
25212536
if (author || command == TODO_REVERT || (flags & AMEND_MSG))
25222537
res = do_commit(r, msg_file, author, reflog_action,
@@ -3234,6 +3249,17 @@ static int read_populate_opts(struct replay_opts *opts)
32343249

32353250
read_strategy_opts(opts, &buf);
32363251
strbuf_reset(&buf);
3252+
if (strbuf_read_file(&buf, rebase_path_trailer(), 0) >= 0) {
3253+
char *p = buf.buf, *nl;
3254+
3255+
while ((nl = strchr(p, '\n'))) {
3256+
*nl = '\0';
3257+
if (*p)
3258+
strvec_push(&opts->trailer_args, p);
3259+
p = nl + 1;
3260+
}
3261+
strbuf_reset(&buf);
3262+
}
32373263

32383264
if (read_oneliner(&ctx->current_fixups,
32393265
rebase_path_current_fixups(),
@@ -3328,6 +3354,14 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,
33283354
write_file(rebase_path_reschedule_failed_exec(), "%s", "");
33293355
else
33303356
write_file(rebase_path_no_reschedule_failed_exec(), "%s", "");
3357+
if (opts->trailer_args.nr) {
3358+
struct strbuf buf = STRBUF_INIT;
3359+
3360+
for (size_t i = 0; i < opts->trailer_args.nr; i++)
3361+
strbuf_addf(&buf, "%s\n", opts->trailer_args.v[i]);
3362+
write_file(rebase_path_trailer(), "%s", buf.buf);
3363+
strbuf_release(&buf);
3364+
}
33313365

33323366
return 0;
33333367
}

sequencer.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ struct replay_opts {
4444
int record_origin;
4545
int no_commit;
4646
int signoff;
47+
struct strvec trailer_args;
4748
int allow_ff;
4849
int allow_rerere_auto;
4950
int allow_empty;
@@ -82,8 +83,9 @@ struct replay_opts {
8283
struct replay_ctx *ctx;
8384
};
8485
#define REPLAY_OPTS_INIT { \
85-
.edit = -1, \
8686
.action = -1, \
87+
.edit = -1, \
88+
.trailer_args = STRVEC_INIT, \
8789
.xopts = STRVEC_INIT, \
8890
.ctx = replay_ctx_new(), \
8991
}

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ integration_tests = [
385385
't3436-rebase-more-options.sh',
386386
't3437-rebase-fixup-options.sh',
387387
't3438-rebase-broken-files.sh',
388+
't3440-rebase-trailer.sh',
388389
't3500-cherry.sh',
389390
't3501-revert-cherry-pick.sh',
390391
't3502-cherry-pick-merge.sh',

t/t3440-rebase-trailer.sh

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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 <[email protected]>"
15+
16+
expect_trailer_msg() {
17+
test_commit_message "$1" <<-EOF
18+
$2
19+
20+
${3:-$REVIEWED_BY_TRAILER}
21+
EOF
22+
}
23+
24+
test_expect_success 'setup repo with a small history' '
25+
git commit --allow-empty -m "Initial empty commit" &&
26+
test_commit first file a &&
27+
test_commit second file &&
28+
git checkout -b conflict-branch first &&
29+
test_commit file-2 file-2 &&
30+
test_commit conflict file &&
31+
test_commit third file
32+
'
33+
34+
test_expect_success 'apply backend is rejected with --trailer' '
35+
head_before=$(git rev-parse HEAD) &&
36+
test_expect_code 128 \
37+
git rebase --apply --trailer "$REVIEWED_BY_TRAILER" \
38+
HEAD^ 2>err &&
39+
test_grep "fatal: --trailer requires the merge backend" err &&
40+
test_cmp_rev HEAD $head_before
41+
'
42+
43+
test_expect_success 'reject empty --trailer argument' '
44+
test_expect_code 128 git rebase -m --trailer "" HEAD^ 2>err &&
45+
test_grep "empty --trailer" err
46+
'
47+
48+
test_expect_success 'reject trailer with missing key before separator' '
49+
test_expect_code 128 git rebase -m --trailer ": no-key" HEAD^ 2>err &&
50+
test_grep "missing key before separator" err
51+
'
52+
53+
test_expect_success 'allow trailer with missing value after separator' '
54+
git rebase -m --trailer "Acked-by:" HEAD~1 third &&
55+
sed -e "s/_/ /g" <<-\EOF >expect &&
56+
third
57+
58+
Acked-by:_
59+
EOF
60+
test_commit_message HEAD expect
61+
'
62+
63+
test_expect_success 'CLI trailer duplicates allowed; replace policy keeps last' '
64+
git -c trailer.Bug.ifexists=replace -c trailer.Bug.ifmissing=add \
65+
rebase -m --trailer "Bug: 123" --trailer "Bug: 456" HEAD~1 third &&
66+
cat >expect <<-\EOF &&
67+
third
68+
69+
Bug: 456
70+
EOF
71+
test_commit_message HEAD expect
72+
'
73+
74+
test_expect_success 'multiple Signed-off-by trailers all preserved' '
75+
git rebase -m \
76+
--trailer "Signed-off-by: Dev A <[email protected]>" \
77+
--trailer "Signed-off-by: Dev B <[email protected]>" HEAD~1 third &&
78+
cat >expect <<-\EOF &&
79+
third
80+
81+
Signed-off-by: Dev A <[email protected]>
82+
Signed-off-by: Dev B <[email protected]>
83+
EOF
84+
test_commit_message HEAD expect
85+
'
86+
87+
test_expect_success 'rebase -m --trailer adds trailer after conflicts' '
88+
git checkout -B conflict-branch third &&
89+
test_commit fourth file &&
90+
test_must_fail git rebase -m \
91+
--trailer "$REVIEWED_BY_TRAILER" \
92+
second &&
93+
git checkout --theirs file &&
94+
git add file &&
95+
git rebase --continue &&
96+
expect_trailer_msg HEAD "fourth" &&
97+
expect_trailer_msg HEAD^ "third"
98+
'
99+
100+
test_expect_success '--trailer handles fixup commands in todo list' '
101+
git checkout -B fixup-trailer HEAD &&
102+
test_commit fixup-base base &&
103+
test_commit fixup-second second &&
104+
first_short=$(git rev-parse --short fixup-base) &&
105+
second_short=$(git rev-parse --short fixup-second) &&
106+
cat >todo <<EOF &&
107+
pick $first_short fixup-base
108+
fixup $second_short fixup-second
109+
EOF
110+
(
111+
set_replace_editor todo &&
112+
git rebase -i --trailer "$REVIEWED_BY_TRAILER" HEAD~2
113+
) &&
114+
expect_trailer_msg HEAD "fixup-base" &&
115+
git reset --hard fixup-second &&
116+
cat >todo <<EOF &&
117+
pick $first_short fixup-base
118+
fixup -C $second_short fixup-second
119+
EOF
120+
(
121+
set_replace_editor todo &&
122+
git rebase -i --trailer "$REVIEWED_BY_TRAILER" HEAD~2
123+
) &&
124+
expect_trailer_msg HEAD "fixup-second"
125+
'
126+
127+
test_expect_success 'rebase --root --trailer updates every commit' '
128+
git checkout first &&
129+
git -c trailer.review.key=Reviewed-by rebase --root \
130+
--trailer=review="Dev <[email protected]>" &&
131+
expect_trailer_msg HEAD "first" &&
132+
expect_trailer_msg HEAD^ "Initial empty commit"
133+
'
134+
test_done

0 commit comments

Comments
 (0)