builtin/history: implement "split" subcommand

It is quite a common use case that one wants to split up one commit into multiple commits by moving parts of the changes of the original commit out into a separate commit. This is quite an involved operation though: 1. Identify the commit in question that is to be dropped. 2. Perform an interactive rebase on top of that commit's parent. 3. Modify the instruction sheet to "edit" the commit that is to be split up. 4. Drop the commit via "git reset HEAD~". 5. Stage changes that should go into the first commit and commit it. 6. Stage changes that should go into the second commit and commit it. 7. Finalize the rebase. This is quite complex, and overall I would claim that most people who are not experts in Git would struggle with this flow. Introduce a new "split" subcommand for git-history(1) to make this way easier. All the user needs to do is to say `git history split $COMMIT`. From hereon, Git asks the user which parts of the commit shall be moved out into a separate commit and, once done, asks the user for the commit message. Git then creates that split-out commit and applies the original commit on top of it. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Mar 2, 2026 at 13:13 UTC d563ecec2845467880f5742e178a9723afef495a
4 files changed +1070
Documentation/git-history.adoc
+62
@@ -9,6 +9,7 @@ SYNOPSIS
9 --------
10 [synopsis]
11 git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
12 +git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
13
14 DESCRIPTION
15 -----------
@@ -57,6 +58,26 @@ The following commands are available to rewrite history in different ways:
58 details of this commit remain unchanged. This command will spawn an
59 editor with the current message of that commit.
60
61 +`split <commit> [--] [<pathspec>...]`::
62 + Interactively split up <commit> into two commits by choosing
63 + hunks introduced by it that will be moved into the new split-out
64 + commit. These hunks will then be written into a new commit that
65 + becomes the parent of the previous commit. The original commit
66 + stays intact, except that its parent will be the newly split-out
67 + commit.
68 ++
69 +The commit messages of the split-up commits will be asked for by launching
70 +the configured editor. Authorship of the commit will be the same as for the
71 +original commit.
72 ++
73 +If passed, _<pathspec>_ can be used to limit which changes shall be split out
74 +of the original commit. Files not matching any of the pathspecs will remain
75 +part of the original commit. For more details, see the 'pathspec' entry in
76 +linkgit:gitglossary[7].
77 ++
78 +It is invalid to select either all or no hunks, as that would lead to
79 +one of the commits becoming empty.
80 +
81 OPTIONS
82 -------
83
@@ -72,6 +93,47 @@ OPTIONS
93 descendants of the original commit will be rewritten. With `head`, only
94 the current `HEAD` reference will be rewritten. Defaults to `branches`.
95
96 +EXAMPLES
97 +--------
98 +
99 +Split a commit
100 +~~~~~~~~~~~~~~
101 +
102 +----------
103 +$ git log --stat --oneline
104 +3f81232 (HEAD -> main) original
105 + bar | 1 +
106 + foo | 1 +
107 + 2 files changed, 2 insertions(+)
108 +
109 +$ git history split HEAD
110 +diff --git a/bar b/bar
111 +new file mode 100644
112 +index 0000000..5716ca5
113 +--- /dev/null
114 ++++ b/bar
115 +@@ -0,0 +1 @@
116 ++bar
117 +(1/1) Stage addition [y,n,q,a,d,p,?]? y
118 +
119 +diff --git a/foo b/foo
120 +new file mode 100644
121 +index 0000000..257cc56
122 +--- /dev/null
123 ++++ b/foo
124 +@@ -0,0 +1 @@
125 ++foo
126 +(1/1) Stage addition [y,n,q,a,d,p,?]? n
127 +
128 +$ git log --stat --oneline
129 +7cebe64 (HEAD -> main) original
130 + foo | 1 +
131 + 1 file changed, 1 insertion(+)
132 +d1582f3 split-out commit
133 + bar | 1 +
134 + 1 file changed, 1 insertion(+)
135 +----------
136 +
137 GIT
138 ---
139 Part of the linkgit:git[1] suite
builtin/history.c
+250
@@ -1,6 +1,7 @@
1 #define USE_THE_REPOSITORY_VARIABLE
2
3 #include "builtin.h"
4 +#include "cache-tree.h"
5 #include "commit.h"
6 #include "commit-reach.h"
7 #include "config.h"
@@ -8,17 +9,24 @@
9 #include "environment.h"
10 #include "gettext.h"
11 #include "hex.h"
12 +#include "lockfile.h"
13 +#include "oidmap.h"
14 #include "parse-options.h"
15 +#include "path.h"
16 +#include "read-cache.h"
17 #include "refs.h"
18 #include "replay.h"
19 #include "revision.h"
20 #include "sequencer.h"
21 #include "strvec.h"
22 #include "tree.h"
23 +#include "unpack-trees.h"
24 #include "wt-status.h"
25
26 #define GIT_HISTORY_REWORD_USAGE \
27 N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
28 +#define GIT_HISTORY_SPLIT_USAGE \
29 + N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
30
31 static void change_data_free(void *util, const char *str UNUSED)
32 {
@@ -484,6 +492,246 @@ out:
492 return ret;
493 }
494
495 +static int write_ondisk_index(struct repository *repo,
496 + struct object_id *oid,
497 + const char *path)
498 +{
499 + struct unpack_trees_options opts = { 0 };
500 + struct lock_file lock = LOCK_INIT;
501 + struct tree_desc tree_desc;
502 + struct index_state index;
503 + struct tree *tree;
504 + int ret;
505 +
506 + index_state_init(&index, repo);
507 +
508 + opts.head_idx = -1;
509 + opts.src_index = &index;
510 + opts.dst_index = &index;
511 +
512 + tree = repo_parse_tree_indirect(repo, oid);
513 + init_tree_desc(&tree_desc, &tree->object.oid, tree->buffer, tree->size);
514 +
515 + if (unpack_trees(1, &tree_desc, &opts)) {
516 + ret = error(_("unable to populate index with tree"));
517 + goto out;
518 + }
519 +
520 + prime_cache_tree(repo, &index, tree);
521 +
522 + if (hold_lock_file_for_update(&lock, path, 0) < 0) {
523 + ret = error_errno(_("unable to acquire index lock"));
524 + goto out;
525 + }
526 +
527 + if (write_locked_index(&index, &lock, COMMIT_LOCK)) {
528 + ret = error(_("unable to write new index file"));
529 + goto out;
530 + }
531 +
532 + ret = 0;
533 +
534 +out:
535 + rollback_lock_file(&lock);
536 + release_index(&index);
537 + return ret;
538 +}
539 +
540 +static int split_commit(struct repository *repo,
541 + struct commit *original,
542 + struct pathspec *pathspec,
543 + struct commit **out)
544 +{
545 + struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
546 + struct strbuf index_file = STRBUF_INIT;
547 + struct index_state index = INDEX_STATE_INIT(repo);
548 + const struct object_id *original_commit_tree_oid;
549 + const struct object_id *old_tree_oid, *new_tree_oid;
550 + struct object_id parent_tree_oid;
551 + char original_commit_oid[GIT_MAX_HEXSZ + 1];
552 + struct commit *first_commit, *second_commit;
553 + struct commit_list *parents = NULL;
554 + struct tree *split_tree;
555 + int ret;
556 +
557 + if (original->parents) {
558 + if (repo_parse_commit(repo, original->parents->item)) {
559 + ret = error(_("unable to parse parent commit %s"),
560 + oid_to_hex(&original->parents->item->object.oid));
561 + goto out;
562 + }
563 +
564 + parent_tree_oid = *get_commit_tree_oid(original->parents->item);
565 + } else {
566 + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
567 + }
568 + original_commit_tree_oid = get_commit_tree_oid(original);
569 +
570 + /*
571 + * Construct the first commit. This is done by taking the original
572 + * commit parent's tree and selectively patching changes from the diff
573 + * between that parent and its child.
574 + */
575 + repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
576 +
577 + ret = write_ondisk_index(repo, &parent_tree_oid, index_file.buf);
578 + if (ret < 0)
579 + goto out;
580 +
581 + ret = read_index_from(&index, index_file.buf, repo->gitdir);
582 + if (ret < 0) {
583 + ret = error(_("failed reading temporary index"));
584 + goto out;
585 + }
586 +
587 + oid_to_hex_r(original_commit_oid, &original->object.oid);
588 + ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
589 + original_commit_oid, pathspec, ADD_P_DISALLOW_EDIT);
590 + if (ret < 0)
591 + goto out;
592 +
593 + split_tree = write_in_core_index_as_tree(repo, &index);
594 + if (!split_tree) {
595 + ret = error(_("failed split tree"));
596 + goto out;
597 + }
598 +
599 + unlink(index_file.buf);
600 + strbuf_release(&index_file);
601 +
602 + /*
603 + * We disallow the cases where either the split-out commit or the
604 + * original commit would become empty. Consequently, if we see that the
605 + * new tree ID matches either of those trees we abort.
606 + */
607 + if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
608 + ret = error(_("split commit is empty"));
609 + goto out;
610 + } else if (oideq(&split_tree->object.oid, original_commit_tree_oid)) {
611 + ret = error(_("split commit tree matches original commit"));
612 + goto out;
613 + }
614 +
615 + /*
616 + * The first commit is constructed from the split-out tree. The base
617 + * that shall be diffed against is the parent of the original commit.
618 + */
619 + ret = commit_tree_with_edited_message_ext(repo, "split-out", original,
620 + original->parents, &parent_tree_oid,
621 + &split_tree->object.oid, &first_commit);
622 + if (ret < 0) {
623 + ret = error(_("failed writing first commit"));
624 + goto out;
625 + }
626 +
627 + /*
628 + * The second commit is constructed from the original tree. The base to
629 + * diff against and the parent in this case is the first split-out
630 + * commit.
631 + */
632 + commit_list_append(first_commit, &parents);
633 +
634 + old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid;
635 + new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
636 +
637 + ret = commit_tree_with_edited_message_ext(repo, "split-out", original,
638 + parents, old_tree_oid,
639 + new_tree_oid, &second_commit);
640 + if (ret < 0) {
641 + ret = error(_("failed writing second commit"));
642 + goto out;
643 + }
644 +
645 + *out = second_commit;
646 + ret = 0;
647 +
648 +out:
649 + if (index_file.len)
650 + unlink(index_file.buf);
651 + strbuf_release(&index_file);
652 + free_commit_list(parents);
653 + release_index(&index);
654 + return ret;
655 +}
656 +
657 +static int cmd_history_split(int argc,
658 + const char **argv,
659 + const char *prefix,
660 + struct repository *repo)
661 +{
662 + const char * const usage[] = {
663 + GIT_HISTORY_SPLIT_USAGE,
664 + NULL,
665 + };
666 + enum ref_action action = REF_ACTION_DEFAULT;
667 + int dry_run = 0;
668 + struct option options[] = {
669 + OPT_CALLBACK_F(0, "update-refs", &action, N_("<refs>"),
670 + N_("control ref update behavior (branches|head|print)"),
671 + PARSE_OPT_NONEG, parse_ref_action),
672 + OPT_BOOL('n', "dry-run", &dry_run,
673 + N_("perform a dry-run without updating any refs")),
674 + OPT_END(),
675 + };
676 + struct commit *original, *rewritten = NULL;
677 + struct strbuf reflog_msg = STRBUF_INIT;
678 + struct pathspec pathspec = { 0 };
679 + struct rev_info revs = { 0 };
680 + int ret;
681 +
682 + argc = parse_options(argc, argv, prefix, options, usage, 0);
683 + if (argc < 1) {
684 + ret = error(_("command expects a committish"));
685 + goto out;
686 + }
687 + repo_config(repo, git_default_config, NULL);
688 +
689 + if (action == REF_ACTION_DEFAULT)
690 + action = REF_ACTION_BRANCHES;
691 +
692 + parse_pathspec(&pathspec, 0,
693 + PATHSPEC_PREFER_FULL |
694 + PATHSPEC_SYMLINK_LEADING_PATH |
695 + PATHSPEC_PREFIX_ORIGIN,
696 + prefix, argv + 1);
697 +
698 + original = lookup_commit_reference_by_name(argv[0]);
699 + if (!original) {
700 + ret = error(_("commit cannot be found: %s"), argv[0]);
701 + goto out;
702 + }
703 +
704 + ret = setup_revwalk(repo, action, original, &revs);
705 + if (ret < 0)
706 + goto out;
707 +
708 + if (original->parents && original->parents->next) {
709 + ret = error(_("cannot split up merge commit"));
710 + goto out;
711 + }
712 +
713 + ret = split_commit(repo, original, &pathspec, &rewritten);
714 + if (ret < 0)
715 + goto out;
716 +
717 + strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
718 +
719 + ret = handle_reference_updates(&revs, action, original, rewritten,
720 + reflog_msg.buf, dry_run);
721 + if (ret < 0) {
722 + ret = error(_("failed replaying descendants"));
723 + goto out;
724 + }
725 +
726 + ret = 0;
727 +
728 +out:
729 + strbuf_release(&reflog_msg);
730 + clear_pathspec(&pathspec);
731 + release_revisions(&revs);
732 + return ret;
733 +}
734 +
735 int cmd_history(int argc,
736 const char **argv,
737 const char *prefix,
@@ -491,11 +739,13 @@ int cmd_history(int argc,
739 {
740 const char * const usage[] = {
741 GIT_HISTORY_REWORD_USAGE,
742 + GIT_HISTORY_SPLIT_USAGE,
743 NULL,
744 };
745 parse_opt_subcommand_fn *fn = NULL;
746 struct option options[] = {
747 OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
748 + OPT_SUBCOMMAND("split", &fn, cmd_history_split),
749 OPT_END(),
750 };
751
t/meson.build
+1
@@ -392,6 +392,7 @@ integration_tests = [
392 't3438-rebase-broken-files.sh',
393 't3450-history.sh',
394 't3451-history-reword.sh',
395 + 't3452-history-split.sh',
396 't3500-cherry.sh',
397 't3501-revert-cherry-pick.sh',
398 't3502-cherry-pick-merge.sh',
t/t3452-history-split.sh new
+757
@@ -0,0 +1,757 @@
1 +#!/bin/sh
2 +
3 +test_description='tests for git-history split subcommand'
4 +
5 +. ./test-lib.sh
6 +. "$TEST_DIRECTORY/lib-log-graph.sh"
7 +
8 +# The fake editor takes multiple arguments, each of which represents a commit
9 +# message. Subsequent invocations of the editor will then yield those messages
10 +# in order.
11 +#
12 +set_fake_editor () {
13 + printf "%s\n" "$@" >fake-input &&
14 + write_script fake-editor.sh <<-\EOF &&
15 + head -n1 fake-input >"$1"
16 + sed 1d fake-input >fake-input.trimmed &&
17 + mv fake-input.trimmed fake-input
18 + EOF
19 + test_set_editor "$(pwd)"/fake-editor.sh
20 +}
21 +
22 +expect_graph () {
23 + cat >expect &&
24 + lib_test_cmp_graph --graph --format=%s "$@"
25 +}
26 +
27 +expect_log () {
28 + git log --format="%s" >actual &&
29 + cat >expect &&
30 + test_cmp expect actual
31 +}
32 +
33 +expect_tree_entries () {
34 + git ls-tree --name-only "$1" >actual &&
35 + cat >expect &&
36 + test_cmp expect actual
37 +}
38 +
39 +test_expect_success 'refuses to work with merge commits' '
40 + test_when_finished "rm -rf repo" &&
41 + git init repo &&
42 + (
43 + cd repo &&
44 + test_commit base &&
45 + git branch branch &&
46 + test_commit ours &&
47 + git switch branch &&
48 + test_commit theirs &&
49 + git switch - &&
50 + git merge theirs &&
51 + test_must_fail git history split HEAD 2>err &&
52 + test_grep "cannot split up merge commit" err &&
53 + test_must_fail git history split HEAD~ 2>err &&
54 + test_grep "replaying merge commits is not supported yet" err
55 + )
56 +'
57 +
58 +test_expect_success 'errors on missing commit argument' '
59 + test_when_finished "rm -rf repo" &&
60 + git init repo &&
61 + (
62 + cd repo &&
63 + test_commit initial &&
64 + test_must_fail git history split 2>err &&
65 + test_grep "command expects a committish" err
66 + )
67 +'
68 +
69 +test_expect_success 'errors on unknown revision' '
70 + test_when_finished "rm -rf repo" &&
71 + git init repo &&
72 + (
73 + cd repo &&
74 + test_commit initial &&
75 + test_must_fail git history split does-not-exist 2>err &&
76 + test_grep "commit cannot be found" err
77 + )
78 +'
79 +
80 +test_expect_success '--dry-run does not modify any refs' '
81 + test_when_finished "rm -rf repo" &&
82 + git init repo &&
83 + (
84 + cd repo &&
85 + test_commit base &&
86 + touch bar foo &&
87 + git add . &&
88 + git commit -m split-me &&
89 +
90 + git refs list --include-root-refs >before &&
91 +
92 + set_fake_editor "first" "second" &&
93 + git history split --dry-run HEAD <<-EOF &&
94 + y
95 + n
96 + EOF
97 +
98 + git refs list --include-root-refs >after &&
99 + test_cmp before after
100 + )
101 +'
102 +
103 +test_expect_success 'can split up tip commit' '
104 + test_when_finished "rm -rf repo" &&
105 + git init repo &&
106 + (
107 + cd repo &&
108 + test_commit initial &&
109 + touch bar foo &&
110 + git add . &&
111 + git commit -m split-me &&
112 +
113 + git symbolic-ref HEAD >expect &&
114 + set_fake_editor "first" "second" &&
115 + git history split HEAD <<-EOF &&
116 + y
117 + n
118 + EOF
119 + git symbolic-ref HEAD >actual &&
120 + test_cmp expect actual &&
121 +
122 + expect_log <<-EOF &&
123 + second
124 + first
125 + initial
126 + EOF
127 +
128 + expect_tree_entries HEAD~ <<-EOF &&
129 + bar
130 + initial.t
131 + EOF
132 +
133 + expect_tree_entries HEAD <<-EOF &&
134 + bar
135 + foo
136 + initial.t
137 + EOF
138 +
139 + git reflog >reflog &&
140 + test_grep "split: updating HEAD" reflog
141 + )
142 +'
143 +
144 +test_expect_success 'can split up root commit' '
145 + test_when_finished "rm -rf repo" &&
146 + git init repo &&
147 + (
148 + cd repo &&
149 + touch bar foo &&
150 + git add . &&
151 + git commit -m root &&
152 + test_commit tip &&
153 +
154 + set_fake_editor "first" "second" &&
155 + git history split HEAD~ <<-EOF &&
156 + y
157 + n
158 + EOF
159 +
160 + expect_log <<-EOF &&
161 + tip
162 + second
163 + first
164 + EOF
165 +
166 + expect_tree_entries HEAD~2 <<-EOF &&
167 + bar
168 + EOF
169 +
170 + expect_tree_entries HEAD~ <<-EOF &&
171 + bar
172 + foo
173 + EOF
174 +
175 + expect_tree_entries HEAD <<-EOF
176 + bar
177 + foo
178 + tip.t
179 + EOF
180 + )
181 +'
182 +
183 +test_expect_success 'can split up in-between commit' '
184 + test_when_finished "rm -rf repo" &&
185 + git init repo &&
186 + (
187 + cd repo &&
188 + test_commit initial &&
189 + touch bar foo &&
190 + git add . &&
191 + git commit -m split-me &&
192 + test_commit tip &&
193 +
194 + set_fake_editor "first" "second" &&
195 + git history split HEAD~ <<-EOF &&
196 + y
197 + n
198 + EOF
199 +
200 + expect_log <<-EOF &&
201 + tip
202 + second
203 + first
204 + initial
205 + EOF
206 +
207 + expect_tree_entries HEAD~2 <<-EOF &&
208 + bar
209 + initial.t
210 + EOF
211 +
212 + expect_tree_entries HEAD~ <<-EOF &&
213 + bar
214 + foo
215 + initial.t
216 + EOF
217 +
218 + expect_tree_entries HEAD <<-EOF
219 + bar
220 + foo
221 + initial.t
222 + tip.t
223 + EOF
224 + )
225 +'
226 +
227 +test_expect_success 'can split HEAD only' '
228 + test_when_finished "rm -rf repo" &&
229 + git init repo &&
230 + (
231 + cd repo &&
232 + test_commit base &&
233 + touch a b &&
234 + git add . &&
235 + git commit -m split-me &&
236 + git branch unrelated &&
237 +
238 + set_fake_editor "ours-a" "ours-b" &&
239 + git history split --update-refs=head HEAD <<-EOF &&
240 + y
241 + n
242 + EOF
243 + expect_graph --branches <<-EOF
244 + * ours-b
245 + * ours-a
246 + | * split-me
247 + |/
248 + * base
249 + EOF
250 + )
251 +'
252 +
253 +test_expect_success 'can split detached HEAD' '
254 + test_when_finished "rm -rf repo" &&
255 + git init repo &&
256 + (
257 + cd repo &&
258 + test_commit initial &&
259 + touch bar foo &&
260 + git add . &&
261 + git commit -m split-me &&
262 + git checkout --detach HEAD &&
263 +
264 + set_fake_editor "first" "second" &&
265 + git history split --update-refs=head HEAD <<-EOF &&
266 + y
267 + n
268 + EOF
269 +
270 + # HEAD should be detached and updated.
271 + test_must_fail git symbolic-ref HEAD &&
272 +
273 + expect_log <<-EOF
274 + second
275 + first
276 + initial
277 + EOF
278 + )
279 +'
280 +
281 +test_expect_success 'can split commit in unrelated branch' '
282 + test_when_finished "rm -rf repo" &&
283 + git init repo &&
284 + (
285 + cd repo &&
286 + test_commit base &&
287 + git branch ours &&
288 + git switch --create theirs &&
289 + touch theirs-a theirs-b &&
290 + git add . &&
291 + git commit -m theirs &&
292 + git switch ours &&
293 + test_commit ours &&
294 +
295 + # With --update-refs=head it is not possible to split up a
296 + # commit that is unrelated to HEAD.
297 + test_must_fail git history split --update-refs=head theirs 2>err &&
298 + test_grep "rewritten commit must be an ancestor of HEAD" err &&
299 +
300 + set_fake_editor "theirs-rewritten-a" "theirs-rewritten-b" &&
301 + git history split theirs <<-EOF &&
302 + y
303 + n
304 + EOF
305 + expect_graph --branches <<-EOF &&
306 + * ours
307 + | * theirs-rewritten-b
308 + | * theirs-rewritten-a
309 + |/
310 + * base
311 + EOF
312 +
313 + expect_tree_entries theirs~ <<-EOF &&
314 + base.t
315 + theirs-a
316 + EOF
317 +
318 + expect_tree_entries theirs <<-EOF
319 + base.t
320 + theirs-a
321 + theirs-b
322 + EOF
323 + )
324 +'
325 +
326 +test_expect_success 'updates multiple descendant branches' '
327 + test_when_finished "rm -rf repo" &&
328 + git init repo --initial-branch=main &&
329 + (
330 + cd repo &&
331 + test_commit base &&
332 + touch file-a file-b &&
333 + git add . &&
334 + git commit -m split-me &&
335 + git branch branch &&
336 + test_commit on-main &&
337 + git switch branch &&
338 + test_commit on-branch &&
339 + git switch main &&
340 +
341 + set_fake_editor "split-a" "split-b" &&
342 + git history split HEAD~ <<-EOF &&
343 + y
344 + n
345 + EOF
346 +
347 + # Both branches should now descend from the split commits.
348 + expect_graph --branches <<-EOF
349 + * on-branch
350 + | * on-main
351 + |/
352 + * split-b
353 + * split-a
354 + * base
355 + EOF
356 + )
357 +'
358 +
359 +test_expect_success 'can pick multiple hunks' '
360 + test_when_finished "rm -rf repo" &&
361 + git init repo &&
362 + (
363 + cd repo &&
364 + touch bar baz foo qux &&
365 + git add . &&
366 + git commit -m split-me &&
367 +
368 + set_fake_editor "first" "second" &&
369 + git history split HEAD <<-EOF &&
370 + y
371 + n
372 + y
373 + n
374 + EOF
375 +
376 + expect_tree_entries HEAD~ <<-EOF &&
377 + bar
378 + foo
379 + EOF
380 +
381 + expect_tree_entries HEAD <<-EOF
382 + bar
383 + baz
384 + foo
385 + qux
386 + EOF
387 + )
388 +'
389 +
390 +test_expect_success 'can use only last hunk' '
391 + test_when_finished "rm -rf repo" &&
392 + git init repo &&
393 + (
394 + cd repo &&
395 + touch bar foo &&
396 + git add . &&
397 + git commit -m split-me &&
398 +
399 + set_fake_editor "first" "second" &&
400 + git history split HEAD <<-EOF &&
401 + n
402 + y
403 + EOF
404 +
405 + expect_log <<-EOF &&
406 + second
407 + first
408 + EOF
409 +
410 + expect_tree_entries HEAD~ <<-EOF &&
411 + foo
412 + EOF
413 +
414 + expect_tree_entries HEAD <<-EOF
415 + bar
416 + foo
417 + EOF
418 + )
419 +'
420 +
421 +test_expect_success 'can split commit with file deletions' '
422 + test_when_finished "rm -rf repo" &&
423 + git init repo &&
424 + (
425 + cd repo &&
426 + echo a >a &&
427 + echo b >b &&
428 + echo c >c &&
429 + git add . &&
430 + git commit -m base &&
431 + git rm a b &&
432 + git commit -m delete-both &&
433 +
434 + set_fake_editor "delete-a" "delete-b" &&
435 + git history split HEAD <<-EOF &&
436 + y
437 + n
438 + EOF
439 +
440 + expect_log <<-EOF &&
441 + delete-b
442 + delete-a
443 + base
444 + EOF
445 +
446 + expect_tree_entries HEAD~ <<-EOF &&
447 + b
448 + c
449 + EOF
450 +
451 + expect_tree_entries HEAD <<-EOF
452 + c
453 + EOF
454 + )
455 +'
456 +
457 +test_expect_success 'preserves original authorship' '
458 + test_when_finished "rm -rf repo" &&
459 + git init repo &&
460 + (
461 + cd repo &&
462 + test_commit initial &&
463 + touch bar foo &&
464 + git add . &&
465 + GIT_AUTHOR_NAME="Other Author" \
466 + GIT_AUTHOR_EMAIL="other@example.com" \
467 + git commit -m split-me &&
468 +
469 + set_fake_editor "first" "second" &&
470 + git history split HEAD <<-EOF &&
471 + y
472 + n
473 + EOF
474 +
475 + git log -1 --format="%an <%ae>" HEAD~ >actual &&
476 + echo "Other Author <other@example.com>" >expect &&
477 + test_cmp expect actual &&
478 +
479 + git log -1 --format="%an <%ae>" HEAD >actual &&
480 + test_cmp expect actual
481 + )
482 +'
483 +
484 +test_expect_success 'aborts with empty commit message' '
485 + test_when_finished "rm -rf repo" &&
486 + git init repo &&
487 + (
488 + cd repo &&
489 + touch bar foo &&
490 + git add . &&
491 + git commit -m split-me &&
492 +
493 + set_fake_editor "" &&
494 + test_must_fail git history split HEAD <<-EOF 2>err &&
495 + y
496 + n
497 + EOF
498 + test_grep "Aborting commit due to empty commit message." err
499 + )
500 +'
501 +
502 +test_expect_success 'commit message editor sees split-out changes' '
503 + test_when_finished "rm -rf repo" &&
504 + git init repo &&
505 + (
506 + cd repo &&
507 + touch bar foo &&
508 + git add . &&
509 + git commit -m split-me &&
510 +
511 + write_script fake-editor.sh <<-\EOF &&
512 + cat "$1" >>MESSAGES &&
513 + echo "some commit message" >"$1"
514 + EOF
515 + test_set_editor "$(pwd)"/fake-editor.sh &&
516 +
517 + git history split HEAD <<-EOF &&
518 + y
519 + n
520 + EOF
521 +
522 + # Note that we expect to see the messages twice, once for each
523 + # of the commits. The committed files are different though.
524 + cat >expect <<-EOF &&
525 + split-me
526 +
527 + # Please enter the commit message for the split-out changes. Lines starting
528 + # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
529 + # Changes to be committed:
530 + # new file: bar
531 + #
532 + split-me
533 +
534 + # Please enter the commit message for the split-out changes. Lines starting
535 + # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
536 + # Changes to be committed:
537 + # new file: foo
538 + #
539 + EOF
540 + test_cmp expect MESSAGES &&
541 +
542 + expect_log <<-EOF
543 + some commit message
544 + some commit message
545 + EOF
546 + )
547 +'
548 +
549 +test_expect_success 'can use pathspec to limit what gets split' '
550 + test_when_finished "rm -rf repo" &&
551 + git init repo &&
552 + (
553 + cd repo &&
554 + touch bar foo &&
555 + git add . &&
556 + git commit -m split-me &&
557 +
558 + set_fake_editor "first" "second" &&
559 + git history split HEAD -- foo <<-EOF &&
560 + y
561 + EOF
562 +
563 + expect_tree_entries HEAD~ <<-EOF &&
564 + foo
565 + EOF
566 +
567 + expect_tree_entries HEAD <<-EOF
568 + bar
569 + foo
570 + EOF
571 + )
572 +'
573 +
574 +test_expect_success 'pathspec matching no files produces empty split error' '
575 + test_when_finished "rm -rf repo" &&
576 + git init repo &&
577 + (
578 + cd repo &&
579 + test_commit initial &&
580 + touch bar foo &&
581 + git add . &&
582 + git commit -m split-me &&
583 +
584 + set_fake_editor "first" "second" &&
585 + test_must_fail git history split HEAD -- nonexistent 2>err &&
586 + test_grep "split commit is empty" err
587 + )
588 +'
589 +
590 +test_expect_success 'split with multiple pathspecs' '
591 + test_when_finished "rm -rf repo" &&
592 + git init repo &&
593 + (
594 + cd repo &&
595 + test_commit initial &&
596 + touch a b c d &&
597 + git add . &&
598 + git commit -m split-me &&
599 +
600 + # Only a and c should be offered for splitting.
601 + set_fake_editor "split-ac" "remainder" &&
602 + git history split HEAD -- a c <<-EOF &&
603 + y
604 + y
605 + EOF
606 +
607 + expect_tree_entries HEAD~ <<-EOF &&
608 + a
609 + c
610 + initial.t
611 + EOF
612 +
613 + expect_tree_entries HEAD <<-EOF
614 + a
615 + b
616 + c
617 + d
618 + initial.t
619 + EOF
620 + )
621 +'
622 +
623 +test_expect_success 'split with file mode change' '
624 + test_when_finished "rm -rf repo" &&
625 + git init repo &&
626 + (
627 + cd repo &&
628 + echo content >script &&
629 + git add . &&
630 + git commit -m base &&
631 + test_chmod +x script &&
632 + echo change >script &&
633 + git commit -a -m "mode and content change" &&
634 +
635 + set_fake_editor "mode-change" "content-change" &&
636 + git history split HEAD <<-EOF &&
637 + y
638 + n
639 + EOF
640 +
641 + expect_log <<-EOF
642 + content-change
643 + mode-change
644 + base
645 + EOF
646 + )
647 +'
648 +
649 +test_expect_success 'refuses to create empty split-out commit' '
650 + test_when_finished "rm -rf repo" &&
651 + git init repo &&
652 + (
653 + cd repo &&
654 + test_commit base &&
655 + touch bar foo &&
656 + git add . &&
657 + git commit -m split-me &&
658 +
659 + test_must_fail git history split HEAD 2>err <<-EOF &&
660 + n
661 + n
662 + EOF
663 + test_grep "split commit is empty" err
664 + )
665 +'
666 +
667 +test_expect_success 'hooks are not executed for rewritten commits' '
668 + test_when_finished "rm -rf repo" &&
669 + git init repo &&
670 + (
671 + cd repo &&
672 + touch bar foo &&
673 + git add . &&
674 + git commit -m split-me &&
675 + old_head=$(git rev-parse HEAD) &&
676 +
677 + ORIG_PATH="$(pwd)" &&
678 + export ORIG_PATH &&
679 + for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg
680 + do
681 + write_script .git/hooks/$hook <<-\EOF || exit 1
682 + touch "$ORIG_PATH"/hooks.log
683 + EOF
684 + done &&
685 +
686 + set_fake_editor "first" "second" &&
687 + git history split HEAD <<-EOF &&
688 + y
689 + n
690 + EOF
691 +
692 + expect_log <<-EOF &&
693 + second
694 + first
695 + EOF
696 +
697 + test_path_is_missing hooks.log
698 + )
699 +'
700 +
701 +test_expect_success 'refuses to create empty original commit' '
702 + test_when_finished "rm -rf repo" &&
703 + git init repo &&
704 + (
705 + cd repo &&
706 + touch bar foo &&
707 + git add . &&
708 + git commit -m split-me &&
709 +
710 + test_must_fail git history split HEAD 2>err <<-EOF &&
711 + y
712 + y
713 + EOF
714 + test_grep "split commit tree matches original commit" err
715 + )
716 +'
717 +
718 +test_expect_success 'retains changes in the worktree and index' '
719 + test_when_finished "rm -rf repo" &&
720 + git init repo &&
721 + (
722 + cd repo &&
723 + echo a >a &&
724 + echo b >b &&
725 + git add . &&
726 + git commit -m "initial commit" &&
727 + echo a-modified >a &&
728 + echo b-modified >b &&
729 + git add b &&
730 + set_fake_editor "a-only" "remainder" &&
731 + git history split HEAD <<-EOF &&
732 + y
733 + n
734 + EOF
735 +
736 + expect_tree_entries HEAD~ <<-EOF &&
737 + a
738 + EOF
739 + expect_tree_entries HEAD <<-EOF &&
740 + a
741 + b
742 + EOF
743 +
744 + cat >expect <<-\EOF &&
745 + M a
746 + M b
747 + ?? actual
748 + ?? expect
749 + ?? fake-editor.sh
750 + ?? fake-input
751 + EOF
752 + git status --porcelain >actual &&
753 + test_cmp expect actual
754 + )
755 +'
756 +
757 +test_done