Thread (185 messages) 185 messages, 9 authors, 2026-09-08

[PATCH v15 0/8] history: add squash subcommand to fold a range

flat view
COLD54d

From: Harald Nordgren via GitGitGadget <hidden>
Date: 2026-09-08 20:42:02
Subsystem: documentation, the rest · Maintainers: Jonathan Corbet, Linus Torvalds

Revision v15 (13 revisions archived).

Revisions (13)
Adds git history squash <revision-range> to fold a range of commits.

Changes in v15:

 * Fixed istarts_with() → starts_with() in squash_check_autosquash_subject()
   so fixup! ABCdef (mixed/upper-case hex) is no longer accepted.
 * Fixed a typo in the adjacent comment (squshed → squashed).

Changes in v14:

 * Update commit message trailers.
 * Simplify rev-parse and tr complexity in the tests.

Changes in v13:

 * Split the squash implementation into five patches covering parsing, range
   validation, branch protection, commit creation, and message editing.
 * Print the sorted names of local branches that prevent a squash, with
   clearer advice for --update-refs=head.
 * Isolate --no-edit behavior from the final patch that enables default
   editor-based message combining.

Changes in v12:

 * Incorporated fixups from Phillip:
   * Reworks range validation into a single walk that rejects roots and
     multiple tips while preserving every parent when squashing into a
     merge.
   * Resolves fixup!, squash! and amend! targets directly, preserving
     message intent and safely consolidating related markers.
   * Builds the editor template from the exact selected revisions, including
     exclusions, while retaining --no-edit behavior.
   * Protects descendant local branches while leaving tags and
     remote-tracking refs unchanged.

Changes in v11:

 * Make message editing the default with the autosquash-style template, add
   --no-edit instead of squash-specific --reedit-message.
 * Validate one actual boundary and tip, rejecting root-reaching and
   multi-tip ranges.
 * Protect only interior local branches, leaving tags and remote-tracking
   refs unchanged.
 * Move sequencer preparation before squash and fold the later
   message-editing patch into the feature commit.

Changes in v10:

 * Record the full revision expression in squash reflog.
 * Preserve the boundary-walk invariant when sanitizing rev-list options.
 * Clarify amend! and --reedit-message documentation.

Changes in v9:

 * Use the last amend! targeting the oldest folded commit as the default
   squashed message. Ignore amend! markers targeting later commits while
   selecting that replacement message.
 * Improve tests.

Changes in v8:

 * --reedit-message now builds the same editor template as git rebase -i
   --autosquash: fixup!, squash! and amend! commits are grouped under the
   commit they target instead of shown in commit order, and an amend!
   replaces its target's message.
 * A fixup!, squash! or amend! is refused only when its target is outside
   the range, so several fixups for an in-range commit fold together. A
   range that is entirely markers for one below-range target is combined
   into a single commit, keeping the last amend! message.
 * Merges inside the range are folded when the range has a single base, with
   no dedicated opt-in flag, --ancestry-path ensures only commits descended
   from the base are folded, and a range reaching more than one base is
   rejected.
 * Rev-list options are accepted and sanitized the way git replay does,
   forcing the walk order back with a warning, which also fixes git history
   squash -- --reverse slipping past the previous option check.
 * Kept this as an explicit squash subcommand rather than making
   --reedit-message the default or renaming the command.

Changes in v7:

 * --reedit-message now builds the same editor template git rebase -i shows
   for a squash (a combination of N commits banner with each folded message
   under its own header) and follows autosquash for markers: a fixup!
   message falls out (commented under a will be skipped header), while a
   squash! or amend! keeps its body with only the marker subject commented
   so its remark can be reworded in. Only the message text is affected,
   every commit's changes are always folded in.
 * Reuse git rebase -i's squash-message code: a preparatory sequencer:
   commit extracts the banner, header and marker-comment helpers so both
   rebase and git history squash build the identical template from one
   source.
 * Refuse a range whose oldest commit is a fixup!, squash! or amend!, since
   the marker's target cannot be inside the range.
 * Reorder the squash usage so dashed options come before <revision-range>,
   and spell out HEAD instead of @ in the documentation and examples.
 * Expand the squash commit message and documentation with this overview,
   and scope the merge limitation so it no longer contradicts squash folding
   a single-base interior merge.

Changes in v6:

 * git history squash now accepts multiple revision arguments, read like the
   arguments to git-rev-list, so a compound range such as @~3.. ^topic
   works.
 * The base to reparent onto is now the oldest in-range commit's parent; a
   boundary other than that base means the range has more than one base and
   is rejected. This also fixes the earlier overly-restrictive handling of
   merges and side branches.
 * A single-commit range (e.g. @^!) is rejected with "nothing to squash"
   (this also covers the @^!-style example that previously succeeded
   silently).
 * Commit messages reworded: the squash commit now gives an overview of
   fixup!/squash!/amend! handling, rewording, merge-parent and ref behavior.

