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.
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
-----------
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
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
-------
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
[ADVICE_FETCH_SHOW_FORCED_UPDATES] = { "fetchShowForcedUpdates" },
[ADVICE_FORCE_DELETE_BRANCH] = { "forceDeleteBranch" },
[ADVICE_GRAFT_FILE_DEPRECATED] = { "graftFileDeprecated" },
- [ADVICE_HISTORY_UPDATE_REFS] = { "historyUpdateRefs" },
[ADVICE_IGNORED_HOOK] = { "ignoredHook" },
[ADVICE_IMPLICIT_IDENTITY] = { "implicitIdentity" },
[ADVICE_MERGE_CONFLICT] = { "mergeConflict" },
ADVICE_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,
#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
-#include "advice.h"
#include "cache-tree.h"
#include "commit.h"
#include "commit-reach.h"
N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
#define GIT_HISTORY_SPLIT_USAGE \
N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
-#define GIT_HISTORY_SQUASH_USAGE \
- N_("git history squash [--dry-run] [--update-refs=(branches|head)] [--reedit-message] <revision-range>")
static void change_data_free(void *util, const char *str UNUSED)
{
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,
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,
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,
{
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);
}
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);
}
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) {
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);
* 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"));
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"));
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,
GIT_HISTORY_FIXUP_USAGE,
GIT_HISTORY_REWORD_USAGE,
GIT_HISTORY_SPLIT_USAGE,
- GIT_HISTORY_SQUASH_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
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(),
};
}
}
+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) ||
{
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 */
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))
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),
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);
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;
*/
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,
'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',
+++ /dev/null
-#!/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