builtin/history: implement "reword" subcommand

Implement a new "reword" subcommand for git-history(1). This subcommand is similar to the user performing an interactive rebase with a single commit changed to use the "reword" instruction. The "reword" subcommand is built on top of the replay subsystem instead of the sequencer. This leads to some major differences compared to git-rebase(1): - We do not check out the commit that is to be reworded and instead perform the operation in-memory. This has the obvious benefit of being significantly faster compared to git-rebase(1), but even more importantly it allows the user to rewrite history even if there are local changes in the working tree or in the index. - We do not execute any hooks, even though we leave some room for changing this in the future. - By default, all local branches that contain the commit will be rewritten. This especially helps with workflows that use stacked branches. Helped-by: Elijah Newren <newren@gmail.com> Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jan 13, 2026 at 10:54 UTC d205234cb05a5e330c0f7f5b3ea764533a74d69e
5 files changed +823 -10
Documentation/git-history.adoc
+18 -2
@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
8 SYNOPSIS
9 --------
10 [synopsis]
11 -git history [<options>]
11 +git history reword <commit> [--ref-action=(branches|head|print)]
12
13 DESCRIPTION
14 -----------
@@ -50,7 +50,23 @@ first-class conflicts.
50 COMMANDS
51 --------
52
53 -No commands are supported yet.
53 +The following commands are available to rewrite history in different ways:
54 +
55 +`reword <commit>`::
56 + Rewrite the commit message of the specified commit. All the other
57 + details of this commit remain unchanged. This command will spawn an
58 + editor with the current message of that commit.
59 +
60 +OPTIONS
61 +-------
62 +
63 +`--ref-action=(branches|head|print)`::
64 + Control which references will be updated by the command, if any. With
65 + `branches`, all local branches that point to commits which are
66 + descendants of the original commit will be rewritten. With `head`, only
67 + the current `HEAD` reference will be rewritten. With `print`, all
68 + updates as they would be performed with `branches` are printed in a
69 + format that can be consumed by linkgit:git-update-ref[1].
70
71 GIT
72 ---
builtin/history.c
+410 -5
@@ -1,22 +1,427 @@
1 +#define USE_THE_REPOSITORY_VARIABLE
2 +
3 #include "builtin.h"
4 +#include "commit.h"
5 +#include "commit-reach.h"
6 +#include "config.h"
7 +#include "editor.h"
8 +#include "environment.h"
9 #include "gettext.h"
10 +#include "hex.h"
11 #include "parse-options.h"
12 +#include "refs.h"
13 +#include "replay.h"
14 +#include "revision.h"
15 +#include "sequencer.h"
16 +#include "strvec.h"
17 +#include "tree.h"
18 +#include "wt-status.h"
19 +
20 +#define GIT_HISTORY_REWORD_USAGE \
21 + N_("git history reword <commit> [--ref-action=(branches|head|print)]")
22 +
23 +static void change_data_free(void *util, const char *str UNUSED)
24 +{
25 + struct wt_status_change_data *d = util;
26 + free(d->rename_source);
27 + free(d);
28 +}
29 +
30 +static int fill_commit_message(struct repository *repo,
31 + const struct object_id *old_tree,
32 + const struct object_id *new_tree,
33 + const char *default_message,
34 + const char *action,
35 + struct strbuf *out)
36 +{
37 + const char *path = git_path_commit_editmsg();
38 + const char *hint =
39 + _("Please enter the commit message for the %s changes."
40 + " Lines starting\nwith '%s' will be ignored, and an"
41 + " empty message aborts the commit.\n");
42 + struct wt_status s;
43 +
44 + strbuf_addstr(out, default_message);
45 + strbuf_addch(out, '\n');
46 + strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
47 + write_file_buf(path, out->buf, out->len);
48 +
49 + wt_status_prepare(repo, &s);
50 + FREE_AND_NULL(s.branch);
51 + s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
52 + s.commit_template = 1;
53 + s.colopts = 0;
54 + s.display_comment_prefix = 1;
55 + s.hints = 0;
56 + s.use_color = 0;
57 + s.whence = FROM_COMMIT;
58 + s.committable = 1;
59 +
60 + s.fp = fopen(git_path_commit_editmsg(), "a");
61 + if (!s.fp)
62 + return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
63 +
64 + wt_status_collect_changes_trees(&s, old_tree, new_tree);
65 + wt_status_print(&s);
66 + wt_status_collect_free_buffers(&s);
67 + string_list_clear_func(&s.change, change_data_free);
68 +
69 + strbuf_reset(out);
70 + if (launch_editor(path, out, NULL)) {
71 + fprintf(stderr, _("Aborting commit as launching the editor failed.\n"));
72 + return -1;
73 + }
74 + strbuf_stripspace(out, comment_line_str);
75 +
76 + cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
77 +
78 + if (!out->len) {
79 + fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
80 + return -1;
81 + }
82 +
83 + return 0;
84 +}
85 +
86 +static int commit_tree_with_edited_message(struct repository *repo,
87 + const char *action,
88 + struct commit *original,
89 + struct commit **out)
90 +{
91 + const char *exclude_gpgsig[] = {
92 + /* We reencode the message, so the encoding needs to be stripped. */
93 + "encoding",
94 + /* We need to strip signatures as those will become invalid. */
95 + "gpgsig",
96 + "gpgsig-sha256",
97 + NULL,
98 + };
99 + const char *original_message, *original_body, *ptr;
100 + struct commit_extra_header *original_extra_headers = NULL;
101 + struct strbuf commit_message = STRBUF_INIT;
102 + struct object_id rewritten_commit_oid;
103 + struct object_id original_tree_oid;
104 + struct object_id parent_tree_oid;
105 + char *original_author = NULL;
106 + struct commit *parent;
107 + size_t len;
108 + int ret;
109 +
110 + original_tree_oid = repo_get_commit_tree(repo, original)->object.oid;
111 +
112 + parent = original->parents ? original->parents->item : NULL;
113 + if (parent) {
114 + if (repo_parse_commit(repo, parent)) {
115 + ret = error(_("unable to parse parent commit %s"),
116 + oid_to_hex(&parent->object.oid));
117 + goto out;
118 + }
119 +
120 + parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
121 + } else {
122 + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
123 + }
124 +
125 + /* We retain authorship of the original commit. */
126 + original_message = repo_logmsg_reencode(repo, original, NULL, NULL);
127 + ptr = find_commit_header(original_message, "author", &len);
128 + if (ptr)
129 + original_author = xmemdupz(ptr, len);
130 + find_commit_subject(original_message, &original_body);
131 +
132 + ret = fill_commit_message(repo, &parent_tree_oid, &original_tree_oid,
133 + original_body, action, &commit_message);
134 + if (ret < 0)
135 + goto out;
136 +
137 + original_extra_headers = read_commit_extra_headers(original, exclude_gpgsig);
138 +
139 + ret = commit_tree_extended(commit_message.buf, commit_message.len, &original_tree_oid,
140 + original->parents, &rewritten_commit_oid, original_author,
141 + NULL, NULL, original_extra_headers);
142 + if (ret < 0)
143 + goto out;
144 +
145 + *out = lookup_commit_or_die(&rewritten_commit_oid, "rewritten commit");
146 +
147 +out:
148 + free_commit_extra_headers(original_extra_headers);
149 + strbuf_release(&commit_message);
150 + free(original_author);
151 + return ret;
152 +}
153 +
154 +enum ref_action {
155 + REF_ACTION_DEFAULT,
156 + REF_ACTION_BRANCHES,
157 + REF_ACTION_HEAD,
158 + REF_ACTION_PRINT,
159 +};
160 +
161 +static int parse_ref_action(const struct option *opt, const char *value, int unset)
162 +{
163 + enum ref_action *action = opt->value;
164 +
165 + BUG_ON_OPT_NEG_NOARG(unset, value);
166 + if (!strcmp(value, "branches")) {
167 + *action = REF_ACTION_BRANCHES;
168 + } else if (!strcmp(value, "head")) {
169 + *action = REF_ACTION_HEAD;
170 + } else if (!strcmp(value, "print")) {
171 + *action = REF_ACTION_PRINT;
172 + } else {
173 + return error(_("%s expects one of 'branches', 'head' or 'print'"),
174 + opt->long_name);
175 + }
176 +
177 + return 0;
178 +}
179 +
180 +static int handle_reference_updates(enum ref_action action,
181 + struct repository *repo,
182 + struct commit *original,
183 + struct commit *rewritten,
184 + const char *reflog_msg)
185 +{
186 + const struct name_decoration *decoration;
187 + struct replay_revisions_options opts = { 0 };
188 + struct replay_result result = { 0 };
189 + struct ref_transaction *transaction = NULL;
190 + struct strvec args = STRVEC_INIT;
191 + struct strbuf err = STRBUF_INIT;
192 + struct commit *head = NULL;
193 + struct rev_info revs;
194 + char hex[GIT_MAX_HEXSZ + 1];
195 + bool detached_head;
196 + int head_flags = 0;
197 + int ret;
198 +
199 + refs_read_ref_full(get_main_ref_store(repo), "HEAD",
200 + RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
201 + detached_head = !(head_flags & REF_ISSYMREF);
202 +
203 + repo_init_revisions(repo, &revs, NULL);
204 + strvec_push(&args, "ignored");
205 + strvec_push(&args, "--reverse");
206 + strvec_push(&args, "--topo-order");
207 + strvec_push(&args, "--full-history");
208 +
209 + /* We only want to see commits that are descendants of the old commit. */
210 + strvec_pushf(&args, "--ancestry-path=%s",
211 + oid_to_hex(&original->object.oid));
212 +
213 + /*
214 + * Ancestry path may also show ancestors of the old commit, but we
215 + * don't want to see those, either.
216 + */
217 + strvec_pushf(&args, "^%s", oid_to_hex(&original->object.oid));
218 +
219 + /*
220 + * When we're asked to update HEAD we need to verify that the commit
221 + * that we want to rewrite is actually an ancestor of it and, if so,
222 + * update it. Otherwise we'll update (or print) all descendant
223 + * branches.
224 + */
225 + if (action == REF_ACTION_HEAD) {
226 + struct commit_list *from_list = NULL;
227 +
228 + head = lookup_commit_reference_by_name("HEAD");
229 + if (!head) {
230 + ret = error(_("cannot look up HEAD"));
231 + goto out;
232 + }
233 +
234 + commit_list_insert(original, &from_list);
235 + ret = repo_is_descendant_of(repo, head, from_list);
236 + free_commit_list(from_list);
237 +
238 + if (ret < 0) {
239 + ret = error(_("cannot determine descendance"));
240 + goto out;
241 + } else if (!ret) {
242 + ret = error(_("rewritten commit must be an ancestor "
243 + "of HEAD when using --ref-action=head"));
244 + goto out;
245 + }
246 +
247 + strvec_push(&args, "HEAD");
248 + } else {
249 + strvec_push(&args, "--branches");
250 + strvec_push(&args, "HEAD");
251 + }
252 +
253 + setup_revisions_from_strvec(&args, &revs, NULL);
254 + if (args.nr != 1)
255 + BUG("revisions were set up with invalid argument");
256 +
257 + opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
258 +
259 + ret = replay_revisions(&revs, &opts, &result);
260 + if (ret)
261 + goto out;
262 +
263 + switch (action) {
264 + case REF_ACTION_BRANCHES:
265 + case REF_ACTION_HEAD:
266 + transaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);
267 + if (!transaction) {
268 + ret = error(_("failed to begin ref transaction: %s"), err.buf);
269 + goto out;
270 + }
271 +
272 + for (size_t i = 0; i < result.updates_nr; i++) {
273 + ret = ref_transaction_update(transaction,
274 + result.updates[i].refname,
275 + &result.updates[i].new_oid,
276 + &result.updates[i].old_oid,
277 + NULL, NULL, 0, reflog_msg, &err);
278 + if (ret) {
279 + ret = error(_("failed to update ref '%s': %s"),
280 + result.updates[i].refname, err.buf);
281 + goto out;
282 + }
283 + }
284 +
285 + /*
286 + * `replay_revisions()` only updates references that are
287 + * ancestors of `rewritten`, so we need to manually
288 + * handle updating references that point to `original`.
289 + */
290 + for (decoration = get_name_decoration(&original->object);
291 + decoration;
292 + decoration = decoration->next)
293 + {
294 + if (decoration->type != DECORATION_REF_LOCAL &&
295 + decoration->type != DECORATION_REF_HEAD)
296 + continue;
297 +
298 + if (action == REF_ACTION_HEAD &&
299 + decoration->type != DECORATION_REF_HEAD)
300 + continue;
301 +
302 + /*
303 + * We only need to update HEAD separately in case it's
304 + * detached. If it's not we'd already update the branch
305 + * it is pointing to.
306 + */
307 + if (action == REF_ACTION_BRANCHES &&
308 + decoration->type == DECORATION_REF_HEAD &&
309 + !detached_head)
310 + continue;
311 +
312 + ret = ref_transaction_update(transaction,
313 + decoration->name,
314 + &rewritten->object.oid,
315 + &original->object.oid,
316 + NULL, NULL, 0, reflog_msg, &err);
317 + if (ret) {
318 + ret = error(_("failed to update ref '%s': %s"),
319 + decoration->name, err.buf);
320 + goto out;
321 + }
322 + }
323 +
324 + if (ref_transaction_commit(transaction, &err)) {
325 + ret = error(_("failed to commit ref transaction: %s"), err.buf);
326 + goto out;
327 + }
328 +
329 + break;
330 + case REF_ACTION_PRINT:
331 + for (size_t i = 0; i < result.updates_nr; i++)
332 + printf("update %s %s %s\n",
333 + result.updates[i].refname,
334 + oid_to_hex(&result.updates[i].new_oid),
335 + oid_to_hex(&result.updates[i].old_oid));
336 + break;
337 + default:
338 + BUG("unsupported ref action %d", action);
339 + }
340 +
341 + ret = 0;
342 +
343 +out:
344 + ref_transaction_free(transaction);
345 + replay_result_release(&result);
346 + release_revisions(&revs);
347 + strbuf_release(&err);
348 + strvec_clear(&args);
349 + return ret;
350 +}
351 +
352 +static int cmd_history_reword(int argc,
353 + const char **argv,
354 + const char *prefix,
355 + struct repository *repo)
356 +{
357 + const char * const usage[] = {
358 + GIT_HISTORY_REWORD_USAGE,
359 + NULL,
360 + };
361 + enum ref_action action = REF_ACTION_DEFAULT;
362 + struct option options[] = {
363 + OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
364 + N_("control ref update behavior (branches|head|print)"),
365 + PARSE_OPT_NONEG, parse_ref_action),
366 + OPT_END(),
367 + };
368 + struct strbuf reflog_msg = STRBUF_INIT;
369 + struct commit *original, *rewritten;
370 + int ret;
371 +
372 + argc = parse_options(argc, argv, prefix, options, usage, 0);
373 + if (argc != 1) {
374 + ret = error(_("command expects a single revision"));
375 + goto out;
376 + }
377 + repo_config(repo, git_default_config, NULL);
378 +
379 + if (action == REF_ACTION_DEFAULT)
380 + action = REF_ACTION_BRANCHES;
381 +
382 + original = lookup_commit_reference_by_name(argv[0]);
383 + if (!original) {
384 + ret = error(_("commit cannot be found: %s"), argv[0]);
385 + goto out;
386 + }
387 +
388 + ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
389 + if (ret < 0) {
390 + ret = error(_("failed writing reworded commit"));
391 + goto out;
392 + }
393 +
394 + strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
395 +
396 + ret = handle_reference_updates(action, repo, original, rewritten,
397 + reflog_msg.buf);
398 + if (ret < 0) {
399 + ret = error(_("failed replaying descendants"));
400 + goto out;
401 + }
402 +
403 + ret = 0;
404 +
405 +out:
406 + strbuf_release(&reflog_msg);
407 + return ret;
408 +}
409
410 int cmd_history(int argc,
411 const char **argv,
412 const char *prefix,
8 - struct repository *repo UNUSED)
413 + struct repository *repo)
414 {
415 const char * const usage[] = {
11 - N_("git history [<options>]"),
416 + GIT_HISTORY_REWORD_USAGE,
417 NULL,
418 };
419 + parse_opt_subcommand_fn *fn = NULL;
420 struct option options[] = {
421 + OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
422 OPT_END(),
423 };
424
425 argc = parse_options(argc, argv, prefix, options, usage, 0);
19 - if (argc)
20 - usagef("unrecognized argument: %s", argv[0]);
21 - return 0;
426 + return fn(argc, argv, prefix, repo);
427 }
t/meson.build
+1
@@ -388,6 +388,7 @@ integration_tests = [
388 't3437-rebase-fixup-options.sh',
389 't3438-rebase-broken-files.sh',
390 't3450-history.sh',
391 + 't3451-history-reword.sh',
392 't3500-cherry.sh',
393 't3501-revert-cherry-pick.sh',
394 't3502-cherry-pick-merge.sh',
t/t3450-history.sh
+3 -3
@@ -5,13 +5,13 @@ test_description='tests for git-history command'
5 . ./test-lib.sh
6
7 test_expect_success 'does nothing without any arguments' '
8 - git history >out 2>&1 &&
9 - test_must_be_empty out
8 + test_must_fail git history 2>err &&
9 + test_grep "need a subcommand" err
10 '
11
12 test_expect_success 'raises an error with unknown argument' '
13 test_must_fail git history garbage 2>err &&
14 - test_grep "unrecognized argument: garbage" err
14 + test_grep "unknown subcommand: .garbage." err
15 '
16
17 test_done
t/t3451-history-reword.sh new
+391
@@ -0,0 +1,391 @@
1 +#!/bin/sh
2 +
3 +test_description='tests for git-history reword subcommand'
4 +
5 +. ./test-lib.sh
6 +. "$TEST_DIRECTORY/lib-log-graph.sh"
7 +
8 +reword_with_message () {
9 + cat >message &&
10 + write_script fake-editor.sh <<-\EOF &&
11 + cp message "$1"
12 + EOF
13 + test_set_editor "$(pwd)"/fake-editor.sh &&
14 + git history reword "$@" &&
15 + rm fake-editor.sh message
16 +}
17 +
18 +expect_graph () {
19 + cat >expect &&
20 + lib_test_cmp_graph --graph --format=%s "$@"
21 +}
22 +
23 +expect_log () {
24 + git log --format="%s" "$@" >actual &&
25 + cat >expect &&
26 + test_cmp expect actual
27 +}
28 +
29 +test_expect_success 'can reword tip of a branch' '
30 + test_when_finished "rm -rf repo" &&
31 + git init repo &&
32 + (
33 + cd repo &&
34 + test_commit first &&
35 + test_commit second &&
36 + test_commit third &&
37 +
38 + git symbolic-ref HEAD >expect &&
39 + reword_with_message HEAD <<-EOF &&
40 + third reworded
41 + EOF
42 + git symbolic-ref HEAD >actual &&
43 + test_cmp expect actual &&
44 +
45 + expect_log <<-\EOF &&
46 + third reworded
47 + second
48 + first
49 + EOF
50 +
51 + git reflog >reflog &&
52 + test_grep "reword: updating HEAD" reflog
53 + )
54 +'
55 +
56 +test_expect_success 'can reword commit in the middle' '
57 + test_when_finished "rm -rf repo" &&
58 + git init repo &&
59 + (
60 + cd repo &&
61 + test_commit first &&
62 + test_commit second &&
63 + test_commit third &&
64 +
65 + git symbolic-ref HEAD >expect &&
66 + reword_with_message HEAD~ <<-EOF &&
67 + second reworded
68 + EOF
69 + git symbolic-ref HEAD >actual &&
70 + test_cmp expect actual &&
71 +
72 + expect_log <<-\EOF
73 + third
74 + second reworded
75 + first
76 + EOF
77 + )
78 +'
79 +
80 +test_expect_success 'can reword commit in the middle even on detached head' '
81 + test_when_finished "rm -rf repo" &&
82 + git init repo &&
83 + (
84 + cd repo &&
85 + test_commit first &&
86 + test_commit second &&
87 + test_commit third_on_main &&
88 + git checkout --detach HEAD^ &&
89 + test_commit third_on_head &&
90 +
91 + reword_with_message HEAD~ <<-EOF &&
92 + second reworded
93 + EOF
94 +
95 + expect_graph HEAD --branches <<-\EOF
96 + * third_on_head
97 + | * third_on_main
98 + |/
99 + * second reworded
100 + * first
101 + EOF
102 + )
103 +'
104 +
105 +test_expect_success 'can reword the detached head' '
106 + test_when_finished "rm -rf repo" &&
107 + git init repo &&
108 + (
109 + cd repo &&
110 + test_commit first &&
111 + test_commit second &&
112 + git checkout --detach HEAD &&
113 + test_commit third &&
114 +
115 + reword_with_message HEAD <<-EOF &&
116 + third reworded
117 + EOF
118 +
119 + expect_log <<-\EOF
120 + third reworded
121 + second
122 + first
123 + EOF
124 + )
125 +'
126 +
127 +test_expect_success 'can reword root commit' '
128 + test_when_finished "rm -rf repo" &&
129 + git init repo &&
130 + (
131 + cd repo &&
132 + test_commit first &&
133 + test_commit second &&
134 + test_commit third &&
135 + reword_with_message HEAD~2 <<-EOF &&
136 + first reworded
137 + EOF
138 +
139 + expect_log <<-\EOF
140 + third
141 + second
142 + first reworded
143 + EOF
144 + )
145 +'
146 +
147 +test_expect_success 'can reword in a bare repo' '
148 + test_when_finished "rm -rf repo repo.git" &&
149 + git init repo &&
150 + test_commit -C repo first &&
151 + git clone --bare repo repo.git &&
152 + (
153 + cd repo.git &&
154 + reword_with_message HEAD <<-EOF &&
155 + reworded
156 + EOF
157 +
158 + expect_log <<-\EOF
159 + reworded
160 + EOF
161 + )
162 +'
163 +
164 +test_expect_success 'can reword a commit on a different branch' '
165 + test_when_finished "rm -rf repo" &&
166 + git init repo &&
167 + (
168 + cd repo &&
169 + test_commit base &&
170 + git branch theirs &&
171 + test_commit ours &&
172 + git switch theirs &&
173 + test_commit theirs &&
174 +
175 + git rev-parse ours >ours-before &&
176 + reword_with_message theirs <<-EOF &&
177 + Reworded theirs
178 + EOF
179 + git rev-parse ours >ours-after &&
180 + test_cmp ours-before ours-after &&
181 +
182 + expect_graph --branches <<-\EOF
183 + * Reworded theirs
184 + | * ours
185 + |/
186 + * base
187 + EOF
188 + )
189 +'
190 +
191 +test_expect_success 'can reword a merge commit' '
192 + test_when_finished "rm -rf repo" &&
193 + git init repo &&
194 + (
195 + cd repo &&
196 + test_commit base &&
197 + git branch branch &&
198 + test_commit ours &&
199 + git switch branch &&
200 + test_commit theirs &&
201 + git switch - &&
202 + git merge theirs &&
203 +
204 + # It is not possible to replay merge commits embedded in the
205 + # history (yet).
206 + test_must_fail git history reword HEAD~ 2>err &&
207 + test_grep "replaying merge commits is not supported yet" err &&
208 +
209 + # But it is possible to reword a merge commit directly.
210 + reword_with_message HEAD <<-EOF &&
211 + Reworded merge commit
212 + EOF
213 + expect_graph <<-\EOF
214 + * Reworded merge commit
215 + |\
216 + | * theirs
217 + * | ours
218 + |/
219 + * base
220 + EOF
221 + )
222 +'
223 +
224 +test_expect_success '--ref-action=print prints ref updates without modifying repo' '
225 + test_when_finished "rm -rf repo" &&
226 + git init repo --initial-branch=main &&
227 + (
228 + cd repo &&
229 + test_commit base &&
230 + git branch branch &&
231 + test_commit ours &&
232 + git switch branch &&
233 + test_commit theirs &&
234 +
235 + git refs list >refs-expect &&
236 + reword_with_message --ref-action=print base >updates <<-\EOF &&
237 + reworded commit
238 + EOF
239 + git refs list >refs-actual &&
240 + test_cmp refs-expect refs-actual &&
241 +
242 + test_grep "update refs/heads/branch" updates &&
243 + test_grep "update refs/heads/main" updates &&
244 + git update-ref --stdin <updates &&
245 + expect_log --branches <<-\EOF
246 + theirs
247 + ours
248 + reworded commit
249 + EOF
250 + )
251 +'
252 +
253 +test_expect_success '--ref-action=head updates only HEAD' '
254 + test_when_finished "rm -rf repo" &&
255 + git init repo --initial-branch=main &&
256 + (
257 + cd repo &&
258 + test_commit base &&
259 + git branch branch &&
260 + test_commit theirs &&
261 + git switch branch &&
262 + test_commit ours &&
263 +
264 + # When told to update HEAD, only, the command will refuse to
265 + # rewrite commits that are not an ancestor of HEAD.
266 + test_must_fail git history reword --ref-action=head theirs 2>err &&
267 + test_grep "rewritten commit must be an ancestor of HEAD" err &&
268 +
269 + reword_with_message --ref-action=head base >updates <<-\EOF &&
270 + reworded base
271 + EOF
272 + expect_log HEAD <<-\EOF &&
273 + ours
274 + reworded base
275 + EOF
276 + expect_log main <<-\EOF
277 + theirs
278 + base
279 + EOF
280 + )
281 +'
282 +
283 +test_expect_success 'editor shows proper status' '
284 + test_when_finished "rm -rf repo" &&
285 + git init repo &&
286 + (
287 + cd repo &&
288 + test_commit first &&
289 +
290 + write_script fake-editor.sh <<-\EOF &&
291 + cp "$1" . &&
292 + printf "\namend a comment\n" >>"$1"
293 + EOF
294 + test_set_editor "$(pwd)"/fake-editor.sh &&
295 + git history reword HEAD &&
296 +
297 + cat >expect <<-EOF &&
298 + first
299 +
300 + # Please enter the commit message for the reworded changes. Lines starting
301 + # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
302 + # Changes to be committed:
303 + # new file: first.t
304 + #
305 + EOF
306 + test_cmp expect COMMIT_EDITMSG &&
307 +
308 + test_commit_message HEAD <<-\EOF
309 + first
310 +
311 + amend a comment
312 + EOF
313 + )
314 +'
315 +
316 +# For now, git-history(1) does not yet execute any hooks. This is subject to
317 +# change in the future, and if it does this test here is expected to start
318 +# failing. In other words, this test is not an endorsement of the current
319 +# status quo.
320 +test_expect_success 'hooks are not executed for rewritten commits' '
321 + test_when_finished "rm -rf repo" &&
322 + git init repo &&
323 + (
324 + cd repo &&
325 + test_commit first &&
326 + test_commit second &&
327 + test_commit third &&
328 +
329 + ORIG_PATH="$(pwd)" &&
330 + export ORIG_PATH &&
331 + for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg
332 + do
333 + write_script .git/hooks/$hook <<-\EOF || exit 1
334 + touch "$ORIG_PATH/hooks.log
335 + EOF
336 + done &&
337 +
338 + reword_with_message HEAD~ <<-EOF &&
339 + second reworded
340 + EOF
341 +
342 + cat >expect <<-EOF &&
343 + third
344 + second reworded
345 + first
346 + EOF
347 + git log --format=%s >actual &&
348 + test_cmp expect actual &&
349 +
350 + test_path_is_missing hooks.log
351 + )
352 +'
353 +
354 +test_expect_success 'aborts with empty commit message' '
355 + test_when_finished "rm -rf repo" &&
356 + git init repo &&
357 + (
358 + cd repo &&
359 + test_commit first &&
360 +
361 + ! reword_with_message HEAD 2>err </dev/null &&
362 + test_grep "Aborting commit due to empty commit message." err
363 + )
364 +'
365 +
366 +test_expect_success 'retains changes in the worktree and index' '
367 + test_when_finished "rm -rf repo" &&
368 + git init repo &&
369 + (
370 + cd repo &&
371 + touch a b &&
372 + git add . &&
373 + git commit -m "initial commit" &&
374 + echo foo >a &&
375 + echo bar >b &&
376 + git add b &&
377 + reword_with_message HEAD <<-EOF &&
378 + message
379 + EOF
380 + cat >expect <<-\EOF &&
381 + M a
382 + M b
383 + ?? actual
384 + ?? expect
385 + EOF
386 + git status --porcelain >actual &&
387 + test_cmp expect actual
388 + )
389 +'
390 +
391 +test_done