Changes in v5:

 * The range walk now uses --ancestry-path, so only commits descended from
   the base are folded; a single revision such as HEAD or HEAD~1 is now
   rejected as "not a <base>..<tip> range" rather than treated as a squash
   down to the root.
 * This adopts the --ancestry-path suggestion; the multi-base rejection is
   unchanged, so a side branch that forked before the base and merged in is
   still refused.
 * Added tests covering more merge topologies: two interior merges, a nested
   merge, an octopus merge, an octopus arm forked before the base, a merge
   among the descendants replayed above the range, and a ref pointing at an
   interior merge commit.

Changes in v4:

 * git history squash now detects when another ref points at a commit inside
   the range being folded and refuses, with an advice.historyUpdateRefs hint
   to use --update-refs=head.
 * A merge inside the range is folded fine as long as the range has a single
   base; a range with merge commit at the tip or base also folds correctly.
   Only a range with more than one base is rejected.

Changes in v3:

 * Moved the feature out of git rebase and into a new git history squash
   <revision-range> subcommand, per the list discussion. git rebase --squash
   is dropped.
 * Takes an arbitrary range (git history squash @~3.., git history squash
   @~5..@~2), folding it into the oldest commit and replaying any
   descendants on top.
 * Implemented as a single tree operation rather than picking each commit,
   so there are no repeated conflict stops (addresses Phillip's efficiency
   point).
 * A merge inside the range is folded fine, only a range with more than one
   base is rejected.
 * --reedit-message seeds the editor with every folded-in message, not just
   the oldest.

Harald Nordgren (8):
  history: extract helper for a commit's parent tree
  history: give commit_tree_ext a message template
  sequencer: share the squash message marker helpers and flags
  history: add skeleton for squash subcommand
  history: validate squash revision ranges
  history: protect branches when squashing a range
  history: create squashed commits without editing
  history: support editing squashed commit messages

 Documentation/config/advice.adoc |   4 +
 Documentation/git-history.adoc   |  59 +-
 advice.c                         |   1 +
 advice.h                         |   1 +
 builtin/history.c                | 720 +++++++++++++++++++++++--
 object.h                         |   1 +
 sequencer.c                      |  70 +--
 sequencer.h                      |  30 ++
 t/meson.build                    |   1 +
 t/t3455-history-squash.sh        | 886 +++++++++++++++++++++++++++++++
 t/t9902-completion.sh            |   1 +
 11 files changed, 1703 insertions(+), 71 deletions(-)
 create mode 100755 t/t3455-history-squash.sh


base-commit: b8242b093d9e941a34460d715e3ce616a34ac3fe
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2337%2FHaraldNordgren%2Frebase-fixup-fold-v15
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2337/HaraldNordgren/rebase-fixup-fold-v15
Pull-Request: https://github.com/git/git/pull/2337

Range-diff vs v14:

 1:  19c5e311a3 = 1:  98a1b756bc history: extract helper for a commit's parent tree
 2:  aadb7a5df4 = 2:  9d99354988 history: give commit_tree_ext a message template
 3:  e1621f656e = 3:  b7a1796838 sequencer: share the squash message marker helpers and flags
 4:  016ae0fd42 ! 4:  8ad1faaa9a history: add skeleton for squash subcommand
     @@ builtin/history.c: int cmd_history(int argc,
       		OPT_END(),
       	};
       
     +
     + ## t/t9902-completion.sh ##
     +@@ t/t9902-completion.sh: test_expect_success 'git history subcommands' '
     + 	fixup Z
     + 	reword Z
     + 	split Z
     ++	squash Z
     + 	EOF
     + 	test_completion "git history --" ""
     + '
 5:  4992d14ce4 = 5:  4389729c83 history: validate squash revision ranges
 6:  b862a5b4c3 = 6:  4d30ff1936 history: protect branches when squashing a range
 7:  03528d3b34 ! 7:  96349a802e history: create squashed commits without editing
     @@ builtin/history.c: out:
      +	}
      +	/*
      +	 * Allow "fixup! <hex object id>", but not "fixup! HEAD^" or
     -+	 * "fixup! main". If the target is not being squshed check the subject
     ++	 * "fixup! main". If the target is not being squashed check the subject
      +	 * to allow "fixup! abc123" and "fixup! <subject of abc123>" to be
      +	 * squashed together.
      +	 */
      +	target = lookup_commit_reference_by_name(s);
     -+	if (target && istarts_with(oid_to_hex(&target->object.oid), s)) {
     ++	if (target && starts_with(oid_to_hex(&target->object.oid), s)) {
      +		unsigned flags =
      +			target->object.flags & (SQUASH_SEEN | SQUASH_AMEND_TARGET);
      +		if (!flags) {
 8:  98aa3922cb = 8:  8da0333646 history: support editing squashed commit messages

-- 
gitgitgadget
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help