From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Phillip Wood <phillip.wood123@gmail.com>,
"D. Ben Knoble" <ben.knoble@gmail.com>,
Patrick Steinhardt <ps@pks.im>, Matt Hunter <m@lfurio.us>,
Harald Nordgren <haraldnordgren@gmail.com>,
Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v9 5/5] history: re-edit a squash with every message
Date: Wed, 15 Jul 2026 15:16:13 +0000 [thread overview]
Message-ID: <fb76afe31c98833582d2b6be764fa3a1c8b71bbe.1784128573.git.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2337.v9.git.git.1784128573.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
By default "git history squash" reuses the oldest commit's message, or
the replacement body from an amend! commit targeting it. When
--reedit-message is given it only reopened that selected message, so the
messages of the other commits in the range were lost.
Gather the message of every commit in the range and build the same editor
template that "git rebase -i --autosquash" shows for a squash, reusing
add_squash_combination_header(), add_squash_message_header() and
squash_subject_comment_len(). Feed the range through
todo_list_rearrange_squash() so that each fixup!, squash! or amend! is
grouped under the commit it targets rather than shown in commit order,
exactly as autosquash would arrange them.
Only the message text differs, the changes are always folded in. A fixup!
message is commented out in full under a "will be skipped" header, a
squash! keeps its body with only the marker subject commented, and an
amend! replaces its target's message unless a squash! already folded into
that target, in which case it behaves like a squash!.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
Documentation/git-history.adoc | 20 +++-
builtin/history.c | 104 +++++++++++++++++
t/t3455-history-squash.sh | 201 +++++++++++++++++++++++++++++++++
3 files changed, 320 insertions(+), 5 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 2c0d861303..dc5580531f 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -118,11 +118,12 @@ already on `topic`. Rev-list options may also be given, but any that would
change how the range is walked are overridden with a warning.
+
The oldest commit's message is preserved by default, except that an `amend!`
-commit targeting it replaces its message. Specify `--reedit-message` to edit
-the resulting message. A merge commit inside the range is folded like any
-other, but the range must have a single base, so a range that reaches more
-than one entry point (for example a side branch that forked before the range
-and was later merged into it) is rejected.
+commit targeting it replaces its message. With `--reedit-message`, an editor
+opens pre-filled with the messages of all the folded commits so you can
+combine them. A merge commit inside the range is folded like any other, but
+the range must have a single base, so a range that reaches more than one entry
+point (for example a side branch that forked before the range and was later
+merged into it) is rejected.
+
A `fixup!`, `squash!`, or `amend!` commit is refused unless the commit it
targets is also in the range, so the fold does not silently absorb a
@@ -130,6 +131,15 @@ marker meant for a commit outside it. The body after an `amend!` subject
replaces the oldest commit's message when the marker targets that commit. As
an exception, a range made up entirely of markers for one target is combined
into a single commit, keeping the last `amend!` message if there is one.
+The changes from every commit in the range are always folded in. Only the
+message text differs.
+With `--reedit-message` the template mirrors `git rebase -i --autosquash`:
+each `fixup!`, `squash!`, or `amend!` is grouped under the commit it
+targets rather than shown in commit order. A `fixup!` message is dropped
+(commented out in full), a `squash!` keeps its body with only the marker
+subject commented, and an `amend!` replaces its target's message, unless
+a `squash!` folded into that target first, in which case it keeps its
+body like a `squash!`.
+
A branch or tag that points at a commit inside the range would be left
dangling once those commits are folded away, so with the default
diff --git a/builtin/history.c b/builtin/history.c
index edf98a21d3..b1f84e8297 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1223,6 +1223,102 @@ static int find_interior_ref(const struct reference *ref, void *cb_data)
return 0;
}
+static bool amend_replaces_target(struct todo_list *todo, int target)
+{
+ int i;
+
+ for (i = target + 1; i < todo->nr &&
+ todo->items[i].command != TODO_PICK; i++) {
+ if (todo->items[i].command == TODO_SQUASH)
+ return false;
+ if (todo->items[i].flags & TODO_REPLACE_FIXUP_MSG)
+ return true;
+ }
+ return false;
+}
+
+static int build_squash_message(struct repository *repo,
+ struct commit *base,
+ struct commit *tip,
+ struct strbuf *out)
+{
+ struct rev_info revs;
+ struct commit *commit;
+ struct strvec args = STRVEC_INIT;
+ struct todo_list todo = TODO_LIST_INIT;
+ struct replay_opts opts = REPLAY_OPTS_INIT;
+ int i, nr_commits, ret;
+
+ repo_init_revisions(repo, &revs, NULL);
+ strvec_push(&args, "ignored");
+ strvec_push(&args, "--reverse");
+ strvec_push(&args, "--topo-order");
+ strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid),
+ oid_to_hex(&tip->object.oid));
+ setup_revisions_from_strvec(&args, &revs, NULL);
+
+ if (prepare_revision_walk(&revs) < 0) {
+ ret = error(_("error preparing revisions"));
+ goto out;
+ }
+
+ while ((commit = get_revision(&revs)))
+ strbuf_addf(&todo.buf, "pick %s\n",
+ oid_to_hex(&commit->object.oid));
+
+ if (todo_list_parse_insn_buffer(repo, &opts, todo.buf.buf, &todo) < 0 ||
+ todo_list_rearrange_squash(&todo) < 0) {
+ ret = error(_("could not prepare the squash message"));
+ goto out;
+ }
+
+ nr_commits = todo.nr;
+ for (i = 0; i < nr_commits; i++) {
+ struct todo_item *item = &todo.items[i];
+ const char *message, *body;
+ size_t commented_len;
+ bool skip, squashing;
+
+ squashing = item->command == TODO_SQUASH ||
+ (item->flags & TODO_REPLACE_FIXUP_MSG);
+ if (item->command == TODO_PICK)
+ skip = amend_replaces_target(&todo, i);
+ else
+ skip = !squashing;
+
+ message = repo_logmsg_reencode(repo, item->commit, NULL, NULL);
+ find_commit_subject(message, &body);
+
+ if (skip)
+ commented_len = strlen(body);
+ else if (squashing)
+ commented_len = squash_subject_comment_len(body, 1);
+ else
+ commented_len = 0;
+
+ if (!i)
+ add_squash_combination_header(out, nr_commits);
+ strbuf_addch(out, '\n');
+ add_squash_message_header(out, i + 1, skip);
+ strbuf_addstr(out, "\n\n");
+ strbuf_add_commented_lines(out, body, commented_len, comment_line_str);
+ strbuf_addstr(out, body + commented_len);
+ strbuf_complete_line(out);
+
+ repo_unuse_commit_buffer(repo, item->commit, message);
+ }
+
+ ret = 0;
+
+out:
+ todo_list_release(&todo);
+ replay_opts_release(&opts);
+ reset_revision_walk();
+ release_revisions(&revs);
+ strvec_clear(&args);
+ return ret;
+}
+
static int cmd_history_squash(int argc,
const char **argv,
const char *prefix,
@@ -1306,6 +1402,14 @@ static int cmd_history_squash(int argc,
}
}
+ if (flags & COMMIT_TREE_EDIT_MESSAGE) {
+ strbuf_reset(&message);
+ ret = build_squash_message(repo, base, tip, &message);
+ if (ret < 0)
+ goto out;
+ message_template = message.buf;
+ }
+
ret = setup_revwalk(repo, action, tip, &revs);
if (ret < 0)
goto out;
diff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh
index d6199b9644..f2835f5379 100755
--- a/t/t3455-history-squash.sh
+++ b/t/t3455-history-squash.sh
@@ -267,6 +267,207 @@ test_expect_success 'preserves authorship of the oldest commit' '
test_cmp expect actual
'
+test_expect_success '--reedit-message offers every folded-in message' '
+ git reset --hard start &&
+ stage_file b &&
+ git commit -m "re-one subject" -m "re-one body line" &&
+ test_commit --no-tag re-two file c &&
+ test_commit re-three file d &&
+
+ write_script editor <<-\EOF &&
+ cat "$1" >edited &&
+ echo combined >"$1"
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ git history squash --reedit-message start.. &&
+
+ cat >expect <<-EOF &&
+ # This is a combination of 3 commits.
+ # This is the 1st commit message:
+
+ re-one subject
+
+ re-one body line
+
+ # This is the commit message #2:
+
+ re-two
+
+ # This is the commit message #3:
+
+ re-three
+
+ # Please enter the commit message for the squash changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # modified: file
+ #
+ EOF
+ test_cmp expect edited &&
+ check_log_subjects -1 <<-\EOF
+ combined
+ EOF
+'
+
+test_expect_success '--reedit-message handles fixup!, squash! and amend! like rebase' '
+ git reset --hard start &&
+ test_commit --no-tag mark-base file b &&
+ stage_file c &&
+ commit_with_message "fixup! mark-base\n\nfixup body\n" &&
+ stage_file d &&
+ commit_with_message "squash! mark-base\n\nsquash remark\n" &&
+ stage_file e &&
+ commit_with_message "amend! mark-base\n\namended message\n" &&
+
+ write_script editor <<-\EOF &&
+ cat "$1" >edited
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ git history squash --reedit-message start.. &&
+
+ cat >expect <<-EOF &&
+ # This is a combination of 4 commits.
+ # This is the 1st commit message:
+
+ mark-base
+
+ # The commit message #2 will be skipped:
+
+ # fixup! mark-base
+ #
+ # fixup body
+
+ # This is the commit message #3:
+
+ # squash! mark-base
+
+ squash remark
+
+ # This is the commit message #4:
+
+ # amend! mark-base
+
+ amended message
+
+ # Please enter the commit message for the squash changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # modified: file
+ #
+ EOF
+ test_cmp expect edited &&
+ check_log_messages -1 <<-\EOF
+ mark-base
+
+ squash remark
+
+ amended message
+
+ EOF
+'
+
+test_expect_success '--reedit-message groups fixups under their targets' '
+ git reset --hard start &&
+ test_commit --no-tag alpha file a1 &&
+ test_commit --no-tag beta file b1 &&
+ stage_file a2 &&
+ commit_with_message "fixup! alpha\n" &&
+ stage_file b2 &&
+ commit_with_message "fixup! beta\n" &&
+
+ write_script editor <<-\EOF &&
+ cat "$1" >edited
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ git history squash --reedit-message start.. &&
+
+ cat >expect <<-EOF &&
+ # This is a combination of 4 commits.
+ # This is the 1st commit message:
+
+ alpha
+
+ # The commit message #2 will be skipped:
+
+ # fixup! alpha
+
+ # This is the commit message #3:
+
+ beta
+
+ # The commit message #4 will be skipped:
+
+ # fixup! beta
+
+ # Please enter the commit message for the squash changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # modified: file
+ #
+ EOF
+ test_cmp expect edited
+'
+
+test_expect_success '--reedit-message lets amend! replace its target message' '
+ git reset --hard start &&
+ test_commit --no-tag mark-base file b &&
+ stage_file c &&
+ commit_with_message "amend! mark-base\n\namended message\n" &&
+ stage_file d &&
+ commit_with_message "squash! mark-base\n\nsquash remark\n" &&
+
+ write_script editor <<-\EOF &&
+ cat "$1" >edited
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ git history squash --reedit-message start.. &&
+
+ cat >expect <<-EOF &&
+ # This is a combination of 3 commits.
+ # The 1st commit message will be skipped:
+
+ # mark-base
+
+ # This is the commit message #2:
+
+ # amend! mark-base
+
+ amended message
+
+ # This is the commit message #3:
+
+ # squash! mark-base
+
+ squash remark
+
+ # Please enter the commit message for the squash changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # modified: file
+ #
+ EOF
+ test_cmp expect edited &&
+ check_log_messages -1 <<-\EOF
+ amended message
+
+ squash remark
+
+ EOF
+'
+
+test_expect_success '--reedit-message aborts on an empty message' '
+ git reset --hard three &&
+ head_before=$(git rev-parse HEAD) &&
+
+ write_script editor <<-\EOF &&
+ >"$1"
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ test_must_fail git history squash --reedit-message start.. &&
+
+ test_cmp_rev "$head_before" HEAD
+'
+
test_expect_success '--update-refs=head only moves HEAD' '
git reset --hard three &&
git branch -f other HEAD &&
--
gitgitgadget
prev parent reply other threads:[~2026-07-15 15:16 UTC|newest]
Thread overview: 104+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-14 19:25 [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit Harald Nordgren via GitGitGadget
2026-06-14 19:25 ` [PATCH 1/2] t3415: remove prepare-commit-msg hook after use Harald Nordgren via GitGitGadget
2026-06-14 19:25 ` [PATCH 2/2] rebase: add --fixup-all to fold a range Harald Nordgren via GitGitGadget
2026-06-15 2:01 ` [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit Junio C Hamano
2026-06-15 8:18 ` Harald Nordgren
2026-06-15 15:17 ` D. Ben Knoble
2026-06-16 8:34 ` Patrick Steinhardt
2026-06-17 9:30 ` Harald Nordgren
2026-06-15 8:37 ` [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit Harald Nordgren via GitGitGadget
2026-06-15 8:37 ` [PATCH v2 1/2] t3415: remove prepare-commit-msg hook after use Harald Nordgren via GitGitGadget
2026-06-15 8:37 ` [PATCH v2 2/2] rebase: add --squash to fold a range Harald Nordgren via GitGitGadget
2026-06-16 10:10 ` [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit Phillip Wood
2026-06-17 9:11 ` Harald Nordgren
2026-06-17 9:48 ` Phillip Wood
2026-06-18 19:17 ` [PATCH v3 0/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-18 19:17 ` [PATCH v3 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-06-18 19:17 ` [PATCH v3 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-06-18 19:17 ` [PATCH v3 3/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-18 20:30 ` Junio C Hamano
2026-06-18 21:24 ` Junio C Hamano
2026-06-18 21:29 ` D. Ben Knoble
2026-06-19 12:55 ` Patrick Steinhardt
2026-06-18 19:17 ` [PATCH v3 4/4] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-06-18 21:23 ` [PATCH v3 0/4] history: add squash subcommand to fold a range D. Ben Knoble
2026-06-19 0:34 ` Junio C Hamano
2026-06-19 12:37 ` Patrick Steinhardt
2026-06-19 16:11 ` Junio C Hamano
2026-06-21 5:53 ` [PATCH v4 " Harald Nordgren via GitGitGadget
2026-06-21 5:53 ` [PATCH v4 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-06-21 5:53 ` [PATCH v4 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-06-21 5:53 ` [PATCH v4 3/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-21 5:53 ` [PATCH v4 4/4] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-06-22 11:54 ` [PATCH v4 0/4] history: add squash subcommand to fold a range Patrick Steinhardt
2026-06-23 10:41 ` Harald Nordgren
2026-06-24 21:54 ` [PATCH v5 " Harald Nordgren via GitGitGadget
2026-06-24 21:54 ` [PATCH v5 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-06-24 21:55 ` [PATCH v5 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-06-24 21:55 ` [PATCH v5 3/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-24 21:55 ` [PATCH v5 4/4] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-06-26 8:52 ` [PATCH v5 0/4] history: add squash subcommand to fold a range Phillip Wood
2026-06-26 9:57 ` Harald Nordgren
2026-06-26 13:12 ` Phillip Wood
2026-06-26 14:02 ` Junio C Hamano
2026-06-26 18:36 ` Harald Nordgren
2026-06-29 6:26 ` Patrick Steinhardt
2026-06-29 15:51 ` Phillip Wood
2026-06-29 16:54 ` Junio C Hamano
2026-07-01 13:45 ` Phillip Wood
2026-06-29 18:03 ` Harald Nordgren
2026-06-29 19:48 ` Phillip Wood
2026-06-29 21:13 ` Harald Nordgren
2026-06-30 13:48 ` Phillip Wood
2026-06-30 18:38 ` Harald Nordgren
2026-07-01 10:31 ` Phillip Wood
2026-07-01 13:47 ` Junio C Hamano
2026-07-01 15:14 ` Phillip Wood
2026-07-01 17:41 ` Junio C Hamano
2026-07-02 13:58 ` Phillip Wood
2026-06-30 2:55 ` Matt Hunter
2026-06-30 7:19 ` Harald Nordgren
2026-06-30 9:23 ` Matt Hunter
2026-06-30 14:01 ` Phillip Wood
2026-07-02 12:54 ` Patrick Steinhardt
2026-07-02 20:28 ` Junio C Hamano
2026-06-29 16:09 ` Harald Nordgren
2026-06-28 8:29 ` [PATCH v6 " Harald Nordgren via GitGitGadget
2026-06-28 8:29 ` [PATCH v6 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-06-28 8:29 ` [PATCH v6 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-06-28 8:29 ` [PATCH v6 3/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-29 5:50 ` Junio C Hamano
2026-06-28 8:29 ` [PATCH v6 4/4] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-06-29 5:50 ` Junio C Hamano
2026-06-29 13:49 ` Harald Nordgren
2026-06-29 14:49 ` Junio C Hamano
2026-06-29 17:38 ` Junio C Hamano
2026-07-06 8:50 ` [PATCH v7 0/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 1/5] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 2/5] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 3/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 4/5] sequencer: extract helpers for the squash message markers Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 5/5] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-07-06 14:06 ` [PATCH v7 0/5] history: add squash subcommand to fold a range Phillip Wood
2026-07-07 7:51 ` Harald Nordgren
2026-07-07 8:55 ` Harald Nordgren
2026-07-07 9:30 ` Phillip Wood
2026-07-07 9:48 ` Phillip Wood
2026-07-06 20:42 ` Junio C Hamano
2026-07-10 9:06 ` [PATCH v8 " Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 1/5] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 2/5] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 3/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 4/5] sequencer: share the squash message marker helpers and flags Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 5/5] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-07-14 4:44 ` [PATCH v8 0/5] history: add squash subcommand to fold a range Matt Hunter
2026-07-14 8:38 ` Harald Nordgren
2026-07-14 9:04 ` Harald Nordgren
2026-07-14 12:36 ` Ben Knoble
2026-07-14 18:41 ` Junio C Hamano
2026-07-15 15:16 ` [PATCH v9 " Harald Nordgren via GitGitGadget
2026-07-15 15:16 ` [PATCH v9 1/5] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-07-15 15:16 ` [PATCH v9 2/5] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-07-15 15:16 ` [PATCH v9 3/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-07-15 15:16 ` [PATCH v9 4/5] sequencer: share the squash message marker helpers and flags Harald Nordgren via GitGitGadget
2026-07-15 15:16 ` Harald Nordgren via GitGitGadget [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=fb76afe31c98833582d2b6be764fa3a1c8b71bbe.1784128573.git.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=ben.knoble@gmail.com \
--cc=git@vger.kernel.org \
--cc=haraldnordgren@gmail.com \
--cc=m@lfurio.us \
--cc=phillip.wood123@gmail.com \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.