sequencer: share the squash message marker helpers and flags

When "git rebase -i" squashes commits it builds an editor template with a "This is a combination of N commits." banner, a "This is the 1st/Nth commit message:" header above each kept message (or a "will be skipped" header for a dropped one), and a commented-out subject for any fixup!, squash! or amend! commit. The banner, the headers and the subject-commenting all live in static helpers in sequencer.c wired to the rebase state, so no other command can present a squash the same way. Pull the three pieces out into add_squash_combination_header(), add_squash_message_header() (which takes a flag for the "will be skipped" variant) and squash_subject_comment_len(), and use them from update_squash_messages() and append_squash_message(). Also move the todo_item_flags enum to the header, so a caller reading the output of todo_list_rearrange_squash() can tell an amend! (TODO_REPLACE_FIXUP_MSG) from a plain fixup!. A later change reuses all of this to give "git history squash" the same template. No change in behavior. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Harald Nordgren committed Aug 1, 2026 at 06:53 UTC 7db34d08670570cb5d4924651d6bd888d4f0eec7
2 files changed +69 -31
sequencer.c
+39 -31
@@ -1880,18 +1880,38 @@ static int is_pick_or_similar(enum todo_command command)
1880 }
1881 }
1882
1883 -enum todo_item_flags {
1884 - TODO_EDIT_MERGE_MSG = (1 << 0),
1885 - TODO_REPLACE_FIXUP_MSG = (1 << 1),
1886 - TODO_EDIT_FIXUP_MSG = (1 << 2),
1887 -};
1888 -
1883 static const char first_commit_msg_str[] = N_("This is the 1st commit message:");
1884 static const char nth_commit_msg_fmt[] = N_("This is the commit message #%d:");
1885 static const char skip_first_commit_msg_str[] = N_("The 1st commit message will be skipped:");
1886 static const char skip_nth_commit_msg_fmt[] = N_("The commit message #%d will be skipped:");
1887 static const char combined_commit_msg_fmt[] = N_("This is a combination of %d commits.");
1888
1889 +void add_squash_combination_header(struct strbuf *buf, int n)
1890 +{
1891 + strbuf_addf(buf, "%s ", comment_line_str);
1892 + strbuf_addf(buf, _(combined_commit_msg_fmt), n);
1893 +}
1894 +
1895 +void add_squash_message_header(struct strbuf *buf, int n, int skip)
1896 +{
1897 + strbuf_addf(buf, "%s ", comment_line_str);
1898 + if (n == 1)
1899 + strbuf_addstr(buf, skip ? _(skip_first_commit_msg_str) :
1900 + _(first_commit_msg_str));
1901 + else
1902 + strbuf_addf(buf, skip ? _(skip_nth_commit_msg_fmt) :
1903 + _(nth_commit_msg_fmt), n);
1904 +}
1905 +
1906 +size_t squash_subject_comment_len(const char *body, int squashing)
1907 +{
1908 + if (starts_with(body, "amend!") ||
1909 + (squashing && (starts_with(body, "squash!") ||
1910 + starts_with(body, "fixup!"))))
1911 + return commit_subject_length(body);
1912 + return 0;
1913 +}
1914 +
1915 static int is_fixup_flag(enum todo_command command, unsigned flag)
1916 {
1917 return command == TODO_FIXUP && ((flag & TODO_REPLACE_FIXUP_MSG) ||
@@ -2005,20 +2025,13 @@ static int append_squash_message(struct strbuf *buf, const char *body,
2025 {
2026 struct replay_ctx *ctx = opts->ctx;
2027 const char *fixup_msg;
2008 - size_t commented_len = 0, fixup_off;
2009 - /*
2010 - * amend is non-interactive and not normally used with fixup!
2011 - * or squash! commits, so only comment out those subjects when
2012 - * squashing commit messages.
2013 - */
2014 - if (starts_with(body, "amend!") ||
2015 - ((command == TODO_SQUASH || seen_squash(ctx)) &&
2016 - (starts_with(body, "squash!") || starts_with(body, "fixup!"))))
2017 - commented_len = commit_subject_length(body);
2028 + size_t commented_len, fixup_off;
2029 +
2030 + commented_len = squash_subject_comment_len(body,
2031 + command == TODO_SQUASH || seen_squash(ctx));
2032
2019 - strbuf_addf(buf, "\n%s ", comment_line_str);
2020 - strbuf_addf(buf, _(nth_commit_msg_fmt),
2021 - ++ctx->current_fixup_count + 1);
2033 + strbuf_addch(buf, '\n');
2034 + add_squash_message_header(buf, ++ctx->current_fixup_count + 1, 0);
2035 strbuf_addstr(buf, "\n\n");
2036 strbuf_add_commented_lines(buf, body, commented_len, comment_line_str);
2037 /* buf->buf may be reallocated so store an offset into the buffer */
@@ -2083,9 +2096,8 @@ static int update_squash_messages(struct repository *r,
2096 eol = !starts_with(buf.buf, comment_line_str) ?
2097 buf.buf : strchrnul(buf.buf, '\n');
2098
2086 - strbuf_addf(&header, "%s ", comment_line_str);
2087 - strbuf_addf(&header, _(combined_commit_msg_fmt),
2088 - ctx->current_fixup_count + 2);
2099 + add_squash_combination_header(&header,
2100 + ctx->current_fixup_count + 2);
2101 strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);
2102 strbuf_release(&header);
2103 if (is_fixup_flag(command, flag) && !seen_squash(ctx))
@@ -2109,12 +2121,9 @@ static int update_squash_messages(struct repository *r,
2121 repo_unuse_commit_buffer(r, head_commit, head_message);
2122 return error(_("cannot write '%s'"), rebase_path_fixup_msg());
2123 }
2112 - strbuf_addf(&buf, "%s ", comment_line_str);
2113 - strbuf_addf(&buf, _(combined_commit_msg_fmt), 2);
2114 - strbuf_addf(&buf, "\n%s ", comment_line_str);
2115 - strbuf_addstr(&buf, is_fixup_flag(command, flag) ?
2116 - _(skip_first_commit_msg_str) :
2117 - _(first_commit_msg_str));
2124 + add_squash_combination_header(&buf, 2);
2125 + strbuf_addch(&buf, '\n');
2126 + add_squash_message_header(&buf, 1, is_fixup_flag(command, flag));
2127 strbuf_addstr(&buf, "\n\n");
2128 if (is_fixup_flag(command, flag))
2129 strbuf_add_commented_lines(&buf, body, strlen(body),
@@ -2133,9 +2142,8 @@ static int update_squash_messages(struct repository *r,
2142 if (command == TODO_SQUASH || is_fixup_flag(command, flag)) {
2143 res = append_squash_message(&buf, body, command, opts, flag);
2144 } else if (command == TODO_FIXUP) {
2136 - strbuf_addf(&buf, "\n%s ", comment_line_str);
2137 - strbuf_addf(&buf, _(skip_nth_commit_msg_fmt),
2138 - ++ctx->current_fixup_count + 1);
2145 + strbuf_addch(&buf, '\n');
2146 + add_squash_message_header(&buf, ++ctx->current_fixup_count + 1, 1);
2147 strbuf_addstr(&buf, "\n\n");
2148 strbuf_add_commented_lines(&buf, body, strlen(body),
2149 comment_line_str);
sequencer.h
+30
@@ -119,6 +119,13 @@ enum todo_command {
119 TODO_COMMENT
120 };
121
122 +/* Bits for the "flags" member of struct todo_item */
123 +enum todo_item_flags {
124 + TODO_EDIT_MERGE_MSG = (1 << 0),
125 + TODO_REPLACE_FIXUP_MSG = (1 << 1),
126 + TODO_EDIT_FIXUP_MSG = (1 << 2),
127 +};
128 +
129 struct todo_item {
130 enum todo_command command;
131 struct commit *commit;
@@ -208,6 +215,29 @@ int todo_list_rearrange_squash(struct todo_list *todo_list);
215 */
216 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag);
217
218 +/*
219 + * Append the "This is a combination of N commits." banner that "git rebase
220 + * -i" writes at the top of a squashed commit's message, commented out with
221 + * the comment character.
222 + */
223 +void add_squash_combination_header(struct strbuf *buf, int n);
224 +
225 +/*
226 + * Append the header (1-based N) that "git rebase -i" writes above each message
227 + * when squashing, commented out with the comment character. With SKIP it reads
228 + * "The ... commit message will be skipped" for a message that is dropped (a
229 + * fixup), otherwise "This is the ... commit message".
230 + */
231 +void add_squash_message_header(struct strbuf *buf, int n, int skip);
232 +
233 +/*
234 + * Return the length of the leading subject of BODY when it should be commented
235 + * out in a squash message, or 0 otherwise. An "amend!" subject always
236 + * qualifies; "squash!" and "fixup!" subjects only when SQUASHING, since a
237 + * plain fixup chain keeps them.
238 + */
239 +size_t squash_subject_comment_len(const char *body, int squashing);
240 +
241 void append_conflicts_hint(struct index_state *istate,
242 struct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode);
243 enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg,