]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Revert "Merge branch 'hn/history-squash' into next"
authorJunio C Hamano <gitster@pobox.com>
Wed, 29 Jul 2026 15:48:02 +0000 (08:48 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 29 Jul 2026 15:48:02 +0000 (08:48 -0700)
This reverts commit 2790c83e4582832b6a5279db7d77d6d62a3ae8ad, reversing
changes made to 3f0d50209424de9b2a3b57975503a767e485f62a.

cf. <f5f7af53-df3e-4902-b350-8fcf8ccb02ad@gmail.com>

Documentation/config/advice.adoc
Documentation/git-history.adoc
advice.c
advice.h
builtin/history.c
sequencer.c
sequencer.h
t/meson.build
t/t3455-history-squash.sh [deleted file]

index e2a348777837c1d927ae0b0334f5b39440a5a29b..81f80a927451236daf33a4e67d0f5f412f474f7b 100644 (file)
@@ -59,10 +59,6 @@ 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.
index 6f3b031d2a40fd5ac1cf16344e183f6509b3f6ed..28b477cd378150ac45a202acb7e92011045933d2 100644 (file)
@@ -12,7 +12,6 @@ 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)] [--reedit-message] <revision-range>
 
 DESCRIPTION
 -----------
@@ -44,11 +43,8 @@ at once.
 LIMITATIONS
 -----------
 
-This command does not (yet) replay merge commits onto the rewritten
-history: if a commit that would be replayed is a merge, the operation is
-rejected, and you should use linkgit:git-rebase[1] with the
-`--rebase-merges` flag instead. The `squash` subcommand can still fold a
-merge that lies inside the range, as long as the range has a single base.
+This command does not (yet) work with histories that contain merges. You
+should use linkgit:git-rebase[1] with the `--rebase-merges` flag instead.
 
 Furthermore, the command does not support operations that can result in merge
 conflicts. This limitation is by design as history rewrites are not intended to
@@ -117,50 +113,6 @@ linkgit:gitglossary[7].
 It is invalid to select either all or no hunks, as that would lead to
 one of the commits becoming empty.
 
-`squash <revision-range>`::
-       Fold all commits in _<revision-range>_ into the oldest commit of that
-       range. The resulting commit keeps the oldest commit's authorship and
-       takes the tree of the range's newest commit, so the whole range
-       collapses into a single commit. Commits above the range are replayed
-       on top of the result.
-+
-The range is given in the usual `<base>..<tip>` form, where _<base>_ is
-the commit just below the oldest commit to squash. For example, `git
-history squash HEAD~3..HEAD` folds the three most recent commits into
-one, and `git history squash HEAD~5..HEAD~2` squashes an interior range
-while leaving the two newest commits in place. Several revisions may be
-given, for example `HEAD~3..HEAD ^topic` to additionally exclude what is
-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.
-+
-The oldest commit's message is preserved by default, except that an `amend!`
-commit targeting it replaces its message. With `--reedit-message`, an editor
-opens pre-filled with the messages of all the folded commits so you can
-combine them. A merge commit inside the range is folded like any other, but
-the range must have a single base, so a range that reaches more than one entry
-point (for example a side branch that forked before the range and was later
-merged into it) is rejected.
-+
-A `fixup!`, `squash!`, or `amend!` commit is refused unless the commit it
-targets is also in the range, so the fold does not silently absorb a
-marker meant for a commit outside it. As an exception, a range made up entirely
-of markers for one target is combined into a single commit, keeping the last
-`amend!` message if there is one.
-+
-With `--reedit-message` the 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 branch or tag that points at a commit inside the range would be left
-dangling once those commits are folded away, so with the default
-`--update-refs=branches` the command refuses. Rerun with
-`--update-refs=head` to rewrite only the current branch and leave such
-refs pointing at the old commits.
-
 OPTIONS
 -------
 
@@ -171,8 +123,7 @@ OPTIONS
        ref updates is generally safe.
 
 `--reedit-message`::
-       Open an editor to modify the rewritten commit's message. For `squash`
-       the editor is pre-filled with the messages of all the folded commits.
+       Open an editor to modify the target commit's message.
 
 `--empty=(drop|keep|abort)`::
        Control what happens when a commit becomes empty as a result of the
index 401d0473917cd9c81bb2c5694fe6ec51911e1500..63bf8b0c5f048122f9577d6417b0c935c49c603a 100644 (file)
--- a/advice.c
+++ b/advice.c
@@ -58,7 +58,6 @@ 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" },
index 3f0b4f0485c7feb8655a31a2f6689bd1f17d5348..66f6cd6a772d8cd74f0bfa372afad9108a4f3e25 100644 (file)
--- a/advice.h
+++ b/advice.h
@@ -25,7 +25,6 @@ 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,
index 92e1571301ecab2af287d4109fc2d7edcf0483cc..000155ad9c24d55e8f4b93bb3107f825424fce3b 100644 (file)
@@ -1,7 +1,6 @@
 #define USE_THE_REPOSITORY_VARIABLE
 
 #include "builtin.h"
-#include "advice.h"
 #include "cache-tree.h"
 #include "commit.h"
 #include "commit-reach.h"
@@ -35,8 +34,6 @@
        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)] [--reedit-message] <revision-range>")
 
 static void change_data_free(void *util, const char *str UNUSED)
 {
@@ -111,7 +108,6 @@ enum commit_tree_flags {
 static int commit_tree_ext(struct repository *repo,
                           const char *action,
                           struct commit *commit_with_message,
-                          const char *message_template,
                           const struct commit_list *parents,
                           const struct object_id *old_tree,
                           const struct object_id *new_tree,
@@ -141,16 +137,13 @@ static int commit_tree_ext(struct repository *repo,
                original_author = xmemdupz(ptr, len);
        find_commit_subject(original_message, &original_body);
 
-       if (!message_template)
-               message_template = original_body;
-
        if (flags & COMMIT_TREE_EDIT_MESSAGE) {
                ret = fill_commit_message(repo, old_tree, new_tree,
-                                         message_template, action, &commit_message);
+                                         original_body, action, &commit_message);
                if (ret < 0)
                        goto out;
        } else {
-               strbuf_addstr(&commit_message, message_template);
+               strbuf_addstr(&commit_message, original_body);
        }
 
        original_extra_headers = read_commit_extra_headers(commit_with_message,
@@ -171,25 +164,6 @@ out:
        return ret;
 }
 
-static int first_parent_tree_oid(struct repository *repo,
-                                struct commit *commit,
-                                struct object_id *out)
-{
-       struct commit *parent = commit->parents ? commit->parents->item : NULL;
-
-       if (!parent) {
-               oidcpy(out, repo->hash_algo->empty_tree);
-               return 0;
-       }
-
-       if (repo_parse_commit(repo, parent))
-               return error(_("unable to parse parent commit %s"),
-                            oid_to_hex(&parent->object.oid));
-
-       oidcpy(out, &repo_get_commit_tree(repo, parent)->object.oid);
-       return 0;
-}
-
 static int commit_tree_with_edited_message(struct repository *repo,
                                           const char *action,
                                           struct commit *original,
@@ -197,13 +171,23 @@ static int commit_tree_with_edited_message(struct repository *repo,
 {
        struct object_id parent_tree_oid;
        const struct object_id *tree_oid;
+       struct commit *parent;
 
        tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
 
-       if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0)
-               return -1;
+       parent = original->parents ? original->parents->item : NULL;
+       if (parent) {
+               if (repo_parse_commit(repo, parent)) {
+                       return error(_("unable to parse parent commit %s"),
+                                    oid_to_hex(&parent->object.oid));
+               }
+
+               parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
+       } else {
+               oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+       }
 
-       return commit_tree_ext(repo, action, original, NULL, original->parents,
+       return commit_tree_ext(repo, action, original, original->parents,
                               &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
 }
 
@@ -491,10 +475,18 @@ static int commit_became_empty(struct repository *repo,
                               struct commit *original,
                               struct tree *result)
 {
+       struct commit *parent = original->parents ? original->parents->item : NULL;
        struct object_id parent_tree_oid;
 
-       if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0)
-               return -1;
+       if (parent) {
+               if (repo_parse_commit(repo, parent))
+                       return error(_("unable to parse parent of %s"),
+                                    oid_to_hex(&original->object.oid));
+
+               parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
+       } else {
+               oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+       }
 
        return oideq(&result->object.oid, &parent_tree_oid);
 }
@@ -682,7 +674,7 @@ static int cmd_history_fixup(int argc,
                goto out;
 
        if (!skip_commit) {
-               ret = commit_tree_ext(repo, "fixup", original, NULL, original->parents,
+               ret = commit_tree_ext(repo, "fixup", original, original->parents,
                                      &original_tree->object.oid, &merge_result.tree->object.oid,
                                      &rewritten, flags);
                if (ret < 0) {
@@ -838,9 +830,16 @@ static int split_commit(struct repository *repo,
        struct tree *split_tree;
        int ret;
 
-       if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0) {
-               ret = -1;
-               goto out;
+       if (original->parents) {
+               if (repo_parse_commit(repo, original->parents->item)) {
+                       ret = error(_("unable to parse parent commit %s"),
+                                   oid_to_hex(&original->parents->item->object.oid));
+                       goto out;
+               }
+
+               parent_tree_oid = *get_commit_tree_oid(original->parents->item);
+       } else {
+               oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
        }
        original_commit_tree_oid = get_commit_tree_oid(original);
 
@@ -893,7 +892,7 @@ static int split_commit(struct repository *repo,
         * The first commit is constructed from the split-out tree. The base
         * that shall be diffed against is the parent of the original commit.
         */
-       ret = commit_tree_ext(repo, "split-out", original, NULL, original->parents, &parent_tree_oid,
+       ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
                              &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
        if (ret < 0) {
                ret = error(_("failed writing first commit"));
@@ -910,7 +909,7 @@ static int split_commit(struct repository *repo,
        old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid;
        new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
 
-       ret = commit_tree_ext(repo, "split-out", original, NULL, parents, old_tree_oid,
+       ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
                              new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
        if (ret < 0) {
                ret = error(_("failed writing second commit"));
@@ -1007,479 +1006,6 @@ out:
        return ret;
 }
 
-/*
- * 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,
-                               const char **argv,
-                               struct commit **base_out,
-                               struct commit **oldest_out,
-                               struct commit **tip_out,
-                               struct oidset *interior_out)
-{
-       struct rev_info revs;
-       struct commit *commit, *base = NULL, *oldest = NULL, *tip = NULL;
-       struct commit_list *boundaries = NULL, *b;
-       struct strvec args = STRVEC_INIT;
-       size_t i;
-       int ret;
-
-       repo_init_revisions(repo, &revs, NULL);
-       revs.reverse = 1;
-       revs.topo_order = 1;
-       revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
-       revs.simplify_history = 0;
-       revs.boundary = 1;
-
-       strvec_push(&args, "ignored");
-       strvec_push(&args, "--ancestry-path");
-       strvec_pushv(&args, argv);
-       setup_revisions_from_strvec(&args, &revs, NULL);
-       if (args.nr != 1) {
-               ret = error(_("unrecognized argument: %s"), args.v[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) {
-               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 = 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 & UNINTERESTING)
-                       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;
-       }
-
-       /*
-        * Set boundary commits aside for the base check below, and put every
-        * in-range commit but the tip into the interior set. A ref pointing
-        * at an interior commit would dangle once the range is folded away.
-        */
-       while ((commit = get_revision(&revs))) {
-               if (commit->object.flags & BOUNDARY) {
-                       commit_list_insert(commit, &boundaries);
-                       continue;
-               }
-               if (!oldest)
-                       oldest = commit;
-               if (tip)
-                       oidset_insert(interior_out, &tip->object.oid);
-               tip = commit;
-       }
-
-       if (!oldest) {
-               ret = error(_("the revision range is empty"));
-               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");
-       }
-       base = oldest->parents->item;
-
-       /*
-        * A boundary other than the base is an in-range commit reaching a
-        * commit outside the range, so the range has more than one base.
-        */
-       for (b = boundaries; b; b = b->next) {
-               if (b->item != base) {
-                       ret = error(_("the revision range has more than one base; "
-                                     "cannot squash"));
-                       goto out;
-               }
-       }
-
-       *base_out = base;
-       *oldest_out = oldest;
-       *tip_out = tip;
-       ret = 0;
-
-out:
-       commit_list_free(boundaries);
-       reset_revision_walk();
-       release_revisions(&revs);
-       strvec_clear(&args);
-       return ret;
-}
-
-static const char *autosquash_target(const char *subject)
-{
-       const char *rest;
-
-       while (skip_prefix(subject, "fixup! ", &rest) ||
-              skip_prefix(subject, "squash! ", &rest) ||
-              skip_prefix(subject, "amend! ", &rest))
-               subject = rest;
-       return subject;
-}
-
-static int reject_dangling_fixups(struct repository *repo,
-                                 struct commit *base,
-                                 struct commit *tip,
-                                 struct commit *oldest,
-                                 struct commit **msg_source,
-                                 struct commit **amend_source)
-{
-       struct todo_list todo = TODO_LIST_INIT;
-       struct replay_opts opts = REPLAY_OPTS_INIT;
-       struct rev_info revs;
-       struct commit *commit, *last_amend = NULL;
-       struct strvec args = STRVEC_INIT;
-       char *dangling_subject = NULL, *dangling_target = NULL;
-       bool mixed_target = false, all_fixups_one_target;
-       bool past_oldest_group = false;
-       int i, ret, nr_dangling = 0;
-
-       *msg_source = oldest;
-       *amend_source = NULL;
-
-       repo_init_revisions(repo, &revs, NULL);
-       strvec_push(&args, "ignored");
-       strvec_push(&args, "--reverse");
-       strvec_push(&args, "--topo-order");
-       strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid),
-                    oid_to_hex(&tip->object.oid));
-       setup_revisions_from_strvec(&args, &revs, NULL);
-
-       if (prepare_revision_walk(&revs) < 0) {
-               ret = error(_("error preparing revisions"));
-               goto out;
-       }
-       while ((commit = get_revision(&revs)))
-               strbuf_addf(&todo.buf, "pick %s\n",
-                           oid_to_hex(&commit->object.oid));
-
-       if (todo_list_parse_insn_buffer(repo, &opts, todo.buf.buf, &todo) < 0 ||
-           todo_list_rearrange_squash(&todo) < 0) {
-               ret = error(_("could not check the range for fixups"));
-               goto out;
-       }
-
-       for (i = 0; i < todo.nr; i++) {
-               const char *message, *subject_start, *target;
-               char *subject;
-               size_t sublen;
-
-               message = repo_logmsg_reencode(repo, todo.items[i].commit,
-                                              NULL, NULL);
-               sublen = find_commit_subject(message, &subject_start);
-
-               if (todo.items[i].command != TODO_PICK) {
-                       if (!past_oldest_group &&
-                           starts_with(subject_start, "amend! "))
-                               *amend_source = todo.items[i].commit;
-                       repo_unuse_commit_buffer(repo, todo.items[i].commit, message);
-                       continue;
-               }
-               if (i)
-                       past_oldest_group = true;
-
-               subject = xmemdupz(subject_start, sublen);
-               target = autosquash_target(subject);
-               if (target != subject) {
-                       nr_dangling++;
-                       if (!dangling_target) {
-                               dangling_target = xstrdup(target);
-                               dangling_subject = xstrdup(subject);
-                       } else if (strcmp(dangling_target, target)) {
-                               mixed_target = true;
-                       }
-                       if (starts_with(subject, "amend! "))
-                               last_amend = todo.items[i].commit;
-               }
-               free(subject);
-               repo_unuse_commit_buffer(repo, todo.items[i].commit, message);
-       }
-
-       all_fixups_one_target = nr_dangling == todo.nr && !mixed_target;
-       if (nr_dangling && !all_fixups_one_target) {
-               ret = error(_("cannot squash '%s': its target is not in the "
-                             "range"), dangling_subject);
-       } else {
-               if (last_amend)
-                       *msg_source = last_amend;
-               ret = 0;
-       }
-
-out:
-       free(dangling_subject);
-       free(dangling_target);
-       todo_list_release(&todo);
-       replay_opts_release(&opts);
-       reset_revision_walk();
-       release_revisions(&revs);
-       strvec_clear(&args);
-       return ret;
-}
-
-struct interior_ref_cb {
-       const struct oidset *interior;
-       const char *name;
-};
-
-static int find_interior_ref(const struct reference *ref, void *cb_data)
-{
-       struct interior_ref_cb *data = cb_data;
-
-       if (oidset_contains(data->interior, ref->oid)) {
-               data->name = xstrdup(ref->name);
-               return 1;
-       }
-
-       return 0;
-}
-
-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,
-                               struct commit *base,
-                               struct commit *tip,
-                               struct strbuf *out)
-{
-       struct rev_info revs;
-       struct commit *commit;
-       struct strvec args = STRVEC_INIT;
-       struct todo_list todo = TODO_LIST_INIT;
-       struct replay_opts opts = REPLAY_OPTS_INIT;
-       int i, nr_commits, ret;
-
-       repo_init_revisions(repo, &revs, NULL);
-       strvec_push(&args, "ignored");
-       strvec_push(&args, "--reverse");
-       strvec_push(&args, "--topo-order");
-       strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid),
-                    oid_to_hex(&tip->object.oid));
-       setup_revisions_from_strvec(&args, &revs, NULL);
-
-       if (prepare_revision_walk(&revs) < 0) {
-               ret = error(_("error preparing revisions"));
-               goto out;
-       }
-
-       while ((commit = get_revision(&revs)))
-               strbuf_addf(&todo.buf, "pick %s\n",
-                           oid_to_hex(&commit->object.oid));
-
-       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);
-       reset_revision_walk();
-       release_revisions(&revs);
-       strvec_clear(&args);
-       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;
-       enum commit_tree_flags flags = 0;
-       int dry_run = 0;
-       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_BIT(0, "reedit-message", &flags,
-                       N_("open an editor to modify the commit message"),
-                       COMMIT_TREE_EDIT_MESSAGE),
-               OPT_END(),
-       };
-       struct strbuf reflog_msg = STRBUF_INIT;
-       struct strbuf message = STRBUF_INIT;
-       struct oidset interior = OIDSET_INIT;
-       struct commit *base, *oldest, *tip, *rewritten, *msg_source,
-               *amend_source;
-       const struct object_id *base_tree_oid, *tip_tree_oid;
-       const char *message_template = NULL;
-       struct commit_list *parents = NULL;
-       struct rev_info revs = { 0 };
-       int ret;
-
-       argc = parse_options(argc, argv, prefix, options, usage,
-                            PARSE_OPT_KEEP_UNKNOWN_OPT);
-       if (!argc) {
-               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;
-
-       ret = resolve_squash_range(repo, argv, &base, &oldest, &tip,
-                                  &interior);
-       if (ret < 0)
-               goto out;
-
-       ret = reject_dangling_fixups(repo, base, tip, oldest, &msg_source,
-                                    &amend_source);
-       if (ret < 0)
-               goto out;
-       if (amend_source) {
-               const char *amend_message, *body;
-
-               amend_message = repo_logmsg_reencode(repo, amend_source,
-                                                    NULL, NULL);
-               find_commit_subject(amend_message, &body);
-               body = skip_blank_lines(body + commit_subject_length(body));
-               strbuf_addstr(&message, body);
-               message_template = message.buf;
-               repo_unuse_commit_buffer(repo, amend_source, amend_message);
-       }
-
-       if (action == REF_ACTION_BRANCHES) {
-               struct interior_ref_cb cb = { .interior = &interior };
-
-               refs_for_each_ref(get_main_ref_store(repo),
-                                 find_interior_ref, &cb);
-               if (cb.name) {
-                       ret = error(_("'%s' points into the squashed range"),
-                                   cb.name);
-                       advise_if_enabled(ADVICE_HISTORY_UPDATE_REFS,
-                                         _("Use --update-refs=head to rewrite only "
-                                           "the current branch and leave such refs "
-                                           "untouched."));
-                       free((char *)cb.name);
-                       goto out;
-               }
-       }
-
-       if (flags & COMMIT_TREE_EDIT_MESSAGE) {
-               strbuf_reset(&message);
-               ret = build_squash_message(repo, base, tip, &message);
-               if (ret < 0)
-                       goto out;
-               message_template = message.buf;
-       }
-
-       ret = setup_revwalk(repo, action, tip, &revs);
-       if (ret < 0)
-               goto out;
-
-       base_tree_oid = &repo_get_commit_tree(repo, base)->object.oid;
-       tip_tree_oid = &repo_get_commit_tree(repo, tip)->object.oid;
-       commit_list_append(base, &parents);
-
-       ret = commit_tree_ext(repo, "squash", msg_source, message_template,
-                             parents,
-                             base_tree_oid, tip_tree_oid, &rewritten, flags);
-       if (ret < 0) {
-               ret = error(_("failed writing squashed commit"));
-               goto out;
-       }
-
-       strbuf_addstr(&reflog_msg, "squash: updating ");
-       strbuf_join_argv(&reflog_msg, argc, argv, ' ');
-
-       ret = handle_reference_updates(&revs, action, tip, rewritten,
-                                      reflog_msg.buf, dry_run,
-                                      REPLAY_EMPTY_COMMIT_ABORT);
-       if (ret < 0) {
-               ret = error(_("failed replaying descendants"));
-               goto out;
-       }
-
-       ret = 0;
-
-out:
-       strbuf_release(&reflog_msg);
-       strbuf_release(&message);
-       oidset_clear(&interior);
-       commit_list_free(parents);
-       release_revisions(&revs);
-       return ret;
-}
-
 static int update_worktree(struct repository *repo,
                           const struct commit *old_head,
                           const struct commit *new_head,
@@ -1668,7 +1194,6 @@ 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;
@@ -1677,7 +1202,6 @@ 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(),
        };
 
index 4d2d59ecbeb311d0c4474bb6affe3e0ca777461e..83c38492052b464a7a5fb1b8d2fe17d8f1de13de 100644 (file)
@@ -1880,38 +1880,18 @@ static int is_pick_or_similar(enum todo_command command)
        }
 }
 
+enum todo_item_flags {
+       TODO_EDIT_MERGE_MSG    = (1 << 0),
+       TODO_REPLACE_FIXUP_MSG = (1 << 1),
+       TODO_EDIT_FIXUP_MSG    = (1 << 2),
+};
+
 static const char first_commit_msg_str[] = N_("This is the 1st commit message:");
 static const char nth_commit_msg_fmt[] = N_("This is the commit message #%d:");
 static const char skip_first_commit_msg_str[] = N_("The 1st commit message will be skipped:");
 static const char skip_nth_commit_msg_fmt[] = N_("The commit message #%d will be skipped:");
 static const char combined_commit_msg_fmt[] = N_("This is a combination of %d commits.");
 
-void add_squash_combination_header(struct strbuf *buf, int n)
-{
-       strbuf_addf(buf, "%s ", comment_line_str);
-       strbuf_addf(buf, _(combined_commit_msg_fmt), n);
-}
-
-void add_squash_message_header(struct strbuf *buf, int n, int skip)
-{
-       strbuf_addf(buf, "%s ", comment_line_str);
-       if (n == 1)
-               strbuf_addstr(buf, skip ? _(skip_first_commit_msg_str) :
-                                  _(first_commit_msg_str));
-       else
-               strbuf_addf(buf, skip ? _(skip_nth_commit_msg_fmt) :
-                           _(nth_commit_msg_fmt), n);
-}
-
-size_t squash_subject_comment_len(const char *body, int squashing)
-{
-       if (starts_with(body, "amend!") ||
-           (squashing && (starts_with(body, "squash!") ||
-                          starts_with(body, "fixup!"))))
-               return commit_subject_length(body);
-       return 0;
-}
-
 static int is_fixup_flag(enum todo_command command, unsigned flag)
 {
        return command == TODO_FIXUP && ((flag & TODO_REPLACE_FIXUP_MSG) ||
@@ -2032,13 +2012,20 @@ static int append_squash_message(struct strbuf *buf, const char *body,
 {
        struct replay_ctx *ctx = opts->ctx;
        const char *fixup_msg;
-       size_t commented_len, fixup_off;
-
-       commented_len = squash_subject_comment_len(body,
-                               command == TODO_SQUASH || seen_squash(ctx));
+       size_t commented_len = 0, fixup_off;
+       /*
+        * amend is non-interactive and not normally used with fixup!
+        * or squash! commits, so only comment out those subjects when
+        * squashing commit messages.
+        */
+       if (starts_with(body, "amend!") ||
+           ((command == TODO_SQUASH || seen_squash(ctx)) &&
+            (starts_with(body, "squash!") || starts_with(body, "fixup!"))))
+               commented_len = commit_subject_length(body);
 
-       strbuf_addch(buf, '\n');
-       add_squash_message_header(buf, ++ctx->current_fixup_count + 1, 0);
+       strbuf_addf(buf, "\n%s ", comment_line_str);
+       strbuf_addf(buf, _(nth_commit_msg_fmt),
+                   ++ctx->current_fixup_count + 1);
        strbuf_addstr(buf, "\n\n");
        strbuf_add_commented_lines(buf, body, commented_len, comment_line_str);
        /* buf->buf may be reallocated so store an offset into the buffer */
@@ -2103,8 +2090,9 @@ static int update_squash_messages(struct repository *r,
                eol = !starts_with(buf.buf, comment_line_str) ?
                        buf.buf : strchrnul(buf.buf, '\n');
 
-               add_squash_combination_header(&header,
-                                             ctx->current_fixup_count + 2);
+               strbuf_addf(&header, "%s ", comment_line_str);
+               strbuf_addf(&header, _(combined_commit_msg_fmt),
+                           ctx->current_fixup_count + 2);
                strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);
                strbuf_release(&header);
                if (is_fixup_flag(command, flag) && !seen_squash(ctx))
@@ -2128,9 +2116,12 @@ static int update_squash_messages(struct repository *r,
                        repo_unuse_commit_buffer(r, head_commit, head_message);
                        return error(_("cannot write '%s'"), rebase_path_fixup_msg());
                }
-               add_squash_combination_header(&buf, 2);
-               strbuf_addch(&buf, '\n');
-               add_squash_message_header(&buf, 1, is_fixup_flag(command, flag));
+               strbuf_addf(&buf, "%s ", comment_line_str);
+               strbuf_addf(&buf, _(combined_commit_msg_fmt), 2);
+               strbuf_addf(&buf, "\n%s ", comment_line_str);
+               strbuf_addstr(&buf, is_fixup_flag(command, flag) ?
+                             _(skip_first_commit_msg_str) :
+                             _(first_commit_msg_str));
                strbuf_addstr(&buf, "\n\n");
                if (is_fixup_flag(command, flag))
                        strbuf_add_commented_lines(&buf, body, strlen(body),
@@ -2149,8 +2140,9 @@ static int update_squash_messages(struct repository *r,
        if (command == TODO_SQUASH || is_fixup_flag(command, flag)) {
                res = append_squash_message(&buf, body, command, opts, flag);
        } else if (command == TODO_FIXUP) {
-               strbuf_addch(&buf, '\n');
-               add_squash_message_header(&buf, ++ctx->current_fixup_count + 1, 1);
+               strbuf_addf(&buf, "\n%s ", comment_line_str);
+               strbuf_addf(&buf, _(skip_nth_commit_msg_fmt),
+                           ++ctx->current_fixup_count + 1);
                strbuf_addstr(&buf, "\n\n");
                strbuf_add_commented_lines(&buf, body, strlen(body),
                                           comment_line_str);
index b01f8970201fc36b116fc76d1223a5bd8b9526b2..64a9c7fb1beccd6f91f7e6e4263d37d40f4885c3 100644 (file)
@@ -119,13 +119,6 @@ enum todo_command {
        TODO_COMMENT
 };
 
-/* Bits for the "flags" member of struct todo_item */
-enum todo_item_flags {
-       TODO_EDIT_MERGE_MSG    = (1 << 0),
-       TODO_REPLACE_FIXUP_MSG = (1 << 1),
-       TODO_EDIT_FIXUP_MSG    = (1 << 2),
-};
-
 struct todo_item {
        enum todo_command command;
        struct commit *commit;
@@ -215,29 +208,6 @@ int todo_list_rearrange_squash(struct todo_list *todo_list);
  */
 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag);
 
-/*
- * Append the "This is a combination of N commits." banner that "git rebase
- * -i" writes at the top of a squashed commit's message, commented out with
- * the comment character.
- */
-void add_squash_combination_header(struct strbuf *buf, int n);
-
-/*
- * Append the header (1-based N) that "git rebase -i" writes above each message
- * when squashing, commented out with the comment character. With SKIP it reads
- * "The ... commit message will be skipped" for a message that is dropped (a
- * fixup), otherwise "This is the ... commit message".
- */
-void add_squash_message_header(struct strbuf *buf, int n, int skip);
-
-/*
- * Return the length of the leading subject of BODY when it should be commented
- * out in a squash message, or 0 otherwise. An "amend!" subject always
- * qualifies; "squash!" and "fixup!" subjects only when SQUASHING, since a
- * plain fixup chain keeps them.
- */
-size_t squash_subject_comment_len(const char *body, int squashing);
-
 void append_conflicts_hint(struct index_state *istate,
                struct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode);
 enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg,
index 78b8ea54adc7b4ed4f3733ff1642334e06c87801..a25f37d2f5ae7d38f4138b2c88443332b5934d53 100644 (file)
@@ -406,7 +406,6 @@ 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',
diff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh
deleted file mode 100755 (executable)
index 6fce924..0000000
+++ /dev/null
@@ -1,770 +0,0 @@
-#!/bin/sh
-
-test_description='tests for git-history squash subcommand'
-
-. ./test-lib.sh
-
-stage_file () {
-       printf "%s\n" "$1" >file &&
-       git add file
-}
-
-commit_with_message () {
-       printf "%b" "$1" >msg &&
-       git commit --allow-empty -qF msg
-}
-
-check_commit_count () {
-       git rev-list --count "$1" >actual &&
-       echo "$2" >expect &&
-       test_cmp expect actual
-}
-
-check_log_subjects () {
-       git log --format="%s" "$1" >actual &&
-       cat >expect &&
-       test_cmp expect actual
-}
-
-check_log_messages () {
-       git log --format="%B" "$1" >actual &&
-       cat >expect &&
-       test_cmp expect actual
-}
-
-test_expect_success 'setup linear history touching two files' '
-       test_commit base file a &&
-       git tag start &&
-       test_commit --no-tag one other x &&
-       test_commit --no-tag two file c &&
-       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' '
-       git reset --hard three &&
-       head_before=$(git rev-parse HEAD) &&
-
-       test_must_fail git history squash "HEAD^!" 2>err &&
-       test_grep "single commit; nothing to squash" err &&
-       test_cmp_rev "$head_before" HEAD
-'
-
-test_expect_success 'accepts multiple revision arguments with an exclusion' '
-       git reset --hard three &&
-       git branch -f keep HEAD~2 &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash start..HEAD ^keep &&
-
-       git reflog -1 --format=%gs >actual &&
-       echo "squash: updating start..HEAD ^keep" >expect &&
-       test_cmp expect actual &&
-
-       check_log_subjects start..HEAD <<-\EOF &&
-       two
-       one
-       EOF
-       test_cmp_rev keep HEAD~1 &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
-
-       git branch -D keep
-'
-
-test_expect_success 'squashes a branch the current branch is not on' '
-       git reset --hard three &&
-       main=$(git symbolic-ref --short HEAD) &&
-       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 history squash start..off-history &&
-
-       check_commit_count start..off-history 1 &&
-       test_cmp_rev "$head_before" HEAD &&
-
-       git branch -D off-history
-'
-
-test_expect_success 'squashes a range into a single commit without changing the tree' '
-       git reset --hard three &&
-       head_before=$(git rev-parse HEAD) &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash --dry-run start.. >out &&
-       predicted=$(awk "/^update refs\/heads\// {print \$3}" out) &&
-       test_cmp_rev "$head_before" HEAD &&
-
-       git history squash start.. &&
-
-       test "$predicted" = "$(git rev-parse HEAD)" &&
-       check_commit_count start..HEAD 1 &&
-       test_cmp_rev start HEAD^ &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
-       check_log_subjects -1 <<-\EOF &&
-       one
-       EOF
-       git reflog >reflog &&
-       test_grep "squash: updating" reflog
-'
-
-test_expect_success 'sanitizes rev-list walk options, before and after --' '
-       git reset --hard three &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash --date-order start.. 2>err &&
-       test_grep "ignoring rev-list options" err &&
-       test_cmp_rev start HEAD^ &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
-
-       git reset --hard three &&
-       git history squash -- --reverse start.. 2>err &&
-       test_grep "ignoring rev-list options" err &&
-       test_cmp_rev start HEAD^ &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
-'
-
-test_expect_success 'squashes an interior range and replays descendants verbatim' '
-       git reset --hard three &&
-       final_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash start..@~1 &&
-
-       check_log_subjects start..HEAD <<-\EOF &&
-       three
-       one
-       EOF
-
-       test_cmp_rev start HEAD~2 &&
-       test "$final_tree" = "$(git rev-parse HEAD^{tree})"
-'
-
-test_expect_success 'squashes when the base is the root commit' '
-       git reset --hard three &&
-       root=$(git rev-list --max-parents=0 HEAD) &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash "$root.." &&
-
-       check_commit_count "$root..HEAD" 1 &&
-       test_cmp_rev "$root" HEAD^ &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
-'
-
-
-test_expect_success 'folds fixups whose target is in the range' '
-       git reset --hard start &&
-       test_commit --no-tag target file b &&
-       git commit --allow-empty -m "fixup! target" &&
-       git commit --allow-empty -m "fixup! target" &&
-       test_commit --no-tag later file c &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       check_log_subjects -1 <<-\EOF
-       target
-       EOF
-'
-
-test_expect_success 'refuses a below-range fixup! after an in-range commit' '
-       git reset --hard start &&
-       test_commit --no-tag inside file b &&
-       test_commit --no-tag "fixup! outside" file c &&
-       head_before=$(git rev-parse HEAD) &&
-
-       test_must_fail git history squash start.. 2>err &&
-       test_grep "target is not in the range" err &&
-       test_cmp_rev "$head_before" HEAD
-'
-
-test_expect_success 'combines a run of fixups for one commit below the range' '
-       git reset --hard start &&
-       stage_file b && git commit -m "fixup! base" &&
-       stage_file c && git commit -m "fixup! base" &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       check_log_subjects -1 <<-\EOF
-       fixup! base
-       EOF
-'
-
-test_expect_success 'combining below-range fixups keeps the last amend! message' '
-       git reset --hard start &&
-       stage_file b && git commit -m "fixup! base" &&
-       stage_file c &&
-       commit_with_message "amend! base\n\namended body\n" &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       check_log_messages -1 <<-\EOF
-       amend! base
-
-       amended body
-
-       EOF
-'
-
-test_expect_success 'refuses fixups for two different commits below the range' '
-       git reset --hard start &&
-       stage_file b && git commit -m "fixup! aaa" &&
-       stage_file c && git commit -m "fixup! bbb" &&
-       head_before=$(git rev-parse HEAD) &&
-
-       test_must_fail git history squash start.. 2>err &&
-       test_grep "target is not in the range" err &&
-       test_cmp_rev "$head_before" HEAD
-'
-
-test_expect_success 'the last amend! for the oldest commit replaces its message' '
-       git reset --hard start &&
-       test_commit --no-tag marker-oldest file b &&
-       git commit --allow-empty -m "squash! marker-oldest" &&
-       commit_with_message "amend! marker-oldest\n\nearlier message\n" &&
-       commit_with_message \
-               "amend! marker-oldest\n\namended subject\n\namended body\n" &&
-       test_commit --no-tag marker-later file c &&
-       commit_with_message "amend! marker-later\n\nwrong message\n" &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       check_log_messages -1 <<-\EOF
-       amended subject
-
-       amended body
-
-       EOF
-'
-
-test_expect_success 'preserves authorship of the oldest commit' '
-       git reset --hard start &&
-       GIT_AUTHOR_NAME=Squasher GIT_AUTHOR_EMAIL=squash@example.com \
-               test_commit --no-tag oldest file b &&
-       test_commit newest file c &&
-
-       git history squash start.. &&
-
-       git log -1 --format="%an <%ae>" >actual &&
-       echo "Squasher <squash@example.com>" >expect &&
-       test_cmp expect actual
-'
-
-test_expect_success '--reedit-message offers every folded-in message' '
-       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 &&
-
-       write_script editor <<-\EOF &&
-       cat "$1" >edited &&
-       echo combined >"$1"
-       EOF
-       test_set_editor "$(pwd)/editor" &&
-       git history squash --reedit-message 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 '--reedit-message 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 --reedit-message 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 '--reedit-message groups fixups under their targets' '
-       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 --reedit-message 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 '--reedit-message lets amend! replace its target message' '
-       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 --reedit-message 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 '--reedit-message aborts on an empty message' '
-       git reset --hard three &&
-       head_before=$(git rev-parse HEAD) &&
-
-       write_script editor <<-\EOF &&
-       >"$1"
-       EOF
-       test_set_editor "$(pwd)/editor" &&
-       test_must_fail git history squash --reedit-message start.. &&
-
-       test_cmp_rev "$head_before" HEAD
-'
-
-test_expect_success '--update-refs=head only moves HEAD' '
-       git reset --hard three &&
-       git branch -f other HEAD &&
-       other_before=$(git rev-parse other) &&
-
-       git history squash --update-refs=head start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       test_cmp_rev "$other_before" other
-'
-
-test_expect_success 'refuses to fold a range a ref points into' '
-       git reset --hard three &&
-       git branch -f mid HEAD~1 &&
-       head_before=$(git rev-parse HEAD) &&
-
-       test_must_fail git history squash start.. 2>err &&
-       test_grep "error: .* points into the squashed range" err &&
-       test_grep "hint: .*--update-refs=head" err &&
-       test_cmp_rev "$head_before" HEAD &&
-
-       git branch -D mid
-'
-
-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 "points into the squashed range" err &&
-       test_grep ! "hint:" err &&
-       test_cmp_rev "$head_before" HEAD &&
-
-       git branch -D mid
-'
-
-test_expect_success '--update-refs=head folds past a ref pointing into the range' '
-       git reset --hard three &&
-       git branch -f mid HEAD~1 &&
-       mid_before=$(git rev-parse mid) &&
-
-       git history squash --update-refs=head start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       test_cmp_rev "$mid_before" mid &&
-
-       git branch -D mid
-'
-
-test_expect_success 'refuses to fold a range a tag points into' '
-       git reset --hard three &&
-       git tag -f mark HEAD~1 &&
-       head_before=$(git rev-parse HEAD) &&
-
-       test_must_fail git history squash start.. 2>err &&
-       test_grep "refs/tags/mark" err &&
-       test_grep "points into the squashed range" err &&
-       test_cmp_rev "$head_before" HEAD &&
-
-       git tag -d mark
-'
-
-test_expect_success 'squashes a range whose internal merge has a single base' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       test_commit --no-tag before-side file b &&
-       git checkout -b inner-side &&
-       test_commit --no-tag on-inner-side inner x &&
-       git checkout "$main" &&
-       test_commit --no-tag after-side file c &&
-       git merge --no-ff -m merge inner-side &&
-       git branch -D inner-side &&
-       test_commit --no-tag after-merge file d &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       check_log_subjects -1 <<-\EOF &&
-       before-side
-       EOF
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
-       test_path_is_file inner
-'
-
-test_expect_success 'folds a merge of a branch that forked at the base' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       git checkout -b base-fork-side &&
-       test_commit --no-tag base-fork-side side x &&
-       git checkout "$main" &&
-       test_commit --no-tag base-fork-main file b &&
-       git merge --no-ff -m "merge base-fork-side" base-fork-side &&
-       git branch -D base-fork-side &&
-       test_commit --no-tag base-fork-tail file c &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       test_cmp_rev start HEAD^ &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
-       test_path_is_file side
-'
-
-test_expect_success 'refuses a merge whose other parent is outside the range' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       git checkout -b outside-parent &&
-       test_commit --no-tag outside-parent outside x &&
-       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 &&
-       merged=$(git rev-parse HEAD) &&
-
-       test_must_fail git history squash "$base.." 2>err &&
-       test_grep "more than one base" err &&
-       test_cmp_rev "$merged" HEAD
-'
-
-test_expect_success 'folds a range whose tip is a merge commit' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       test_commit --no-tag tipmerge-base file b &&
-       git checkout -b tipmerge-side &&
-       test_commit --no-tag tipmerge-side side x &&
-       git checkout "$main" &&
-       test_commit --no-tag tipmerge-main file c &&
-       git merge --no-ff -m "merge tipmerge-side" tipmerge-side &&
-       git branch -D tipmerge-side &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
-       test_path_is_file side
-'
-
-test_expect_success 'folds a range whose base is a merge commit' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       git checkout -b basemerge-side &&
-       test_commit --no-tag basemerge-side side x &&
-       git checkout "$main" &&
-       test_commit --no-tag basemerge-main file b &&
-       git merge --no-ff -m "merge basemerge-side" basemerge-side &&
-       git branch -D basemerge-side &&
-       base=$(git rev-parse HEAD) &&
-       test_commit --no-tag basemerge-one file c &&
-       test_commit --no-tag basemerge-two file d &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash "$base.." &&
-
-       check_commit_count "$base..HEAD" 1 &&
-       test_cmp_rev "$base" HEAD^ &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
-'
-
-test_expect_success 'folds a range with two interior merges' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       test_commit --no-tag two-merge-a file a1 &&
-       git checkout -b two-merge-s1 &&
-       test_commit --no-tag two-merge-s1 s1 x &&
-       git checkout "$main" &&
-       git merge --no-ff -m "merge s1" two-merge-s1 &&
-       test_commit --no-tag two-merge-b file b1 &&
-       git checkout -b two-merge-s2 &&
-       test_commit --no-tag two-merge-s2 s2 y &&
-       git checkout "$main" &&
-       git merge --no-ff -m "merge s2" two-merge-s2 &&
-       git branch -D two-merge-s1 two-merge-s2 &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
-       test_path_is_file s1 &&
-       test_path_is_file s2
-'
-
-test_expect_success 'folds a range with a nested merge' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       git checkout -b nested-outer &&
-       test_commit --no-tag nested-outer outer x &&
-       git checkout -b nested-inner &&
-       test_commit --no-tag nested-inner inner y &&
-       git checkout nested-outer &&
-       git merge --no-ff -m "merge inner" nested-inner &&
-       git checkout "$main" &&
-       test_commit --no-tag nested-main file b1 &&
-       git merge --no-ff -m "merge outer" nested-outer &&
-       git branch -D nested-outer nested-inner &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
-       test_path_is_file outer &&
-       test_path_is_file inner
-'
-
-test_expect_success 'folds a range with an octopus merge' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       test_commit --no-tag octo-base file a1 &&
-       git checkout -b octo-1 &&
-       test_commit --no-tag octo-1 o1 x &&
-       git checkout "$main" &&
-       git checkout -b octo-2 &&
-       test_commit --no-tag octo-2 o2 y &&
-       git checkout "$main" &&
-       git merge --no-ff -m octopus octo-1 octo-2 &&
-       git branch -D octo-1 octo-2 &&
-       tip_tree=$(git rev-parse HEAD^{tree}) &&
-
-       git history squash start.. &&
-
-       check_commit_count start..HEAD 1 &&
-       test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
-       test_path_is_file o1 &&
-       test_path_is_file o2
-'
-
-test_expect_success 'refuses an octopus merge with an arm forked before the base' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       git checkout -b octo-pre &&
-       test_commit octo-pre-side pside x &&
-       git checkout "$main" &&
-       test_commit octo-pre-main file b1 &&
-       octo_base=$(git rev-parse HEAD) &&
-       git checkout -b octo-within &&
-       test_commit --no-tag octo-within wside y &&
-       git checkout "$main" &&
-       git merge --no-ff -m octopus octo-pre octo-within &&
-       merged=$(git rev-parse HEAD) &&
-       git branch -D octo-pre octo-within &&
-
-       test_must_fail git history squash "$octo_base.." 2>err &&
-       test_grep "more than one base" err &&
-       test_cmp_rev "$merged" HEAD
-'
-
-test_expect_success 'refuses when a descendant above the range is a merge' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       test_commit --no-tag desc-one file b &&
-       test_commit --no-tag desc-two file c &&
-       git tag desc-tip &&
-       git checkout -b desc-above &&
-       test_commit --no-tag desc-above above x &&
-       git checkout "$main" &&
-       test_commit --no-tag desc-main file d &&
-       git merge --no-ff -m "merge desc-above" desc-above &&
-       git branch -D desc-above &&
-       head_before=$(git rev-parse HEAD) &&
-
-       test_must_fail git history squash start..desc-tip 2>err &&
-       test_grep "merge commits is not supported" err &&
-       test_cmp_rev "$head_before" HEAD
-'
-
-test_expect_success 'refuses to fold a range a ref points into at a merge' '
-       git reset --hard start &&
-       main=$(git symbolic-ref --short HEAD) &&
-       test_commit --no-tag refmerge-base file b &&
-       git checkout -b refmerge-side &&
-       test_commit --no-tag refmerge-side side x &&
-       git checkout "$main" &&
-       test_commit --no-tag refmerge-main file c &&
-       git merge --no-ff -m "interior merge" refmerge-side &&
-       git branch -D refmerge-side &&
-       git branch at-merge HEAD &&
-       test_commit --no-tag refmerge-tail file d &&
-       head_before=$(git rev-parse HEAD) &&
-
-       test_must_fail git history squash start.. 2>err &&
-       test_grep "at-merge" err &&
-       test_grep "points into the squashed range" err &&
-       test_cmp_rev "$head_before" HEAD &&
-
-       git branch -D at-merge
-'
-
-test_done