From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Phillip Wood <phillip.wood123@gmail.com>,
"D. Ben Knoble" <ben.knoble@gmail.com>,
Patrick Steinhardt <ps@pks.im>, Matt Hunter <m@lfurio.us>,
Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>,
Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v13 0/8] history: add squash subcommand to fold a range
Date: Fri, 07 Aug 2026 07:39:23 +0000 [thread overview]
Message-ID: <pull.2337.v13.git.git.1786088371.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2337.git.git.1781465141.gitgitgadget@gmail.com>
Adds git history squash <revision-range> to fold a range of commits.
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 +++++++++++++++++++++++++++++++
10 files changed, 1702 insertions(+), 71 deletions(-)
create mode 100755 t/t3455-history-squash.sh
base-commit: 2c78326f810173a4f3aefd8021f1e07575412481
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2337%2FHaraldNordgren%2Frebase-fixup-fold-v13
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2337/HaraldNordgren/rebase-fixup-fold-v13
Pull-Request: https://github.com/git/git/pull/2337
Range-diff vs v12:
1: 20e050a0ad = 1: b175bdca54 history: extract helper for a commit's parent tree
2: b99867a0d4 = 2: 965074f9e8 history: give commit_tree_ext a message template
3: 352db997e9 = 3: cdbdd766b2 sequencer: share the squash message marker helpers and flags
-: ---------- > 4: f7d953b081 history: add skeleton for squash subcommand
-: ---------- > 5: dbbf66ba02 history: validate squash revision ranges
-: ---------- > 6: e71a8adfbe history: protect branches when squashing a range
4: 33d3eca6db ! 7: 8b3551d0d4 history: add squash subcommand to fold a range
@@ Metadata
Author: Harald Nordgren <haraldnordgren@gmail.com>
## Commit message ##
- history: add squash subcommand to fold a range
+ history: create squashed commits without editing
- 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".
+ Create one replacement commit from the resolved range when --no-edit is
+ selected. Preserve the authorship and all parents of the oldest commit,
+ use the tip tree, and replay descendants through the existing history
+ rewrite machinery. Record the complete revision expression in the
+ reflog and retain dry-run and update-refs behavior.
- Add "git history squash <revision-range>" to do this directly. It folds
- every selected commit into one, preserving the authorship and parents of
- the oldest commit and taking the tree of the tip, then replays commits
- above the range. By default an editor opens with every folded message in
- the same template used by "git rebase -i --autosquash". With --no-edit,
- the selected message is used without opening an editor.
-
- Resolve fixup!, squash! and amend! subjects directly while walking the
- selected commits. A marker is rejected unless its fixed target is also
- selected, except that a range made up entirely of related markers can be
- consolidated. Without editing, refuse any squash! or amend! whose message
- would be discarded.
-
- Read the range like arguments to "git rev-list" and accept multiple
- revisions and rev-list options. Restore the walk settings required by the
- fold after setup_revisions(), warning when an option changed them. The
- range must name a bottom commit, must not reach a root, and must have one
- tip. Every parent after the oldest commit must be selected or also be a
- parent of the oldest commit. This permits internal merges and preserves
- all parents when the oldest commit is itself a merge.
-
- By default, refuse when a local branch descends from the selected commits
- but cannot remain a descendant of the result. Tags and remote-tracking
- refs are left unchanged. Use --update-refs=head to leave such local
- branches unchanged as well.
+ Resolve fixup!, squash! and amend! subjects while walking the range.
+ Reject markers whose targets are not selected and refuse any no-edit
+ fold that would discard a squash! or amend! message. A range made
+ entirely from related markers can still be consolidated, with the last
+ applicable amend! body supplying the message.
Inspired-by: Sergey Chernov <serega.morph@gmail.com>
Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
- ## Documentation/config/advice.adoc ##
-@@ Documentation/config/advice.adoc: all advice messages.
- forceDeleteBranch::
- Shown when the user tries to delete a not fully merged
- branch without the force option set.
-+ historyUpdateRefs::
-+ Shown when `git history squash` refuses because a ref points
-+ into the range being folded, to tell the user about
-+ `--update-refs=head`.
- ignoredHook::
- Shown when a hook is ignored because the hook is not
- set as executable.
-
## Documentation/git-history.adoc ##
-@@ Documentation/git-history.adoc: git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(
- git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
- git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
- git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
-+git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>
-
- DESCRIPTION
- -----------
@@ Documentation/git-history.adoc: at once.
LIMITATIONS
-----------
@@ Documentation/git-history.adoc: linkgit:gitglossary[7].
+already on `topic`. Rev-list options may also be given, but any that would
+change how the range is walked are overridden with a warning.
++
-+An editor opens pre-filled with the messages of all the folded commits so you
-+can combine them. With `--no-edit`, the oldest commit's message is preserved
-+instead, except that an `amend!` commit targeting it replaces its message.
++With `--no-edit`, the oldest commit's message is preserved, except that an
++`amend!` commit targeting it replaces its message.
++
+The selected commits must form a connected graph with a single tip and must
+not include a root commit. Every parent of a commit after the oldest one must
@@ Documentation/git-history.adoc: linkgit:gitglossary[7].
+the last `amend!` message is used if there is one; a `squash!` or `amend!` is
+otherwise refused if folding it would discard its message.
++
-+The editor template mirrors `git rebase -i --autosquash`: each `fixup!`,
-+`squash!`, or `amend!` is grouped under the commit it targets rather than
-+shown in commit order. A `fixup!` message is dropped (commented out in full),
-+a `squash!` keeps its body with only the marker subject commented, and an
-+`amend!` replaces its target's message, unless a `squash!` folded into that
-+target first, in which case it keeps its body like a `squash!`.
-++
+A local branch descended from a selected commit but not from the range tip
+cannot be rewritten as a descendant of the result, so with the default
+`--update-refs=branches` the command refuses. Rerun with `--update-refs=head`
@@ Documentation/git-history.adoc: linkgit:gitglossary[7].
OPTIONS
-------
-@@ Documentation/git-history.adoc: OPTIONS
- objects will be written into the repository, so applying these printed
- ref updates is generally safe.
-
-+`--edit`::
-+`--no-edit`::
-+ For `squash`, open an editor to combine the messages of the folded commits.
-+ This is the default; use `--no-edit` to keep the selected message without
-+ opening an editor.
-+
- `--reedit-message`::
- Open an editor to modify the target commit's message.
-
-
- ## advice.c ##
-@@ advice.c: static struct {
- [ADVICE_FETCH_SHOW_FORCED_UPDATES] = { "fetchShowForcedUpdates" },
- [ADVICE_FORCE_DELETE_BRANCH] = { "forceDeleteBranch" },
- [ADVICE_GRAFT_FILE_DEPRECATED] = { "graftFileDeprecated" },
-+ [ADVICE_HISTORY_UPDATE_REFS] = { "historyUpdateRefs" },
- [ADVICE_IGNORED_HOOK] = { "ignoredHook" },
- [ADVICE_IMPLICIT_IDENTITY] = { "implicitIdentity" },
- [ADVICE_MERGE_CONFLICT] = { "mergeConflict" },
-
- ## advice.h ##
-@@ advice.h: enum advice_type {
- ADVICE_FETCH_SHOW_FORCED_UPDATES,
- ADVICE_FORCE_DELETE_BRANCH,
- ADVICE_GRAFT_FILE_DEPRECATED,
-+ ADVICE_HISTORY_UPDATE_REFS,
- ADVICE_IGNORED_HOOK,
- ADVICE_IMPLICIT_IDENTITY,
- ADVICE_MERGE_CONFLICT,
## builtin/history.c ##
-@@
- #define USE_THE_REPOSITORY_VARIABLE
-
- #include "builtin.h"
-+#include "advice.h"
- #include "cache-tree.h"
- #include "commit.h"
- #include "commit-reach.h"
-@@
- #include "path.h"
- #include "read-cache.h"
- #include "refs.h"
-+#include "ref-filter.h"
- #include "replay.h"
- #include "reset.h"
- #include "revision.h"
-@@
- N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
- #define GIT_HISTORY_SPLIT_USAGE \
- N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
-+#define GIT_HISTORY_SQUASH_USAGE \
-+ N_("git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>")
-
- static void change_data_free(void *util, const char *str UNUSED)
- {
@@ builtin/history.c: out:
- return ret;
- }
-+/*Remember to update object flag allocation in object.h */
-+#define SQUASH_SEEN (1u << 11)
-+#define SQUASH_TIP (1u << 12)
+ #define SQUASH_SEEN (1u << 11)
+ #define SQUASH_TIP (1u << 12)
+#define SQUASH_AMEND_TARGET (1u << 13)
+
+static bool is_autosquash_subject(const char *s)
@@ builtin/history.c: out:
+ repo_unuse_commit_buffer(repo, commit, buf);
+ return ret;
+}
-+
-+static int build_squash_message(struct repository *repo,
-+ const struct strbuf *todo_buf,
-+ struct strbuf *out);
-+
-+/*
-+ * Resolve a "<base>..<tip>" revision range into the base commit just outside
-+ * the range (which becomes the parent of the squashed commit), the oldest
-+ * commit contained in the range (whose message the squash reuses), and the
-+ * range tip (whose tree becomes the result). A merge inside the range is fine,
-+ * but the range must have a single base and must not reach a root commit.
-+ */
-+static int resolve_squash_range(struct repository *repo,
-+ bool update_branches,
-+ bool edit_message,
-+ int argc, const char **argv,
-+ struct commit **oldest_out,
+
+ static int setup_squash_revisions(struct repository *repo,
+ int argc, const char **argv,
+@@ builtin/history.c: static int resolve_squash_range(struct repository *repo,
+ bool update_branches,
+ int argc, const char **argv,
+ struct commit **oldest_out,
+- struct commit **tip_out)
+ struct commit **tip_out,
+ char **message_out)
-+{
-+ struct rev_info revs;
+ {
+ struct rev_info revs;
+ struct subject_data subject_data = SUBJECT_DATA_INIT;
-+ struct commit *commit, *oldest = NULL, *tip = NULL;
-+ struct strbuf todo_buf = STRBUF_INIT;
-+ size_t i;
-+ int ret, tip_count = 0;
-+ struct ref_filter filter = REF_FILTER_INIT;
-+ struct ref_array refs = { 0 };
-+
-+ repo_init_revisions(repo, &revs, NULL);
-+ subject_data.edit_message = edit_message;
-+ revs.reverse = 1;
-+ revs.topo_order = 1;
-+ revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
-+ revs.simplify_history = 0;
-+ revs.ancestry_path = 1;
-+ revs.limited = 1;
-+ revs.ancestry_path_implicit_bottoms = 1;
-+
-+ argc = setup_revisions(argc, argv, &revs, NULL);
-+ if (argc > 1) {
-+ ret = error(_("unrecognized argument: %s"), argv[1]);
-+ goto out;
-+ }
-+
-+ if (revs.reverse != 1 || revs.topo_order != 1 ||
-+ revs.sort_order != REV_SORT_IN_GRAPH_ORDER ||
-+ revs.simplify_history != 0 || revs.boundary == 1 ||
-+ revs.ancestry_path != 1 || revs.limited != 1 ||
-+ revs.ancestry_path_implicit_bottoms != 1) {
-+ warning(_("ignoring rev-list options that would change how the "
-+ "range is walked"));
-+ revs.reverse = 1;
-+ revs.topo_order = 1;
-+ revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
-+ revs.simplify_history = 0;
-+ revs.boundary = 0;
-+ revs.ancestry_path = 1;
-+ revs.limited = 1;
-+ revs.ancestry_path_implicit_bottoms = 1;
-+ }
-+
-+ /*
-+ * A squash needs a base to reparent onto, so the range has to exclude
-+ * something, as in "<base>..<tip>". A revision range with no such
-+ * bottom commit cannot be squashed.
-+ */
-+ for (i = 0; i < revs.cmdline.nr; i++)
-+ if (revs.cmdline.rev[i].flags & BOTTOM)
-+ break;
-+ if (i == revs.cmdline.nr) {
-+ ret = error(_("not a '<base>..<tip>' revision range"));
-+ goto out;
-+ }
-+
-+ if (prepare_revision_walk(&revs) < 0) {
-+ ret = error(_("error preparing revisions"));
-+ goto out;
-+ }
-+ while ((commit = get_revision(&revs))) {
-+ struct commit_list *p;
-+
-+ if (edit_message)
-+ strbuf_addf(&todo_buf, "pick %s\n",
-+ oid_to_hex(&commit->object.oid));
-+
-+ if (!commit->parents) {
-+ ret = error(_("cannot squash down to root commit"));
-+ goto out;
-+ }
-+ for (p = commit->parents; oldest && p; p = p->next) {
-+ struct commit_list *q;
-+ struct object *o;
-+ bool seen;
-+
-+ if (repo_parse_commit(repo, p->item)) {
-+ ret = error(_("cannot parse commit"));
-+ goto out;
-+ }
-+ o = &p->item->object;
-+ seen = o->flags & SQUASH_SEEN;
-+ /*
-+ * Allow parents that match the parents of the
-+ * squashed commit.
-+ */
-+ for (q = oldest->parents; !seen && q; q = q->next) {
-+ if (p->item == q->item) {
-+ seen = true;
-+ commit_list_insert(commit, &filter.with_commit);
-+ }
-+ }
-+ if (!seen) {
-+ ret = error(_("parent %s of commit %s is "
-+ "outside the revision range"),
-+ repo_find_unique_abbrev(repo, &o->oid,
-+ DEFAULT_ABBREV),
-+ repo_find_unique_abbrev(repo, &commit->object.oid,
-+ DEFAULT_ABBREV));
-+ goto out;
-+ }
-+ if (o->flags & SQUASH_TIP) {
-+ tip_count--;
-+ o->flags &= ~SQUASH_TIP;
-+ }
-+ }
-+ if (!oldest) {
-+ commit_list_insert(commit, &filter.with_commit);
-+ oldest = commit;
-+ }
+ struct commit *commit, *oldest = NULL, *tip = NULL;
+ int ret, tip_count = 0;
+ bool walk_started = false;
+@@ builtin/history.c: static int resolve_squash_range(struct repository *repo,
+ commit_list_insert(commit, &filter.with_commit);
+ oldest = commit;
+ }
+ if (squash_check_subject(repo, commit, &subject_data)) {
+ ret = -1;
+ goto out;
+ }
-+ tip = commit;
-+ tip->object.flags |= SQUASH_SEEN | SQUASH_TIP;
-+ tip_count++;
-+ }
-+ clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP | SQUASH_AMEND_TARGET);
-+ reset_revision_walk();
-+ if (!tip_count) {
-+ ret = error(_("the revision range is empty"));
-+ goto out;
-+ } else if (tip_count != 1) {
-+ ret = error(_("the revision range contains more than one tip "
-+ "commit"));
-+ goto out;
-+ } else if (oldest == tip) {
-+ ret = error(_("the revision range holds a single commit; "
-+ "nothing to squash"));
-+ goto out;
-+ } else if (!oldest->parents) {
-+ BUG("an in-range commit must have a parent");
-+ }
-+ commit_list_insert(tip, &filter.no_commit);
-+ filter.kind = FILTER_REFS_BRANCHES;
-+ if (update_branches &&
-+ filter_refs(&refs, &filter, filter.kind)) {
-+ ret = error(_("could not filter refs"));
-+ goto out;
-+ }
-+ if (refs.nr) {
-+ /*
-+ * TODO: list the branches and also check HEADS from other worktrees
-+ */
-+ ret = error(_("a branch points to a commit that is being squashed"));
-+ advise_if_enabled(ADVICE_HISTORY_UPDATE_REFS,
-+ _("Use --update-refs=head to rewrite only "
-+ "the current branch and leave such refs "
-+ "untouched."));
-+ goto out;
-+ }
-+ if (edit_message) {
-+ strbuf_reset(&subject_data.squash_message);
-+ ret = build_squash_message(repo, &todo_buf,
-+ &subject_data.squash_message);
-+ if (ret < 0)
-+ goto out;
-+ }
-+ *oldest_out = oldest;
-+ *tip_out = tip;
+ tip = commit;
+ tip->object.flags |= SQUASH_SEEN | SQUASH_TIP;
+ tip_count++;
+@@ builtin/history.c: static int resolve_squash_range(struct repository *repo,
+
+ *oldest_out = oldest;
+ *tip_out = tip;
+ *message_out = strbuf_detach(&subject_data.squash_message, NULL);
-+ ret = 0;
-+
-+out:
-+ strbuf_release(&todo_buf);
+ ret = 0;
+
+ out:
+- clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP);
++ clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP |
++ SQUASH_AMEND_TARGET);
+ if (walk_started)
+ reset_revision_walk();
+ subject_data_clear(&subject_data);
-+ release_revisions(&revs);
-+ ref_filter_clear(&filter);
-+ ref_array_clear(&refs);
-+ return ret;
-+}
-+
-+static bool amend_replaces_target(struct todo_list *todo, int target)
-+{
-+ int i;
-+
-+ for (i = target + 1; i < todo->nr &&
-+ todo->items[i].command != TODO_PICK; i++) {
-+ if (todo->items[i].command == TODO_SQUASH)
-+ return false;
-+ if (todo->items[i].flags & TODO_REPLACE_FIXUP_MSG)
-+ return true;
-+ }
-+ return false;
-+}
-+
-+static int build_squash_message(struct repository *repo,
-+ const struct strbuf *todo_buf,
-+ struct strbuf *out)
-+{
-+ struct todo_list todo = TODO_LIST_INIT;
-+ struct replay_opts opts = REPLAY_OPTS_INIT;
-+ int i, nr_commits, ret;
-+
-+ if (todo_list_parse_insn_buffer(repo, &opts, todo_buf->buf, &todo) < 0 ||
-+ todo_list_rearrange_squash(&todo) < 0) {
-+ ret = error(_("could not prepare the squash message"));
-+ goto out;
-+ }
-+
-+ nr_commits = todo.nr;
-+ for (i = 0; i < nr_commits; i++) {
-+ struct todo_item *item = &todo.items[i];
-+ const char *message, *body;
-+ size_t commented_len;
-+ bool skip, squashing;
-+
-+ squashing = item->command == TODO_SQUASH ||
-+ (item->flags & TODO_REPLACE_FIXUP_MSG);
-+ if (item->command == TODO_PICK)
-+ skip = amend_replaces_target(&todo, i);
-+ else
-+ skip = !squashing;
-+
-+ message = repo_logmsg_reencode(repo, item->commit, NULL, NULL);
-+ find_commit_subject(message, &body);
-+
-+ if (skip)
-+ commented_len = strlen(body);
-+ else if (squashing)
-+ commented_len = squash_subject_comment_len(body, 1);
-+ else
-+ commented_len = 0;
-+
-+ if (!i)
-+ add_squash_combination_header(out, nr_commits);
-+ strbuf_addch(out, '\n');
-+ add_squash_message_header(out, i + 1, skip);
-+ strbuf_addstr(out, "\n\n");
-+ strbuf_add_commented_lines(out, body, commented_len, comment_line_str);
-+ strbuf_addstr(out, body + commented_len);
-+ strbuf_complete_line(out);
-+
-+ repo_unuse_commit_buffer(repo, item->commit, message);
-+ }
-+
-+ ret = 0;
-+
-+out:
-+ todo_list_release(&todo);
-+ replay_opts_release(&opts);
-+ return ret;
-+}
-+
-+static int cmd_history_squash(int argc,
-+ const char **argv,
-+ const char *prefix,
-+ struct repository *repo)
-+{
-+ const char * const usage[] = {
-+ GIT_HISTORY_SQUASH_USAGE,
-+ NULL,
-+ };
-+ enum ref_action action = REF_ACTION_DEFAULT;
-+ int dry_run = 0;
-+ int edit = 1;
-+ struct option options[] = {
-+ OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
-+ N_("control which refs should be updated"),
-+ PARSE_OPT_NONEG, parse_ref_action),
-+ OPT_BOOL('n', "dry-run", &dry_run,
-+ N_("perform a dry-run without updating any refs")),
-+ OPT_BOOL('e', "edit", &edit,
-+ N_("edit the commit message")),
-+ OPT_END(),
-+ };
+ release_revisions(&revs);
+ ref_filter_clear(&filter);
+ ref_array_clear(&refs);
+@@ builtin/history.c: static int cmd_history_squash(int argc,
+ N_("edit the commit message")),
+ OPT_END(),
+ };
+- struct commit *oldest, *tip;
+ struct strbuf reflog_msg = STRBUF_INIT;
+ struct commit *oldest, *tip, *rewritten;
+ const struct object_id *base_tree_oid, *tip_tree_oid;
+ char *message_template = NULL;
+ struct rev_info revs = { 0 };
-+ int ret;
-+
-+ argc = parse_options(argc, argv, prefix, options, usage,
-+ PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage,
+ PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);
+- if (argc < 2)
+- return error(_("command expects a revision range"));
+ if (argc < 2) {
+ ret = error(_("command expects a revision range"));
+ goto out;
+ }
-+ repo_config(repo, git_default_config, NULL);
-+
-+ if (action == REF_ACTION_DEFAULT)
-+ action = REF_ACTION_BRANCHES;
+ repo_config(repo, git_default_config, NULL);
+
+ if (action == REF_ACTION_DEFAULT)
+ action = REF_ACTION_BRANCHES;
+
+ strbuf_addstr(&reflog_msg, "squash: updating ");
+ strbuf_join_argv(&reflog_msg, argc - 1, argv + 1, ' ');
+
-+ ret = resolve_squash_range(repo, action == REF_ACTION_BRANCHES,
-+ edit,
+ ret = resolve_squash_range(repo, action == REF_ACTION_BRANCHES,
+- argc, argv, &oldest, &tip);
+ argc, argv, &oldest, &tip,
+ &message_template);
-+ if (ret < 0)
+ if (ret < 0)
+- return ret;
++ goto out;
++ if (edit) {
++ ret = error(_("message editing is not supported yet; use '--no-edit'"));
+ goto out;
++ }
+
+ ret = setup_revwalk(repo, action, tip, &revs);
+ if (ret < 0)
@@ builtin/history.c: out:
+
+ ret = commit_tree_ext(repo, "squash", oldest, message_template,
+ oldest->parents, base_tree_oid, tip_tree_oid,
-+ &rewritten,
-+ edit ? COMMIT_TREE_EDIT_MESSAGE : 0);
++ &rewritten, 0);
+ if (ret < 0) {
+ ret = error(_("failed writing squashed commit"));
+ goto out;
@@ builtin/history.c: out:
+ ret = error(_("failed replaying descendants"));
+ goto out;
+ }
-+
+
+- return error(_("squashing commits is not implemented yet"));
+ ret = 0;
+
+out:
@@ builtin/history.c: out:
+ release_revisions(&revs);
+ free(message_template);
+ return ret;
-+}
-+
- static int update_worktree(struct repository *repo,
- const struct commit *old_head,
- const struct commit *new_head,
-@@ builtin/history.c: int cmd_history(int argc,
- GIT_HISTORY_FIXUP_USAGE,
- GIT_HISTORY_REWORD_USAGE,
- GIT_HISTORY_SPLIT_USAGE,
-+ GIT_HISTORY_SQUASH_USAGE,
- NULL,
- };
- parse_opt_subcommand_fn *fn = NULL;
-@@ builtin/history.c: int cmd_history(int argc,
- OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
- OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
- OPT_SUBCOMMAND("split", &fn, cmd_history_split),
-+ OPT_SUBCOMMAND("squash", &fn, cmd_history_squash),
- OPT_END(),
- };
+ }
+ static int update_worktree(struct repository *repo,
## object.h ##
@@ object.h: void object_array_init(struct object_array *array);
* bisect.c: 16
* bundle.c: 16
* http-push.c: 11-----14
+- * builtin/history.c: 1112
+ * builtin/history.c: 11---13
* commit-graph.c: 15
* commit-reach.c: 16-------20
* builtin/last-modified.c: 1617
- ## t/meson.build ##
-@@ t/meson.build: integration_tests = [
- 't3452-history-split.sh',
- 't3453-history-fixup.sh',
- 't3454-history-drop.sh',
-+ 't3455-history-squash.sh',
- 't3500-cherry.sh',
- 't3501-revert-cherry-pick.sh',
- 't3502-cherry-pick-merge.sh',
-
- ## t/t3455-history-squash.sh (new) ##
-@@
-+#!/bin/sh
-+
-+test_description='tests for git-history squash subcommand'
-+
-+. ./test-lib.sh
-+
+ ## t/t3455-history-squash.sh ##
+@@ t/t3455-history-squash.sh: test_description='tests for git-history squash subcommand'
+
+ . ./test-lib.sh
+
+-test_expect_success 'setup linear history' '
+stage_file () {
+ printf "%s\n" "$1" >file &&
+ git add file
@@ t/t3455-history-squash.sh (new)
+}
+
+test_expect_success 'setup linear history touching two files' '
-+ test_commit base file a start &&
+ test_commit base file a start &&
+- test_commit one file b &&
+- test_commit two file c &&
+- test_commit three file d
+ GIT_AUTHOR_NAME=One GIT_AUTHOR_EMAIL=one@example.com \
+ test_commit one other x &&
+ GIT_AUTHOR_NAME=Two GIT_AUTHOR_EMAIL=two@example.com \
+ test_commit two file c &&
+ GIT_AUTHOR_NAME=Three GIT_AUTHOR_EMAIL=three@example.com \
+ test_commit three file d
-+'
-+
-+test_expect_success 'errors on missing range argument' '
-+ test_must_fail git history squash 2>err &&
-+ test_grep "expects a revision range" err
-+'
-+
-+test_expect_success 'errors on an empty range' '
-+ test_must_fail git history squash HEAD..HEAD 2>err &&
-+ test_grep "the revision range is empty" err
-+'
-+
-+test_expect_success 'errors on a single revision that is not a range' '
-+ test_must_fail git history squash HEAD 2>err &&
-+ test_grep "not a .*range" err &&
-+ test_must_fail git history squash HEAD~1 2>err &&
-+ test_grep "not a .*range" err
-+'
-+
-+test_expect_success 'errors on a range holding a single commit' '
+ '
+
+ test_expect_success 'errors on missing range argument' '
+@@ t/t3455-history-squash.sh: test_expect_success 'errors on a single revision that is not a range' '
+ '
+
+ test_expect_success 'errors on a range holding a single commit' '
+ git reset --hard three &&
+ head_before=$(git rev-parse HEAD) &&
+
-+ test_must_fail git history squash "HEAD^!" 2>err &&
+ test_must_fail git history squash "HEAD^!" 2>err &&
+- test_grep "single commit; nothing to squash" err
+ test_grep "single commit; nothing to squash" err &&
+ test_cmp_rev "$head_before" HEAD
-+'
-+
+ '
+
+-test_expect_success 'rejects a root commit' '
+test_expect_success 'rejects root commit' '
+ # create a disconnected root commit
-+ oid=$(git commit-tree -m root three^{tree}) &&
+ oid=$(git commit-tree -m root three^{tree}) &&
+- test_must_fail git history squash \
+- --ancestry-path=start "$oid..three" 2>err &&
+- test_grep "cannot squash down to root commit" err
+ # because we pass --ancestry-path when calling setup_revs() it the
+ # revision walk will only include commits decended from $oid so
+ # we need to give it another --ancestry-path commit to actually walk
@@ t/t3455-history-squash.sh (new)
+ test_must_fail git history squash --ancestry-path=start $oid..three 2>err &&
+ echo "error: cannot squash down to root commit" >expect &&
+ test_cmp expect err
-+'
-+
-+test_expect_success 'rejects multiple tips' '
-+ oid=$(git commit-tree -m tip -p start^0 three^{tree}) &&
+ '
+
+ test_expect_success 'rejects multiple tips' '
+ oid=$(git commit-tree -m tip -p start^0 three^{tree}) &&
+- test_must_fail git history squash ^start "$oid" three~1 2>err &&
+- test_grep "revision range contains more than one tip" err
+ test_must_fail git history squash ^start $oid three~1 2>err &&
+ echo "error: the revision range contains more than one tip commit" >expect &&
+ test_cmp expect err &&
@@ t/t3455-history-squash.sh (new)
+ git history squash --no-edit ^start three~1 three &&
+ test_cmp_rev HEAD~1 start^0 &&
+ test_cmp_rev HEAD^{tree} three^{tree}
-+'
-+
+ '
+
+-test_expect_success 'rejects a merge parent outside the range' '
+- git reset --hard start &&
+test_expect_success 'accepts multiple revision arguments with an exclusion' '
+ git reset --hard three &&
+ git branch -f keep HEAD~2 &&
@@ t/t3455-history-squash.sh (new)
+
+test_expect_success 'squashes a branch the current branch is not on' '
+ git reset --hard three &&
-+ main=$(git symbolic-ref --short HEAD) &&
+ main=$(git symbolic-ref --short HEAD) &&
+- git checkout -b outside-parent &&
+- test_commit --no-tag outside-parent outside x &&
+ head_before=$(git rev-parse HEAD) &&
+ git checkout -b off-history start &&
+ test_commit --no-tag off-one off a &&
+ test_commit --no-tag off-two off b &&
-+ git checkout "$main" &&
-+
+ git checkout "$main" &&
+- test_commit --no-tag outside-main file b &&
+- base=$(git rev-parse HEAD) &&
+- test_commit --no-tag outside-mid file c &&
+- git merge --no-ff -m "merge outside-parent" outside-parent &&
+- git branch -D outside-parent &&
+
+- test_must_fail git history squash "$base.." 2>err &&
+- test_grep "parent .* of commit .* is outside the revision range" err
+ git history squash --no-edit start..off-history &&
+
+ check_commit_count start..off-history 1 &&
@@ t/t3455-history-squash.sh (new)
+ test_must_fail git history squash --no-edit HEAD~3.. 2>err &&
+ test_grep "^error: cannot squash .* target is not being squashed" err &&
+ test_set_editor :
-+'
-+
+ '
+
+-test_expect_success 'prints branches that cannot follow the squash' '
+test_expect_success 'squashing fixups into a merge' '
+ test_when_finished \
+ "git switch -f $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME; \
@@ t/t3455-history-squash.sh (new)
+ sed 1,2d msg | test_commit_message HEAD
+'
+
-+test_expect_success 'edits every message and aborts on an empty result' '
-+ git reset --hard start &&
-+ stage_file b &&
-+ git commit -m "re-one subject" -m "re-one body line" &&
-+ test_commit --no-tag re-two file c &&
-+ test_commit re-three file d &&
-+ head_before=$(git rev-parse HEAD) &&
-+
-+ write_script empty-editor <<-\EOF &&
-+ >"$1"
-+ EOF
-+ test_set_editor "$(pwd)/empty-editor" &&
-+ test_must_fail git history squash start.. 2>err &&
-+ test_grep "Aborting commit due to empty commit message" err &&
-+ test_cmp_rev "$head_before" HEAD &&
-+
-+ write_script editor <<-\EOF &&
-+ cat "$1" >edited &&
-+ echo combined >"$1"
-+ EOF
-+ test_set_editor "$(pwd)/editor" &&
-+ git history squash start.. &&
-+
-+ cat >expect <<-EOF &&
-+ # This is a combination of 3 commits.
-+ # This is the 1st commit message:
-+
-+ re-one subject
-+
-+ re-one body line
-+
-+ # This is the commit message #2:
-+
-+ re-two
-+
-+ # This is the commit message #3:
-+
-+ re-three
-+
-+ # Please enter the commit message for the squash changes. Lines starting
-+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
-+ # Changes to be committed:
-+ # modified: file
-+ #
-+ EOF
-+ test_cmp expect edited &&
-+ check_log_subjects -1 <<-\EOF
-+ combined
-+ EOF
-+'
-+
-+test_expect_success 'handles fixup!, squash! and amend! like rebase' '
-+ git reset --hard start &&
-+ test_commit --no-tag mark-base file b &&
-+ stage_file c &&
-+ commit_with_message "fixup! mark-base\n\nfixup body\n" &&
-+ stage_file d &&
-+ commit_with_message "squash! mark-base\n\nsquash remark\n" &&
-+ stage_file e &&
-+ commit_with_message "amend! mark-base\n\namended message\n" &&
-+
-+ write_script editor <<-\EOF &&
-+ cat "$1" >edited
-+ EOF
-+ test_set_editor "$(pwd)/editor" &&
-+ git history squash start.. &&
-+
-+ cat >expect <<-EOF &&
-+ # This is a combination of 4 commits.
-+ # This is the 1st commit message:
-+
-+ mark-base
-+
-+ # The commit message #2 will be skipped:
-+
-+ # fixup! mark-base
-+ #
-+ # fixup body
-+
-+ # This is the commit message #3:
-+
-+ # squash! mark-base
-+
-+ squash remark
-+
-+ # This is the commit message #4:
-+
-+ # amend! mark-base
-+
-+ amended message
-+
-+ # Please enter the commit message for the squash changes. Lines starting
-+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
-+ # Changes to be committed:
-+ # modified: file
-+ #
-+ EOF
-+ test_cmp expect edited &&
-+ check_log_messages -1 <<-\EOF
-+ mark-base
-+
-+ squash remark
-+
-+ amended message
-+
-+ EOF
-+'
-+
-+test_expect_success 'groups fixups under their targets in the editor' '
-+ git reset --hard start &&
-+ test_commit --no-tag alpha file a1 &&
-+ test_commit --no-tag beta file b1 &&
-+ stage_file a2 &&
-+ commit_with_message "fixup! alpha\n" &&
-+ stage_file b2 &&
-+ commit_with_message "fixup! beta\n" &&
-+
-+ write_script editor <<-\EOF &&
-+ cat "$1" >edited
-+ EOF
-+ test_set_editor "$(pwd)/editor" &&
-+ git history squash start.. &&
-+
-+ cat >expect <<-EOF &&
-+ # This is a combination of 4 commits.
-+ # This is the 1st commit message:
-+
-+ alpha
-+
-+ # The commit message #2 will be skipped:
-+
-+ # fixup! alpha
-+
-+ # This is the commit message #3:
-+
-+ beta
-+
-+ # The commit message #4 will be skipped:
-+
-+ # fixup! beta
-+
-+ # Please enter the commit message for the squash changes. Lines starting
-+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
-+ # Changes to be committed:
-+ # modified: file
-+ #
-+ EOF
-+ test_cmp expect edited
-+'
-+
-+test_expect_success 'lets amend! replace its target message in the editor' '
-+ git reset --hard start &&
-+ test_commit --no-tag mark-base file b &&
-+ stage_file c &&
-+ commit_with_message "amend! mark-base\n\namended message\n" &&
-+ stage_file d &&
-+ commit_with_message "squash! mark-base\n\nsquash remark\n" &&
-+
-+ write_script editor <<-\EOF &&
-+ cat "$1" >edited
-+ EOF
-+ test_set_editor "$(pwd)/editor" &&
-+ git history squash start.. &&
-+
-+ cat >expect <<-EOF &&
-+ # This is a combination of 3 commits.
-+ # The 1st commit message will be skipped:
-+
-+ # mark-base
-+
-+ # This is the commit message #2:
-+
-+ # amend! mark-base
-+
-+ amended message
-+
-+ # This is the commit message #3:
-+
-+ # squash! mark-base
-+
-+ squash remark
-+
-+ # Please enter the commit message for the squash changes. Lines starting
-+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
-+ # Changes to be committed:
-+ # modified: file
-+ #
-+ EOF
-+ test_cmp expect edited &&
-+ check_log_messages -1 <<-\EOF
-+ amended message
-+
-+ squash remark
-+
-+ EOF
-+'
-+
+test_expect_success '--update-refs=head only moves HEAD' '
+ git reset --hard three &&
+ git branch -f other HEAD &&
@@ t/t3455-history-squash.sh (new)
+'
+
+test_expect_success 'refuses to fold a range a branch points into' '
-+ test_when_finished \
-+ "git switch -f $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME; \
-+ git branch -D feature" &&
-+ git checkout -f -b feature start &&
-+ test_commit C1 &&
-+ test_commit C2 &&
-+ git checkout -b topic-1 start &&
-+ test_commit C3 &&
-+ test_commit C4 &&
-+ git checkout C3 &&
-+ test_commit C5 &&
-+ git checkout feature &&
-+ git merge C5 &&
-+ test_commit C6 &&
-+ git checkout -b topic-2 C2 &&
-+ test_commit C7 &&
-+ git checkout feature &&
-+
-+ test_must_fail git history squash start.. 2>err &&
-+ # TODO: check the branch names when we print them (topic-1 & topic-2)
-+ test_grep "^error: a branch points to" err &&
+ test_when_finished \
+ "git switch -f $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME; \
+ git branch -D feature" &&
+@@ t/t3455-history-squash.sh: test_expect_success 'prints branches that cannot follow the squash' '
+ test_grep "^error: the following branches cannot be rewritten" err &&
+ test_grep "^ topic-1$" err &&
+ test_grep "^ topic-2$" err &&
+- test_grep "^hint: .* --update-refs=head" err
+ test_grep "^hint: .* --update-refs=head" err &&
+ test_cmp_rev C6 HEAD &&
+
@@ t/t3455-history-squash.sh (new)
+ test_cmp_rev start HEAD^ &&
+ test_cmp_rev C6^{tree} HEAD^{tree} &&
+ test_cmp_rev C6 HEAD@{1}
-+'
-+
-+test_expect_success 'advice.historyUpdateRefs silences the hint' '
-+ git reset --hard three &&
-+ git branch -f mid HEAD~1 &&
+ '
+
+ test_expect_success 'advice.historyUpdateRefs silences the hint' '
+ git reset --hard three &&
+ git branch -f mid HEAD~1 &&
+ head_before=$(git rev-parse HEAD) &&
-+
-+ test_must_fail git -c advice.historyUpdateRefs=false \
-+ history squash start.. 2>err &&
-+ test_grep "^error: a branch points to" err &&
-+ test_grep ! "hint:" err &&
+
+ test_must_fail git -c advice.historyUpdateRefs=false \
+ history squash start.. 2>err &&
+ test_grep "^error: the following branches cannot be rewritten" err &&
+ test_grep "^ mid$" err &&
+ test_grep ! "hint:" err &&
+ test_cmp_rev "$head_before" HEAD &&
-+
-+ git branch -D mid
-+'
-+
+
+ git branch -D mid
+ '
+
+test_expect_success 'leaves tags and remote-tracking refs unchanged' '
+ git reset --hard three &&
+ git tag -f mark HEAD~1 &&
@@ t/t3455-history-squash.sh (new)
+ head_before=$(git rev-parse HEAD) &&
+
+ test_must_fail git history squash start.. 2>err &&
-+ # TODO: test for branch nome "at-merge"
-+ test_grep "a branch points to a commit" err &&
++ test_grep "^error: the following branches cannot be rewritten" err &&
++ test_grep "^ at-merge$" err &&
+ test_cmp_rev "$head_before" HEAD &&
+
+ git branch -D at-merge
+'
+
-+test_done
+ test_done
-: ---------- > 8: 821217ed7c history: support editing squashed commit messages
--
gitgitgadget
next prev parent reply other threads:[~2026-08-07 7:39 UTC|newest]
Thread overview: 154+ messages / expand[flat|nested] mbox.gz Atom feed top
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 15:17 ` D. Ben Knoble
2026-06-16 8:34 ` Patrick Steinhardt
2026-06-17 9:30 ` 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
2026-06-16 10:10 ` [PATCH v2 0/2] rebase: add --squash to fold a range into its first commit Phillip Wood
2026-06-17 9:11 ` Harald Nordgren
2026-06-17 9:48 ` Phillip Wood
2026-06-18 19:17 ` [PATCH v3 0/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-18 19:17 ` [PATCH v3 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-06-18 19:17 ` [PATCH v3 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-06-18 19:17 ` [PATCH v3 3/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-18 20:30 ` Junio C Hamano
2026-06-18 21:24 ` Junio C Hamano
2026-06-18 21:29 ` D. Ben Knoble
2026-06-19 12:55 ` Patrick Steinhardt
2026-06-18 19:17 ` [PATCH v3 4/4] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-06-18 21:23 ` [PATCH v3 0/4] history: add squash subcommand to fold a range D. Ben Knoble
2026-06-19 0:34 ` Junio C Hamano
2026-06-19 12:37 ` Patrick Steinhardt
2026-06-19 16:11 ` Junio C Hamano
2026-06-21 5:53 ` [PATCH v4 " Harald Nordgren via GitGitGadget
2026-06-21 5:53 ` [PATCH v4 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-06-21 5:53 ` [PATCH v4 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-06-21 5:53 ` [PATCH v4 3/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-21 5:53 ` [PATCH v4 4/4] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-06-22 11:54 ` [PATCH v4 0/4] history: add squash subcommand to fold a range Patrick Steinhardt
2026-06-23 10:41 ` Harald Nordgren
2026-06-24 21:54 ` [PATCH v5 " Harald Nordgren via GitGitGadget
2026-06-24 21:54 ` [PATCH v5 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-06-24 21:55 ` [PATCH v5 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-06-24 21:55 ` [PATCH v5 3/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-24 21:55 ` [PATCH v5 4/4] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-06-26 8:52 ` [PATCH v5 0/4] history: add squash subcommand to fold a range Phillip Wood
2026-06-26 9:57 ` Harald Nordgren
2026-06-26 13:12 ` Phillip Wood
2026-06-26 14:02 ` Junio C Hamano
2026-06-26 18:36 ` Harald Nordgren
2026-06-29 6:26 ` Patrick Steinhardt
2026-06-29 15:51 ` Phillip Wood
2026-06-29 16:54 ` Junio C Hamano
2026-07-01 13:45 ` Phillip Wood
2026-06-29 18:03 ` Harald Nordgren
2026-06-29 19:48 ` Phillip Wood
2026-06-29 21:13 ` Harald Nordgren
2026-06-30 13:48 ` Phillip Wood
2026-06-30 18:38 ` Harald Nordgren
2026-07-01 10:31 ` Phillip Wood
2026-07-01 13:47 ` Junio C Hamano
2026-07-01 15:14 ` Phillip Wood
2026-07-01 17:41 ` Junio C Hamano
2026-07-02 13:58 ` Phillip Wood
2026-06-30 2:55 ` Matt Hunter
2026-06-30 7:19 ` Harald Nordgren
2026-06-30 9:23 ` Matt Hunter
2026-06-30 14:01 ` Phillip Wood
2026-07-02 12:54 ` Patrick Steinhardt
2026-07-02 20:28 ` Junio C Hamano
2026-06-29 16:09 ` Harald Nordgren
2026-06-28 8:29 ` [PATCH v6 " Harald Nordgren via GitGitGadget
2026-06-28 8:29 ` [PATCH v6 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-06-28 8:29 ` [PATCH v6 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-06-28 8:29 ` [PATCH v6 3/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-06-29 5:50 ` Junio C Hamano
2026-06-28 8:29 ` [PATCH v6 4/4] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-06-29 5:50 ` Junio C Hamano
2026-06-29 13:49 ` Harald Nordgren
2026-06-29 14:49 ` Junio C Hamano
2026-06-29 17:38 ` Junio C Hamano
2026-07-06 8:50 ` [PATCH v7 0/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 1/5] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 2/5] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 3/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 4/5] sequencer: extract helpers for the squash message markers Harald Nordgren via GitGitGadget
2026-07-06 8:50 ` [PATCH v7 5/5] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-07-06 14:06 ` [PATCH v7 0/5] history: add squash subcommand to fold a range Phillip Wood
2026-07-07 7:51 ` Harald Nordgren
2026-07-07 8:55 ` Harald Nordgren
2026-07-07 9:30 ` Phillip Wood
2026-07-07 9:48 ` Phillip Wood
2026-07-06 20:42 ` Junio C Hamano
2026-07-10 9:06 ` [PATCH v8 " Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 1/5] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 2/5] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 3/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 4/5] sequencer: share the squash message marker helpers and flags Harald Nordgren via GitGitGadget
2026-07-10 9:06 ` [PATCH v8 5/5] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-07-14 4:44 ` [PATCH v8 0/5] history: add squash subcommand to fold a range Matt Hunter
2026-07-14 8:38 ` Harald Nordgren
2026-07-14 9:04 ` Harald Nordgren
2026-07-14 12:36 ` Ben Knoble
2026-07-14 18:41 ` Junio C Hamano
2026-07-15 15:16 ` [PATCH v9 " Harald Nordgren via GitGitGadget
2026-07-15 15:16 ` [PATCH v9 1/5] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-07-15 15:16 ` [PATCH v9 2/5] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-07-15 15:16 ` [PATCH v9 3/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-07-18 8:52 ` Matt Hunter
2026-07-18 9:28 ` Harald Nordgren
2026-07-15 15:16 ` [PATCH v9 4/5] sequencer: share the squash message marker helpers and flags Harald Nordgren via GitGitGadget
2026-07-15 15:16 ` [PATCH v9 5/5] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-07-18 8:52 ` Matt Hunter
2026-07-18 9:36 ` Harald Nordgren
2026-07-20 8:26 ` [PATCH v10 0/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-07-20 8:27 ` [PATCH v10 1/5] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-07-20 8:27 ` [PATCH v10 2/5] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-07-20 8:27 ` [PATCH v10 3/5] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-08-03 9:49 ` Phillip Wood
2026-08-03 9:49 ` [PATCH v10 3.1/3.7] fixup! " Phillip Wood
2026-08-03 9:49 ` [PATCH v10 3.2/3.7] " Phillip Wood
2026-08-03 9:49 ` [PATCH v10 3.3/3.7] " Phillip Wood
2026-08-03 9:49 ` [PATCH v10 3.4/3.7] " Phillip Wood
2026-08-03 9:49 ` [PATCH v10 3.5/3.7] " Phillip Wood
2026-08-03 9:49 ` [PATCH v10 3.6/3.7] " Phillip Wood
2026-08-03 9:49 ` [PATCH v10 3.7/3.7] " Phillip Wood
2026-08-03 16:35 ` [PATCH v10 3/5] " Harald Nordgren
2026-08-04 9:36 ` Phillip Wood
2026-08-04 13:21 ` Junio C Hamano
2026-08-04 20:41 ` Harald Nordgren
2026-08-04 20:50 ` Harald Nordgren
2026-08-07 14:32 ` Tuomas Ahola
2026-08-04 21:12 ` Junio C Hamano
2026-08-07 13:47 ` Phillip Wood
2026-08-07 18:31 ` Harald Nordgren
2026-08-06 9:19 ` Kristoffer Haugsbakk
2026-07-20 8:27 ` [PATCH v10 4/5] sequencer: share the squash message marker helpers and flags Harald Nordgren via GitGitGadget
2026-07-20 8:27 ` [PATCH v10 5/5] history: re-edit a squash with every message Harald Nordgren via GitGitGadget
2026-07-21 1:33 ` [PATCH v10 0/5] history: add squash subcommand to fold a range Matt Hunter
2026-08-01 6:53 ` [PATCH v11 0/4] " Harald Nordgren via GitGitGadget
2026-08-01 6:53 ` [PATCH v11 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-08-01 6:53 ` [PATCH v11 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-08-01 6:53 ` [PATCH v11 3/4] sequencer: share the squash message marker helpers and flags Harald Nordgren via GitGitGadget
2026-08-01 6:53 ` [PATCH v11 4/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-08-04 8:30 ` [PATCH v12 0/4] " Harald Nordgren via GitGitGadget
2026-08-04 8:30 ` [PATCH v12 1/4] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-08-04 8:30 ` [PATCH v12 2/4] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-08-04 8:30 ` [PATCH v12 3/4] sequencer: share the squash message marker helpers and flags Harald Nordgren via GitGitGadget
2026-08-04 8:30 ` [PATCH v12 4/4] history: add squash subcommand to fold a range Harald Nordgren via GitGitGadget
2026-08-07 7:39 ` Harald Nordgren via GitGitGadget [this message]
2026-08-07 7:39 ` [PATCH v13 1/8] history: extract helper for a commit's parent tree Harald Nordgren via GitGitGadget
2026-08-07 7:39 ` [PATCH v13 2/8] history: give commit_tree_ext a message template Harald Nordgren via GitGitGadget
2026-08-07 7:39 ` [PATCH v13 3/8] sequencer: share the squash message marker helpers and flags Harald Nordgren via GitGitGadget
2026-08-07 7:39 ` [PATCH v13 4/8] history: add skeleton for squash subcommand Harald Nordgren via GitGitGadget
2026-08-07 7:39 ` [PATCH v13 5/8] history: validate squash revision ranges Harald Nordgren via GitGitGadget
2026-08-07 7:39 ` [PATCH v13 6/8] history: protect branches when squashing a range Harald Nordgren via GitGitGadget
2026-08-07 7:39 ` [PATCH v13 7/8] history: create squashed commits without editing Harald Nordgren via GitGitGadget
2026-08-13 17:28 ` Junio C Hamano
2026-08-13 21:52 ` Junio C Hamano
2026-08-07 7:39 ` [PATCH v13 8/8] history: support editing squashed commit messages Harald Nordgren via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.2337.v13.git.git.1786088371.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=ben.knoble@gmail.com \
--cc=git@vger.kernel.org \
--cc=haraldnordgren@gmail.com \
--cc=kristofferhaugsbakk@fastmail.com \
--cc=m@lfurio.us \
--cc=phillip.wood123@gmail.com \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.