* [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit
@ 2026-06-14 19:25 Harald Nordgren via GitGitGadget
2026-06-14 19:25 ` [PATCH 1/2] t3415: remove prepare-commit-msg hook after use Harald Nordgren via GitGitGadget
` (3 more replies)
0 siblings, 4 replies; 8+ messages in thread
From: Harald Nordgren via GitGitGadget @ 2026-06-14 19:25 UTC (permalink / raw)
To: git; +Cc: Harald Nordgren
Adds git rebase --autosquash --fixup [<upstream>] to fold a range of commits
into its oldest one, reusing that commit's message.
Related idea: https://github.com/gitgitgadget/git/issues/1135
Harald Nordgren (2):
t3415: remove prepare-commit-msg hook after use
rebase: add --fixup-all to fold a range
Documentation/git-rebase.adoc | 11 ++++
builtin/rebase.c | 13 ++++-
sequencer.c | 24 +++++++-
sequencer.h | 2 +-
t/t3415-rebase-autosquash.sh | 106 ++++++++++++++++++++++++++++++++++
5 files changed, 152 insertions(+), 4 deletions(-)
base-commit: ea97ad8d017de0c9037451a78008a0fd60abea0c
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2337%2FHaraldNordgren%2Frebase-fixup-fold-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2337/HaraldNordgren/rebase-fixup-fold-v1
Pull-Request: https://github.com/git/git/pull/2337
--
gitgitgadget
^ permalink raw reply [flat|nested] 8+ messages in thread* [PATCH 1/2] t3415: remove prepare-commit-msg hook after use 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 ` Harald Nordgren via GitGitGadget 2026-06-14 19:25 ` [PATCH 2/2] rebase: add --fixup-all to fold a range Harald Nordgren via GitGitGadget ` (2 subsequent siblings) 3 siblings, 0 replies; 8+ messages in thread From: Harald Nordgren via GitGitGadget @ 2026-06-14 19:25 UTC (permalink / raw) To: git; +Cc: Harald Nordgren, Harald Nordgren From: Harald Nordgren <haraldnordgren@gmail.com> The "pick and fixup respect commit.cleanup" test left its prepare-commit-msg hook in place, leaking it into later tests. Remove it with test_when_finished. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> --- t/t3415-rebase-autosquash.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index 5033411a43..8964d1cc88 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -490,6 +490,7 @@ test_expect_success 'pick and fixup respect commit.cleanup' ' git reset --hard base && test_commit --no-tag "fixup! second commit" file1 fixup && test_commit something && + test_when_finished "rm -f .git/hooks/prepare-commit-msg" && write_script .git/hooks/prepare-commit-msg <<-\EOF && printf "\n# Prepared\n" >> "$1" EOF -- gitgitgadget ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 2/2] rebase: add --fixup-all to fold a range 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 ` 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:37 ` [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit Harald Nordgren via GitGitGadget 3 siblings, 0 replies; 8+ messages in thread From: Harald Nordgren via GitGitGadget @ 2026-06-14 19:25 UTC (permalink / raw) To: git; +Cc: Harald Nordgren, Harald Nordgren From: Harald Nordgren <haraldnordgren@gmail.com> Folding a series of commits into one required either an interactive rebase where each commit after the first was hand-edited to "fixup", or a "git reset --soft" to the merge base followed by "git commit --amend". Add "git rebase --autosquash --fixup-all [<upstream>]" to do this directly. It keeps the first commit in the range as a "pick" and turns every later commit into a "fixup", so the whole range collapses into a single commit that reuses the first commit's message. With no <upstream> argument the range is "@{upstream}..HEAD", folding all unpushed commits into one. Fold the commits in their original order, so that any fixup!/squash! commits already present in the range are folded in as well. Allow the flag only together with --autosquash, and reject --rebase-merges since a merge commit cannot be folded into another commit. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> --- Documentation/git-rebase.adoc | 11 ++++ builtin/rebase.c | 13 ++++- sequencer.c | 24 +++++++- sequencer.h | 2 +- t/t3415-rebase-autosquash.sh | 105 ++++++++++++++++++++++++++++++++++ 5 files changed, 151 insertions(+), 4 deletions(-) diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc index f6c22d1598..d1a3e4ef64 100644 --- a/Documentation/git-rebase.adoc +++ b/Documentation/git-rebase.adoc @@ -602,6 +602,16 @@ option can be used to override that setting. + See also INCOMPATIBLE OPTIONS below. +--fixup-all:: + Valid only when used with `--autosquash`. Keep the first commit in + the range as a `pick` and change every later commit to a `fixup`, so + the whole range is folded into a single commit that reuses the first + commit's message. With no `<upstream>` argument this folds all commits + since `@{upstream}` into one. The commits are folded in their original + order, so any `fixup!`/`squash!` commits already in the range are folded + in as well. Cannot be combined with `--rebase-merges`, as a merge + commit cannot be folded into another commit. + --autostash:: --no-autostash:: Automatically create a temporary stash entry before the operation @@ -652,6 +662,7 @@ are incompatible with the following options: * --strategy * --strategy-option * --autosquash + * --fixup-all * --rebase-merges * --interactive * --exec diff --git a/builtin/rebase.c b/builtin/rebase.c index fa4f5d9306..a363fbc1f2 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -118,6 +118,7 @@ struct rebase_options { int allow_rerere_autoupdate; int keep_empty; int autosquash; + int fixup_all; char *gpg_sign_opt; int autostash; int committer_date_is_author_date; @@ -329,7 +330,8 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags) ret = complete_action(the_repository, &replay, flags, shortrevisions, opts->onto_name, opts->onto, &opts->orig_head->object.oid, &opts->exec, - opts->autosquash, opts->update_refs, &todo_list); + opts->autosquash, opts->fixup_all, opts->update_refs, + &todo_list); cleanup: replay_opts_release(&replay); @@ -1205,6 +1207,8 @@ int cmd_rebase(int argc, OPT_BOOL(0, "autosquash", &options.autosquash, N_("move commits that begin with " "squash!/fixup! under -i")), + OPT_BOOL(0, "fixup-all", &options.fixup_all, + N_("fold all commits in the range into the first one")), OPT_BOOL(0, "update-refs", &options.update_refs, N_("update branches that point to commits " "that are being rebased")), @@ -1594,6 +1598,13 @@ int cmd_rebase(int argc, options.rebase_merges = (options.rebase_merges >= 0) ? options.rebase_merges : ((options.config_rebase_merges >= 0) ? options.config_rebase_merges : 0); + if (options.fixup_all && options.autosquash != 1) + die(_("--fixup-all requires --autosquash")); + + if (options.fixup_all && options.rebase_merges) + die(_("options '%s' and '%s' cannot be used together"), + "--fixup-all", "--rebase-merges"); + if (options.autosquash == 1) { imply_merge(&options, "--autosquash"); } else if (options.autosquash == -1) { diff --git a/sequencer.c b/sequencer.c index 57855b0066..eeaf6226fe 100644 --- a/sequencer.c +++ b/sequencer.c @@ -6554,11 +6554,29 @@ static int todo_list_add_update_ref_commands(struct todo_list *todo_list) return 0; } +static void todo_list_fixup_all_but_first(struct todo_list *todo_list) +{ + int i, seen_first = 0; + + for (i = 0; i < todo_list->nr; i++) { + struct todo_item *item = todo_list->items + i; + + if (!item->commit || item->command == TODO_DROP) + continue; + if (!seen_first) { + seen_first = 1; + item->command = TODO_PICK; + continue; + } + item->command = TODO_FIXUP; + } +} + int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags, const char *shortrevisions, const char *onto_name, struct commit *onto, const struct object_id *orig_head, struct string_list *commands, unsigned autosquash, - unsigned update_refs, + unsigned fixup_all, unsigned update_refs, struct todo_list *todo_list) { char shortonto[GIT_MAX_HEXSZ + 1]; @@ -6581,7 +6599,9 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla if (update_refs && todo_list_add_update_ref_commands(todo_list)) return -1; - if (autosquash && todo_list_rearrange_squash(todo_list)) + if (fixup_all) + todo_list_fixup_all_but_first(todo_list); + else if (autosquash && todo_list_rearrange_squash(todo_list)) return -1; if (commands->nr) diff --git a/sequencer.h b/sequencer.h index 3164bd437d..9bb6b42c94 100644 --- a/sequencer.h +++ b/sequencer.h @@ -196,7 +196,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla const char *shortrevisions, const char *onto_name, struct commit *onto, const struct object_id *orig_head, struct string_list *commands, unsigned autosquash, - unsigned update_refs, + unsigned fixup_all, unsigned update_refs, struct todo_list *todo_list); int todo_list_rearrange_squash(struct todo_list *todo_list); diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index 8964d1cc88..21d4159ebd 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -511,4 +511,109 @@ test_expect_success 'pick and fixup respect commit.cleanup' ' test_commit_message HEAD -m "something" ' +test_expect_success '--fixup-all folds the range into the first commit' ' + git reset --hard base && + test_commit --no-tag fold1 file_fold a && + test_commit --no-tag fold2 file_fold b && + test_commit --no-tag fold3 file_fold c && + git rebase --autosquash --fixup-all HEAD~3 && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "fold1" && + echo c >expect && + test_cmp expect file_fold +' + +test_expect_success '--fixup-all folds smoothly when a fixup! commit is in the series' ' + git reset --hard base && + test_commit --no-tag foldA file_fold a && + test_commit --no-tag foldB file_fold b && + git commit --allow-empty --fixup HEAD~1 && + git rebase --autosquash --fixup-all HEAD~3 && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "foldA" && + echo b >expect && + test_cmp expect file_fold +' + +test_expect_success '--fixup-all picks the first commit even if it is a fixup!' ' + git reset --hard base && + test_commit --no-tag fixupbase file_fix a && + git commit --allow-empty --fixup HEAD && + test_commit --no-tag fixuptail file_fix b && + git rebase --autosquash --fixup-all HEAD~3 && + test_cmp_rev base HEAD~1 && + echo b >expect && + test_cmp expect file_fix +' + +test_expect_success '--fixup-all with a single commit in range is a no-op' ' + git reset --hard base && + test_commit --no-tag solo file_solo a && + git rev-parse HEAD >expect && + git rebase --autosquash --fixup-all HEAD~1 && + git rev-parse HEAD >actual && + test_cmp expect actual +' + +test_expect_success '--fixup-all with an empty range succeeds' ' + git reset --hard base && + git rebase --autosquash --fixup-all HEAD && + test_cmp_rev base HEAD +' + +test_expect_success '--fixup-all skips a dropped commit in the range' ' + git reset --hard base && + test_commit --no-tag fixdrop1 file_drop a && + git commit --allow-empty -m "empty in the middle" && + test_commit --no-tag fixdrop3 file_drop b && + git rebase --autosquash --empty=drop --fixup-all HEAD~3 && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "fixdrop1" && + echo b >expect && + test_cmp expect file_drop +' + +test_expect_success '--fixup-all folds a merge commit in the middle of the range' ' + git reset --hard base && + test_commit --no-tag mid-first && + git checkout -b mid-side && + test_commit --no-tag mid-merged && + git checkout - && + git merge --no-ff -m "merge mid-side" mid-side && + test_commit --no-tag mid-last && + git rebase --autosquash --fixup-all base && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "mid-first" && + test_path_is_file mid-merged.t +' + +test_expect_success '--fixup-all keeps the first flattened commit when a merge sorts first' ' + git reset --hard base && + git checkout -b head-side && + test_commit --no-tag head-merged && + git checkout - && + git merge --no-ff -m "merge head-side" head-side && + test_commit --no-tag head-last && + git rebase --autosquash --fixup-all base && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "head-merged" && + test_path_is_file head-merged.t +' + +test_expect_success '--fixup-all requires --autosquash' ' + git reset --hard base && + test_must_fail git rebase --fixup-all HEAD~1 2>err && + test_grep "fixup-all requires --autosquash" err && + test_must_fail git rebase --no-autosquash --fixup-all HEAD~1 2>err && + test_grep "fixup-all requires --autosquash" err +' + +test_expect_success '--fixup-all and --rebase-merges cannot be combined' ' + git reset --hard base && + test_must_fail git rebase --autosquash --rebase-merges \ + --fixup-all HEAD~1 2>err && + test_grep "cannot be used together" err && + test_path_is_missing .git/rebase-merge +' + test_done -- gitgitgadget ^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit 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 ` Junio C Hamano 2026-06-15 8:18 ` 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 3 siblings, 1 reply; 8+ messages in thread From: Junio C Hamano @ 2026-06-15 2:01 UTC (permalink / raw) To: Harald Nordgren via GitGitGadget; +Cc: git, Harald Nordgren "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes: > Adds git rebase --autosquash --fixup [<upstream>] to fold a range of commits > into its oldest one, reusing that commit's message. [2/2] seems to add "--fixup-all" but I agree with the "related idea" that naming it and modelling it after "merge --squash" would be easier to understand. > Related idea: https://github.com/gitgitgadget/git/issues/1135 I also wonder if we can do something like this without adding any new option or command. E.g., if you have four patch series, where the initial implementation HEAD~3 is followed by "oops it was still wrong" fix-up HEAD~2, HEAD~1 and HEAD, then git reset --soft HEAD~3 && git commit --amend --no-edit is what the user wants to do, no? ^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit 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 0 siblings, 0 replies; 8+ messages in thread From: Harald Nordgren @ 2026-06-15 8:18 UTC (permalink / raw) To: Junio C Hamano; +Cc: Harald Nordgren via GitGitGadget, git > > Adds git rebase --autosquash --fixup [<upstream>] to fold a range of commits > > into its oldest one, reusing that commit's message. > > [2/2] seems to add "--fixup-all" but I agree with the "related idea" > that naming it and modelling it after "merge --squash" would be > easier to understand. Sounds reasonable. > I also wonder if we can do something like this without adding any > new option or command. E.g., if you have four patch series, where > the initial implementation HEAD~3 is followed by "oops it was still > wrong" fix-up HEAD~2, HEAD~1 and HEAD, then > > git reset --soft HEAD~3 && git commit --amend --no-edit > > is what the user wants to do, no? I don't think it's enough. First of all the user has to know the N for HEAD~N, and then 'git reset --soft HEAD~N && git commit --amend --no-edit' is still quite ugly. Harald ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit 2026-06-14 19:25 [PATCH 0/2] rebase: add --fixup to fold a range into its oldest commit Harald Nordgren via GitGitGadget ` (2 preceding siblings ...) 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:37 ` 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 3 siblings, 2 replies; 8+ messages in thread From: Harald Nordgren via GitGitGadget @ 2026-06-15 8:37 UTC (permalink / raw) To: git; +Cc: Harald Nordgren Rename to rebase --squash. Harald Nordgren (2): t3415: remove prepare-commit-msg hook after use rebase: add --squash to fold a range Documentation/git-rebase.adoc | 11 ++++ builtin/rebase.c | 16 ++++- sequencer.c | 24 ++++++- sequencer.h | 2 +- t/t3415-rebase-autosquash.sh | 118 ++++++++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 5 deletions(-) base-commit: ea97ad8d017de0c9037451a78008a0fd60abea0c Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2337%2FHaraldNordgren%2Frebase-fixup-fold-v2 Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2337/HaraldNordgren/rebase-fixup-fold-v2 Pull-Request: https://github.com/git/git/pull/2337 Range-diff vs v1: 1: c55b9cd6f7 = 1: c55b9cd6f7 t3415: remove prepare-commit-msg hook after use 2: bd1bc62aa8 ! 2: 22d4276ff5 rebase: add --fixup-all to fold a range @@ Metadata Author: Harald Nordgren <haraldnordgren@gmail.com> ## Commit message ## - rebase: add --fixup-all to fold a range + rebase: add --squash to fold a range Folding a series of commits into one required either an interactive rebase where each commit after the first was hand-edited to "fixup", or a "git reset --soft" to the merge base followed by "git commit --amend". - Add "git rebase --autosquash --fixup-all [<upstream>]" to do this - directly. It keeps the first commit in the range as a "pick" and turns - every later commit into a "fixup", so the whole range collapses into a - single commit that reuses the first commit's message. With no <upstream> - argument the range is "@{upstream}..HEAD", folding all unpushed commits - into one. + Add "git rebase --squash [<upstream>]" to do this directly. It keeps + the first commit in the range as a "pick" and turns every later commit + into a "fixup", so the whole range collapses into a single commit that + reuses the first commit's message. With no <upstream> argument the range + is "@{upstream}..HEAD", folding all unpushed commits into one. - Fold the commits in their original order, so that any fixup!/squash! - commits already present in the range are folded in as well. Allow the - flag only together with --autosquash, and reject --rebase-merges since a - merge commit cannot be folded into another commit. + The option implies the merge backend, so it works on its own without + --autosquash. Fold the commits in their original order, so that any + fixup!/squash! commits already present in the range are folded in as + well. Reject --rebase-merges since a merge commit cannot be folded into + another commit. + Inspired-by: Sergey Chernov <serega.morph@gmail.com> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> ## Documentation/git-rebase.adoc ## @@ Documentation/git-rebase.adoc: option can be used to override that setting. + See also INCOMPATIBLE OPTIONS below. -+--fixup-all:: -+ Valid only when used with `--autosquash`. Keep the first commit in -+ the range as a `pick` and change every later commit to a `fixup`, so -+ the whole range is folded into a single commit that reuses the first -+ commit's message. With no `<upstream>` argument this folds all commits -+ since `@{upstream}` into one. The commits are folded in their original -+ order, so any `fixup!`/`squash!` commits already in the range are folded -+ in as well. Cannot be combined with `--rebase-merges`, as a merge -+ commit cannot be folded into another commit. ++--squash:: ++ Keep the first commit in the range as a `pick` and change every later ++ commit to a `fixup`, so the whole range is folded into a single commit ++ that reuses the first commit's message. With no `<upstream>` argument ++ this folds all commits since `@{upstream}` into one. The commits are ++ folded in their original order, so any `fixup!`/`squash!` commits ++ already in the range are folded in as well. Cannot be combined with ++ `--rebase-merges`, as a merge commit cannot be folded into another ++ commit. + --autostash:: --no-autostash:: @@ Documentation/git-rebase.adoc: are incompatible with the following options: * --strategy * --strategy-option * --autosquash -+ * --fixup-all ++ * --squash * --rebase-merges * --interactive * --exec @@ builtin/rebase.c: struct rebase_options { int allow_rerere_autoupdate; int keep_empty; int autosquash; -+ int fixup_all; ++ int squash; char *gpg_sign_opt; int autostash; int committer_date_is_author_date; @@ builtin/rebase.c: static int do_interactive_rebase(struct rebase_options *opts, shortrevisions, opts->onto_name, opts->onto, &opts->orig_head->object.oid, &opts->exec, - opts->autosquash, opts->update_refs, &todo_list); -+ opts->autosquash, opts->fixup_all, opts->update_refs, ++ opts->autosquash, opts->squash, opts->update_refs, + &todo_list); cleanup: @@ builtin/rebase.c: int cmd_rebase(int argc, OPT_BOOL(0, "autosquash", &options.autosquash, N_("move commits that begin with " "squash!/fixup! under -i")), -+ OPT_BOOL(0, "fixup-all", &options.fixup_all, ++ OPT_BOOL(0, "squash", &options.squash, + N_("fold all commits in the range into the first one")), OPT_BOOL(0, "update-refs", &options.update_refs, N_("update branches that point to commits " "that are being rebased")), +@@ builtin/rebase.c: int cmd_rebase(int argc, + if ((options.flags & REBASE_INTERACTIVE_EXPLICIT) || + (options.action != ACTION_NONE) || + (options.exec.nr > 0) || +- options.autosquash == 1) { ++ options.autosquash == 1 || ++ options.squash) { + allow_preemptive_ff = 0; + } + if (options.committer_date_is_author_date || options.ignore_date) @@ builtin/rebase.c: int cmd_rebase(int argc, options.rebase_merges = (options.rebase_merges >= 0) ? options.rebase_merges : ((options.config_rebase_merges >= 0) ? options.config_rebase_merges : 0); -+ if (options.fixup_all && options.autosquash != 1) -+ die(_("--fixup-all requires --autosquash")); -+ -+ if (options.fixup_all && options.rebase_merges) ++ if (options.squash && options.rebase_merges) + die(_("options '%s' and '%s' cannot be used together"), -+ "--fixup-all", "--rebase-merges"); ++ "--squash", "--rebase-merges"); ++ ++ if (options.squash) ++ imply_merge(&options, "--squash"); + if (options.autosquash == 1) { imply_merge(&options, "--autosquash"); @@ sequencer.c: static int todo_list_add_update_ref_commands(struct todo_list *todo struct commit *onto, const struct object_id *orig_head, struct string_list *commands, unsigned autosquash, - unsigned update_refs, -+ unsigned fixup_all, unsigned update_refs, ++ unsigned squash, unsigned update_refs, struct todo_list *todo_list) { char shortonto[GIT_MAX_HEXSZ + 1]; @@ sequencer.c: int complete_action(struct repository *r, struct replay_opts *opts, return -1; - if (autosquash && todo_list_rearrange_squash(todo_list)) -+ if (fixup_all) ++ if (squash) + todo_list_fixup_all_but_first(todo_list); + else if (autosquash && todo_list_rearrange_squash(todo_list)) return -1; @@ sequencer.h: int complete_action(struct repository *r, struct replay_opts *opts, struct commit *onto, const struct object_id *orig_head, struct string_list *commands, unsigned autosquash, - unsigned update_refs, -+ unsigned fixup_all, unsigned update_refs, ++ unsigned squash, unsigned update_refs, struct todo_list *todo_list); int todo_list_rearrange_squash(struct todo_list *todo_list); @@ t/t3415-rebase-autosquash.sh: test_expect_success 'pick and fixup respect commit test_commit_message HEAD -m "something" ' -+test_expect_success '--fixup-all folds the range into the first commit' ' ++test_expect_success '--squash folds the range into the first commit' ' + git reset --hard base && + test_commit --no-tag fold1 file_fold a && + test_commit --no-tag fold2 file_fold b && + test_commit --no-tag fold3 file_fold c && -+ git rebase --autosquash --fixup-all HEAD~3 && ++ git rebase --squash HEAD~3 && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "fold1" && + echo c >expect && + test_cmp expect file_fold +' + -+test_expect_success '--fixup-all folds smoothly when a fixup! commit is in the series' ' ++test_expect_success '--squash folds smoothly when a fixup! commit is in the series' ' + git reset --hard base && + test_commit --no-tag foldA file_fold a && + test_commit --no-tag foldB file_fold b && + git commit --allow-empty --fixup HEAD~1 && -+ git rebase --autosquash --fixup-all HEAD~3 && ++ git rebase --squash HEAD~3 && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "foldA" && + echo b >expect && + test_cmp expect file_fold +' + -+test_expect_success '--fixup-all picks the first commit even if it is a fixup!' ' ++test_expect_success '--squash picks the first commit even if it is a fixup!' ' + git reset --hard base && + test_commit --no-tag fixupbase file_fix a && + git commit --allow-empty --fixup HEAD && + test_commit --no-tag fixuptail file_fix b && -+ git rebase --autosquash --fixup-all HEAD~3 && ++ git rebase --squash HEAD~3 && + test_cmp_rev base HEAD~1 && + echo b >expect && + test_cmp expect file_fix +' + -+test_expect_success '--fixup-all with a single commit in range is a no-op' ' ++test_expect_success '--squash with a single commit in range is a no-op' ' + git reset --hard base && + test_commit --no-tag solo file_solo a && + git rev-parse HEAD >expect && -+ git rebase --autosquash --fixup-all HEAD~1 && ++ git rebase --squash HEAD~1 && + git rev-parse HEAD >actual && + test_cmp expect actual +' + -+test_expect_success '--fixup-all with an empty range succeeds' ' ++test_expect_success '--squash with an empty range succeeds' ' + git reset --hard base && -+ git rebase --autosquash --fixup-all HEAD && ++ git rebase --squash HEAD && + test_cmp_rev base HEAD +' + -+test_expect_success '--fixup-all skips a dropped commit in the range' ' ++test_expect_success '--squash skips a dropped commit in the range' ' + git reset --hard base && + test_commit --no-tag fixdrop1 file_drop a && + git commit --allow-empty -m "empty in the middle" && + test_commit --no-tag fixdrop3 file_drop b && -+ git rebase --autosquash --empty=drop --fixup-all HEAD~3 && ++ git rebase --squash --empty=drop HEAD~3 && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "fixdrop1" && + echo b >expect && + test_cmp expect file_drop +' + -+test_expect_success '--fixup-all folds a merge commit in the middle of the range' ' ++test_expect_success '--squash folds a merge commit in the middle of the range' ' + git reset --hard base && + test_commit --no-tag mid-first && + git checkout -b mid-side && @@ t/t3415-rebase-autosquash.sh: test_expect_success 'pick and fixup respect commit + git checkout - && + git merge --no-ff -m "merge mid-side" mid-side && + test_commit --no-tag mid-last && -+ git rebase --autosquash --fixup-all base && ++ git rebase --squash base && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "mid-first" && + test_path_is_file mid-merged.t +' + -+test_expect_success '--fixup-all keeps the first flattened commit when a merge sorts first' ' ++test_expect_success '--squash keeps the first flattened commit when a merge sorts first' ' + git reset --hard base && + git checkout -b head-side && + test_commit --no-tag head-merged && + git checkout - && + git merge --no-ff -m "merge head-side" head-side && + test_commit --no-tag head-last && -+ git rebase --autosquash --fixup-all base && ++ git rebase --squash base && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "head-merged" && + test_path_is_file head-merged.t +' + -+test_expect_success '--fixup-all requires --autosquash' ' ++test_expect_success '--squash takes precedence over --autosquash' ' + git reset --hard base && -+ test_must_fail git rebase --fixup-all HEAD~1 2>err && -+ test_grep "fixup-all requires --autosquash" err && -+ test_must_fail git rebase --no-autosquash --fixup-all HEAD~1 2>err && -+ test_grep "fixup-all requires --autosquash" err ++ test_commit --no-tag combo-first && ++ test_commit --no-tag combo-mid && ++ git commit --allow-empty --fixup HEAD~1 && ++ test_commit --no-tag combo-last && ++ git rebase --autosquash --squash base && ++ test_cmp_rev base HEAD~1 && ++ test_commit_message HEAD -m "combo-first" ++' ++ ++test_expect_success '--squash folds the range with rebase.autosquash set' ' ++ test_config rebase.autosquash true && ++ git reset --hard base && ++ test_commit --no-tag cfg-first && ++ test_commit --no-tag cfg-last && ++ git rebase --squash base && ++ test_cmp_rev base HEAD~1 && ++ test_commit_message HEAD -m "cfg-first" +' + -+test_expect_success '--fixup-all and --rebase-merges cannot be combined' ' ++test_expect_success '--squash and --rebase-merges cannot be combined' ' + git reset --hard base && -+ test_must_fail git rebase --autosquash --rebase-merges \ -+ --fixup-all HEAD~1 2>err && ++ test_must_fail git rebase --rebase-merges --squash HEAD~1 2>err && + test_grep "cannot be used together" err && + test_path_is_missing .git/rebase-merge +' -- gitgitgadget ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v2 1/2] t3415: remove prepare-commit-msg hook after use 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 ` Harald Nordgren via GitGitGadget 2026-06-15 8:37 ` [PATCH v2 2/2] rebase: add --squash to fold a range Harald Nordgren via GitGitGadget 1 sibling, 0 replies; 8+ messages in thread From: Harald Nordgren via GitGitGadget @ 2026-06-15 8:37 UTC (permalink / raw) To: git; +Cc: Harald Nordgren, Harald Nordgren From: Harald Nordgren <haraldnordgren@gmail.com> The "pick and fixup respect commit.cleanup" test left its prepare-commit-msg hook in place, leaking it into later tests. Remove it with test_when_finished. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> --- t/t3415-rebase-autosquash.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index 5033411a43..8964d1cc88 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -490,6 +490,7 @@ test_expect_success 'pick and fixup respect commit.cleanup' ' git reset --hard base && test_commit --no-tag "fixup! second commit" file1 fixup && test_commit something && + test_when_finished "rm -f .git/hooks/prepare-commit-msg" && write_script .git/hooks/prepare-commit-msg <<-\EOF && printf "\n# Prepared\n" >> "$1" EOF -- gitgitgadget ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v2 2/2] rebase: add --squash to fold a range 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 ` Harald Nordgren via GitGitGadget 1 sibling, 0 replies; 8+ messages in thread From: Harald Nordgren via GitGitGadget @ 2026-06-15 8:37 UTC (permalink / raw) To: git; +Cc: Harald Nordgren, Harald Nordgren From: Harald Nordgren <haraldnordgren@gmail.com> Folding a series of commits into one required either an interactive rebase where each commit after the first was hand-edited to "fixup", or a "git reset --soft" to the merge base followed by "git commit --amend". Add "git rebase --squash [<upstream>]" to do this directly. It keeps the first commit in the range as a "pick" and turns every later commit into a "fixup", so the whole range collapses into a single commit that reuses the first commit's message. With no <upstream> argument the range is "@{upstream}..HEAD", folding all unpushed commits into one. The option implies the merge backend, so it works on its own without --autosquash. Fold the commits in their original order, so that any fixup!/squash! commits already present in the range are folded in as well. Reject --rebase-merges since a merge commit cannot be folded into another commit. Inspired-by: Sergey Chernov <serega.morph@gmail.com> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> --- Documentation/git-rebase.adoc | 11 ++++ builtin/rebase.c | 16 ++++- sequencer.c | 24 ++++++- sequencer.h | 2 +- t/t3415-rebase-autosquash.sh | 117 ++++++++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 5 deletions(-) diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc index f6c22d1598..4244288148 100644 --- a/Documentation/git-rebase.adoc +++ b/Documentation/git-rebase.adoc @@ -602,6 +602,16 @@ option can be used to override that setting. + See also INCOMPATIBLE OPTIONS below. +--squash:: + Keep the first commit in the range as a `pick` and change every later + commit to a `fixup`, so the whole range is folded into a single commit + that reuses the first commit's message. With no `<upstream>` argument + this folds all commits since `@{upstream}` into one. The commits are + folded in their original order, so any `fixup!`/`squash!` commits + already in the range are folded in as well. Cannot be combined with + `--rebase-merges`, as a merge commit cannot be folded into another + commit. + --autostash:: --no-autostash:: Automatically create a temporary stash entry before the operation @@ -652,6 +662,7 @@ are incompatible with the following options: * --strategy * --strategy-option * --autosquash + * --squash * --rebase-merges * --interactive * --exec diff --git a/builtin/rebase.c b/builtin/rebase.c index fa4f5d9306..2df9f04728 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -118,6 +118,7 @@ struct rebase_options { int allow_rerere_autoupdate; int keep_empty; int autosquash; + int squash; char *gpg_sign_opt; int autostash; int committer_date_is_author_date; @@ -329,7 +330,8 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags) ret = complete_action(the_repository, &replay, flags, shortrevisions, opts->onto_name, opts->onto, &opts->orig_head->object.oid, &opts->exec, - opts->autosquash, opts->update_refs, &todo_list); + opts->autosquash, opts->squash, opts->update_refs, + &todo_list); cleanup: replay_opts_release(&replay); @@ -1205,6 +1207,8 @@ int cmd_rebase(int argc, OPT_BOOL(0, "autosquash", &options.autosquash, N_("move commits that begin with " "squash!/fixup! under -i")), + OPT_BOOL(0, "squash", &options.squash, + N_("fold all commits in the range into the first one")), OPT_BOOL(0, "update-refs", &options.update_refs, N_("update branches that point to commits " "that are being rebased")), @@ -1471,7 +1475,8 @@ int cmd_rebase(int argc, if ((options.flags & REBASE_INTERACTIVE_EXPLICIT) || (options.action != ACTION_NONE) || (options.exec.nr > 0) || - options.autosquash == 1) { + options.autosquash == 1 || + options.squash) { allow_preemptive_ff = 0; } if (options.committer_date_is_author_date || options.ignore_date) @@ -1594,6 +1599,13 @@ int cmd_rebase(int argc, options.rebase_merges = (options.rebase_merges >= 0) ? options.rebase_merges : ((options.config_rebase_merges >= 0) ? options.config_rebase_merges : 0); + if (options.squash && options.rebase_merges) + die(_("options '%s' and '%s' cannot be used together"), + "--squash", "--rebase-merges"); + + if (options.squash) + imply_merge(&options, "--squash"); + if (options.autosquash == 1) { imply_merge(&options, "--autosquash"); } else if (options.autosquash == -1) { diff --git a/sequencer.c b/sequencer.c index 57855b0066..bb42b40796 100644 --- a/sequencer.c +++ b/sequencer.c @@ -6554,11 +6554,29 @@ static int todo_list_add_update_ref_commands(struct todo_list *todo_list) return 0; } +static void todo_list_fixup_all_but_first(struct todo_list *todo_list) +{ + int i, seen_first = 0; + + for (i = 0; i < todo_list->nr; i++) { + struct todo_item *item = todo_list->items + i; + + if (!item->commit || item->command == TODO_DROP) + continue; + if (!seen_first) { + seen_first = 1; + item->command = TODO_PICK; + continue; + } + item->command = TODO_FIXUP; + } +} + int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags, const char *shortrevisions, const char *onto_name, struct commit *onto, const struct object_id *orig_head, struct string_list *commands, unsigned autosquash, - unsigned update_refs, + unsigned squash, unsigned update_refs, struct todo_list *todo_list) { char shortonto[GIT_MAX_HEXSZ + 1]; @@ -6581,7 +6599,9 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla if (update_refs && todo_list_add_update_ref_commands(todo_list)) return -1; - if (autosquash && todo_list_rearrange_squash(todo_list)) + if (squash) + todo_list_fixup_all_but_first(todo_list); + else if (autosquash && todo_list_rearrange_squash(todo_list)) return -1; if (commands->nr) diff --git a/sequencer.h b/sequencer.h index 3164bd437d..1d5a164f02 100644 --- a/sequencer.h +++ b/sequencer.h @@ -196,7 +196,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla const char *shortrevisions, const char *onto_name, struct commit *onto, const struct object_id *orig_head, struct string_list *commands, unsigned autosquash, - unsigned update_refs, + unsigned squash, unsigned update_refs, struct todo_list *todo_list); int todo_list_rearrange_squash(struct todo_list *todo_list); diff --git a/t/t3415-rebase-autosquash.sh b/t/t3415-rebase-autosquash.sh index 8964d1cc88..ce9abe5147 100755 --- a/t/t3415-rebase-autosquash.sh +++ b/t/t3415-rebase-autosquash.sh @@ -511,4 +511,121 @@ test_expect_success 'pick and fixup respect commit.cleanup' ' test_commit_message HEAD -m "something" ' +test_expect_success '--squash folds the range into the first commit' ' + git reset --hard base && + test_commit --no-tag fold1 file_fold a && + test_commit --no-tag fold2 file_fold b && + test_commit --no-tag fold3 file_fold c && + git rebase --squash HEAD~3 && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "fold1" && + echo c >expect && + test_cmp expect file_fold +' + +test_expect_success '--squash folds smoothly when a fixup! commit is in the series' ' + git reset --hard base && + test_commit --no-tag foldA file_fold a && + test_commit --no-tag foldB file_fold b && + git commit --allow-empty --fixup HEAD~1 && + git rebase --squash HEAD~3 && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "foldA" && + echo b >expect && + test_cmp expect file_fold +' + +test_expect_success '--squash picks the first commit even if it is a fixup!' ' + git reset --hard base && + test_commit --no-tag fixupbase file_fix a && + git commit --allow-empty --fixup HEAD && + test_commit --no-tag fixuptail file_fix b && + git rebase --squash HEAD~3 && + test_cmp_rev base HEAD~1 && + echo b >expect && + test_cmp expect file_fix +' + +test_expect_success '--squash with a single commit in range is a no-op' ' + git reset --hard base && + test_commit --no-tag solo file_solo a && + git rev-parse HEAD >expect && + git rebase --squash HEAD~1 && + git rev-parse HEAD >actual && + test_cmp expect actual +' + +test_expect_success '--squash with an empty range succeeds' ' + git reset --hard base && + git rebase --squash HEAD && + test_cmp_rev base HEAD +' + +test_expect_success '--squash skips a dropped commit in the range' ' + git reset --hard base && + test_commit --no-tag fixdrop1 file_drop a && + git commit --allow-empty -m "empty in the middle" && + test_commit --no-tag fixdrop3 file_drop b && + git rebase --squash --empty=drop HEAD~3 && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "fixdrop1" && + echo b >expect && + test_cmp expect file_drop +' + +test_expect_success '--squash folds a merge commit in the middle of the range' ' + git reset --hard base && + test_commit --no-tag mid-first && + git checkout -b mid-side && + test_commit --no-tag mid-merged && + git checkout - && + git merge --no-ff -m "merge mid-side" mid-side && + test_commit --no-tag mid-last && + git rebase --squash base && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "mid-first" && + test_path_is_file mid-merged.t +' + +test_expect_success '--squash keeps the first flattened commit when a merge sorts first' ' + git reset --hard base && + git checkout -b head-side && + test_commit --no-tag head-merged && + git checkout - && + git merge --no-ff -m "merge head-side" head-side && + test_commit --no-tag head-last && + git rebase --squash base && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "head-merged" && + test_path_is_file head-merged.t +' + +test_expect_success '--squash takes precedence over --autosquash' ' + git reset --hard base && + test_commit --no-tag combo-first && + test_commit --no-tag combo-mid && + git commit --allow-empty --fixup HEAD~1 && + test_commit --no-tag combo-last && + git rebase --autosquash --squash base && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "combo-first" +' + +test_expect_success '--squash folds the range with rebase.autosquash set' ' + test_config rebase.autosquash true && + git reset --hard base && + test_commit --no-tag cfg-first && + test_commit --no-tag cfg-last && + git rebase --squash base && + test_cmp_rev base HEAD~1 && + test_commit_message HEAD -m "cfg-first" +' + +test_expect_success '--squash and --rebase-merges cannot be combined' ' + git reset --hard base && + test_must_fail git rebase --rebase-merges --squash HEAD~1 2>err && + test_grep "cannot be used together" err && + test_path_is_missing .git/rebase-merge +' + test_done -- gitgitgadget ^ permalink raw reply related [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-06-15 8:37 UTC | newest] Thread overview: 8+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 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 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
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox