[RFC PATCH 2/2] rebase: support --trailer

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



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







[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux