From: Junio C Hamano Date: Wed, 29 Jul 2026 15:48:02 +0000 (-0700) Subject: Revert "Merge branch 'hn/history-squash' into next" X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=11cd3fbfc2d0190af1a673947e223afbd5e17302;p=thirdparty%2Fgit.git Revert "Merge branch 'hn/history-squash' into next" This reverts commit 2790c83e4582832b6a5279db7d77d6d62a3ae8ad, reversing changes made to 3f0d50209424de9b2a3b57975503a767e485f62a. cf. --- diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc index e2a3487778..81f80a9274 100644 --- a/Documentation/config/advice.adoc +++ b/Documentation/config/advice.adoc @@ -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. diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc index 6f3b031d2a..28b477cd37 100644 --- a/Documentation/git-history.adoc +++ b/Documentation/git-history.adoc @@ -12,7 +12,6 @@ git history drop [--dry-run] [--update-refs=(branches|head)] [--empty=( git history fixup [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] git history reword [--dry-run] [--update-refs=(branches|head)] git history split [--dry-run] [--update-refs=(branches|head)] [--] [...] -git history squash [--dry-run] [--update-refs=(branches|head)] [--reedit-message] 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 `:: - Fold all commits in __ 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 `..` form, where __ 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 diff --git a/advice.c b/advice.c index 401d047391..63bf8b0c5f 100644 --- 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" }, diff --git a/advice.h b/advice.h index 3f0b4f0485..66f6cd6a77 100644 --- 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, diff --git a/builtin/history.c b/builtin/history.c index 92e1571301..000155ad9c 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -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 [--dry-run] [--update-refs=(branches|head)]") #define GIT_HISTORY_SPLIT_USAGE \ N_("git history split [--dry-run] [--update-refs=(branches|head)] [--] [...]") -#define GIT_HISTORY_SQUASH_USAGE \ - N_("git history squash [--dry-run] [--update-refs=(branches|head)] [--reedit-message] ") 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 ".." 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 "..". 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 '..' 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(), }; diff --git a/sequencer.c b/sequencer.c index 4d2d59ecbe..83c3849205 100644 --- a/sequencer.c +++ b/sequencer.c @@ -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); diff --git a/sequencer.h b/sequencer.h index b01f897020..64a9c7fb1b 100644 --- a/sequencer.h +++ b/sequencer.h @@ -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, diff --git a/t/meson.build b/t/meson.build index 78b8ea54ad..a25f37d2f5 100644 --- a/t/meson.build +++ b/t/meson.build @@ -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 index 6fce924327..0000000000 --- a/t/t3455-history-squash.sh +++ /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 " >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