Revert "Merge branch 'hn/history-squash' into next"

This reverts commit 2790c83e4582832b6a5279db7d77d6d62a3ae8ad, reversing changes made to 3f0d50209424de9b2a3b57975503a767e485f62a. cf. <f5f7af53-df3e-4902-b350-8fcf8ccb02ad@gmail.com>

Junio C Hamano committed Jul 29, 2026 at 08:48 UTC 11cd3fbfc2d0190af1a673947e223afbd5e17302
9 files changed +72 -1412
Documentation/config/advice.adoc
-4
@@ -59,10 +59,6 @@ all advice messages.
59 forceDeleteBranch::
60 Shown when the user tries to delete a not fully merged
61 branch without the force option set.
62 - historyUpdateRefs::
63 - Shown when `git history squash` refuses because a ref points
64 - into the range being folded, to tell the user about
65 - `--update-refs=head`.
62 ignoredHook::
63 Shown when a hook is ignored because the hook is not
64 set as executable.
Documentation/git-history.adoc
+3 -52
@@ -12,7 +12,6 @@ git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(
12 git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
13 git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
14 git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
15 -git history squash [--dry-run] [--update-refs=(branches|head)] [--reedit-message] <revision-range>
15
16 DESCRIPTION
17 -----------
@@ -44,11 +43,8 @@ at once.
43 LIMITATIONS
44 -----------
45
47 -This command does not (yet) replay merge commits onto the rewritten
48 -history: if a commit that would be replayed is a merge, the operation is
49 -rejected, and you should use linkgit:git-rebase[1] with the
50 -`--rebase-merges` flag instead. The `squash` subcommand can still fold a
51 -merge that lies inside the range, as long as the range has a single base.
46 +This command does not (yet) work with histories that contain merges. You
47 +should use linkgit:git-rebase[1] with the `--rebase-merges` flag instead.
48
49 Furthermore, the command does not support operations that can result in merge
50 conflicts. This limitation is by design as history rewrites are not intended to
@@ -117,50 +113,6 @@ linkgit:gitglossary[7].
113 It is invalid to select either all or no hunks, as that would lead to
114 one of the commits becoming empty.
115
120 -`squash <revision-range>`::
121 - Fold all commits in _<revision-range>_ into the oldest commit of that
122 - range. The resulting commit keeps the oldest commit's authorship and
123 - takes the tree of the range's newest commit, so the whole range
124 - collapses into a single commit. Commits above the range are replayed
125 - on top of the result.
126 -+
127 -The range is given in the usual `<base>..<tip>` form, where _<base>_ is
128 -the commit just below the oldest commit to squash. For example, `git
129 -history squash HEAD~3..HEAD` folds the three most recent commits into
130 -one, and `git history squash HEAD~5..HEAD~2` squashes an interior range
131 -while leaving the two newest commits in place. Several revisions may be
132 -given, for example `HEAD~3..HEAD ^topic` to additionally exclude what is
133 -already on `topic`. Rev-list options may also be given, but any that would
134 -change how the range is walked are overridden with a warning.
135 -+
136 -The oldest commit's message is preserved by default, except that an `amend!`
137 -commit targeting it replaces its message. With `--reedit-message`, an editor
138 -opens pre-filled with the messages of all the folded commits so you can
139 -combine them. A merge commit inside the range is folded like any other, but
140 -the range must have a single base, so a range that reaches more than one entry
141 -point (for example a side branch that forked before the range and was later
142 -merged into it) is rejected.
143 -+
144 -A `fixup!`, `squash!`, or `amend!` commit is refused unless the commit it
145 -targets is also in the range, so the fold does not silently absorb a
146 -marker meant for a commit outside it. As an exception, a range made up entirely
147 -of markers for one target is combined into a single commit, keeping the last
148 -`amend!` message if there is one.
149 -+
150 -With `--reedit-message` the template mirrors `git rebase -i --autosquash`:
151 -each `fixup!`, `squash!`, or `amend!` is grouped under the commit it
152 -targets rather than shown in commit order. A `fixup!` message is dropped
153 -(commented out in full), a `squash!` keeps its body with only the marker
154 -subject commented, and an `amend!` replaces its target's message, unless
155 -a `squash!` folded into that target first, in which case it keeps its
156 -body like a `squash!`.
157 -+
158 -A branch or tag that points at a commit inside the range would be left
159 -dangling once those commits are folded away, so with the default
160 -`--update-refs=branches` the command refuses. Rerun with
161 -`--update-refs=head` to rewrite only the current branch and leave such
162 -refs pointing at the old commits.
163 -
116 OPTIONS
117 -------
118
@@ -171,8 +123,7 @@ OPTIONS
123 ref updates is generally safe.
124
125 `--reedit-message`::
174 - Open an editor to modify the rewritten commit's message. For `squash`
175 - the editor is pre-filled with the messages of all the folded commits.
126 + Open an editor to modify the target commit's message.
127
128 `--empty=(drop|keep|abort)`::
129 Control what happens when a commit becomes empty as a result of the
advice.c
-1
@@ -58,7 +58,6 @@ static struct {
58 [ADVICE_FETCH_SHOW_FORCED_UPDATES] = { "fetchShowForcedUpdates" },
59 [ADVICE_FORCE_DELETE_BRANCH] = { "forceDeleteBranch" },
60 [ADVICE_GRAFT_FILE_DEPRECATED] = { "graftFileDeprecated" },
61 - [ADVICE_HISTORY_UPDATE_REFS] = { "historyUpdateRefs" },
61 [ADVICE_IGNORED_HOOK] = { "ignoredHook" },
62 [ADVICE_IMPLICIT_IDENTITY] = { "implicitIdentity" },
63 [ADVICE_MERGE_CONFLICT] = { "mergeConflict" },
advice.h
-1
@@ -25,7 +25,6 @@ enum advice_type {
25 ADVICE_FETCH_SHOW_FORCED_UPDATES,
26 ADVICE_FORCE_DELETE_BRANCH,
27 ADVICE_GRAFT_FILE_DEPRECATED,
28 - ADVICE_HISTORY_UPDATE_REFS,
28 ADVICE_IGNORED_HOOK,
29 ADVICE_IMPLICIT_IDENTITY,
30 ADVICE_MERGE_CONFLICT,
builtin/history.c
+38 -514
@@ -1,7 +1,6 @@
1 #define USE_THE_REPOSITORY_VARIABLE
2
3 #include "builtin.h"
4 -#include "advice.h"
4 #include "cache-tree.h"
5 #include "commit.h"
6 #include "commit-reach.h"
@@ -35,8 +34,6 @@
34 N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
35 #define GIT_HISTORY_SPLIT_USAGE \
36 N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
38 -#define GIT_HISTORY_SQUASH_USAGE \
39 - N_("git history squash [--dry-run] [--update-refs=(branches|head)] [--reedit-message] <revision-range>")
37
38 static void change_data_free(void *util, const char *str UNUSED)
39 {
@@ -111,7 +108,6 @@ enum commit_tree_flags {
108 static int commit_tree_ext(struct repository *repo,
109 const char *action,
110 struct commit *commit_with_message,
114 - const char *message_template,
111 const struct commit_list *parents,
112 const struct object_id *old_tree,
113 const struct object_id *new_tree,
@@ -141,16 +137,13 @@ static int commit_tree_ext(struct repository *repo,
137 original_author = xmemdupz(ptr, len);
138 find_commit_subject(original_message, &original_body);
139
144 - if (!message_template)
145 - message_template = original_body;
146 -
140 if (flags & COMMIT_TREE_EDIT_MESSAGE) {
141 ret = fill_commit_message(repo, old_tree, new_tree,
149 - message_template, action, &commit_message);
142 + original_body, action, &commit_message);
143 if (ret < 0)
144 goto out;
145 } else {
153 - strbuf_addstr(&commit_message, message_template);
146 + strbuf_addstr(&commit_message, original_body);
147 }
148
149 original_extra_headers = read_commit_extra_headers(commit_with_message,
@@ -171,25 +164,6 @@ out:
164 return ret;
165 }
166
174 -static int first_parent_tree_oid(struct repository *repo,
175 - struct commit *commit,
176 - struct object_id *out)
177 -{
178 - struct commit *parent = commit->parents ? commit->parents->item : NULL;
179 -
180 - if (!parent) {
181 - oidcpy(out, repo->hash_algo->empty_tree);
182 - return 0;
183 - }
184 -
185 - if (repo_parse_commit(repo, parent))
186 - return error(_("unable to parse parent commit %s"),
187 - oid_to_hex(&parent->object.oid));
188 -
189 - oidcpy(out, &repo_get_commit_tree(repo, parent)->object.oid);
190 - return 0;
191 -}
192 -
167 static int commit_tree_with_edited_message(struct repository *repo,
168 const char *action,
169 struct commit *original,
@@ -197,13 +171,23 @@ static int commit_tree_with_edited_message(struct repository *repo,
171 {
172 struct object_id parent_tree_oid;
173 const struct object_id *tree_oid;
174 + struct commit *parent;
175
176 tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
177
203 - if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0)
204 - return -1;
178 + parent = original->parents ? original->parents->item : NULL;
179 + if (parent) {
180 + if (repo_parse_commit(repo, parent)) {
181 + return error(_("unable to parse parent commit %s"),
182 + oid_to_hex(&parent->object.oid));
183 + }
184 +
185 + parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
186 + } else {
187 + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
188 + }
189
206 - return commit_tree_ext(repo, action, original, NULL, original->parents,
190 + return commit_tree_ext(repo, action, original, original->parents,
191 &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
192 }
193
@@ -491,10 +475,18 @@ static int commit_became_empty(struct repository *repo,
475 struct commit *original,
476 struct tree *result)
477 {
478 + struct commit *parent = original->parents ? original->parents->item : NULL;
479 struct object_id parent_tree_oid;
480
496 - if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0)
497 - return -1;
481 + if (parent) {
482 + if (repo_parse_commit(repo, parent))
483 + return error(_("unable to parse parent of %s"),
484 + oid_to_hex(&original->object.oid));
485 +
486 + parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
487 + } else {
488 + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
489 + }
490
491 return oideq(&result->object.oid, &parent_tree_oid);
492 }
@@ -682,7 +674,7 @@ static int cmd_history_fixup(int argc,
674 goto out;
675
676 if (!skip_commit) {
685 - ret = commit_tree_ext(repo, "fixup", original, NULL, original->parents,
677 + ret = commit_tree_ext(repo, "fixup", original, original->parents,
678 &original_tree->object.oid, &merge_result.tree->object.oid,
679 &rewritten, flags);
680 if (ret < 0) {
@@ -838,9 +830,16 @@ static int split_commit(struct repository *repo,
830 struct tree *split_tree;
831 int ret;
832
841 - if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0) {
842 - ret = -1;
843 - goto out;
833 + if (original->parents) {
834 + if (repo_parse_commit(repo, original->parents->item)) {
835 + ret = error(_("unable to parse parent commit %s"),
836 + oid_to_hex(&original->parents->item->object.oid));
837 + goto out;
838 + }
839 +
840 + parent_tree_oid = *get_commit_tree_oid(original->parents->item);
841 + } else {
842 + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
843 }
844 original_commit_tree_oid = get_commit_tree_oid(original);
845
@@ -893,7 +892,7 @@ static int split_commit(struct repository *repo,
892 * The first commit is constructed from the split-out tree. The base
893 * that shall be diffed against is the parent of the original commit.
894 */
896 - ret = commit_tree_ext(repo, "split-out", original, NULL, original->parents, &parent_tree_oid,
895 + ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
896 &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
897 if (ret < 0) {
898 ret = error(_("failed writing first commit"));
@@ -910,7 +909,7 @@ static int split_commit(struct repository *repo,
909 old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid;
910 new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
911
913 - ret = commit_tree_ext(repo, "split-out", original, NULL, parents, old_tree_oid,
912 + ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
913 new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
914 if (ret < 0) {
915 ret = error(_("failed writing second commit"));
@@ -1007,479 +1006,6 @@ out:
1006 return ret;
1007 }
1008
1010 -/*
1011 - * Resolve a "<base>..<tip>" revision range into the base commit just outside
1012 - * the range (which becomes the parent of the squashed commit), the oldest
1013 - * commit contained in the range (whose message the squash reuses), and the
1014 - * range tip (whose tree becomes the result). A merge inside the range is fine,
1015 - * but the range must have a single base and must not reach a root commit.
1016 - */
1017 -static int resolve_squash_range(struct repository *repo,
1018 - const char **argv,
1019 - struct commit **base_out,
1020 - struct commit **oldest_out,
1021 - struct commit **tip_out,
1022 - struct oidset *interior_out)
1023 -{
1024 - struct rev_info revs;
1025 - struct commit *commit, *base = NULL, *oldest = NULL, *tip = NULL;
1026 - struct commit_list *boundaries = NULL, *b;
1027 - struct strvec args = STRVEC_INIT;
1028 - size_t i;
1029 - int ret;
1030 -
1031 - repo_init_revisions(repo, &revs, NULL);
1032 - revs.reverse = 1;
1033 - revs.topo_order = 1;
1034 - revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
1035 - revs.simplify_history = 0;
1036 - revs.boundary = 1;
1037 -
1038 - strvec_push(&args, "ignored");
1039 - strvec_push(&args, "--ancestry-path");
1040 - strvec_pushv(&args, argv);
1041 - setup_revisions_from_strvec(&args, &revs, NULL);
1042 - if (args.nr != 1) {
1043 - ret = error(_("unrecognized argument: %s"), args.v[1]);
1044 - goto out;
1045 - }
1046 -
1047 - if (revs.reverse != 1 || revs.topo_order != 1 ||
1048 - revs.sort_order != REV_SORT_IN_GRAPH_ORDER ||
1049 - revs.simplify_history != 0 || revs.boundary != 1) {
1050 - warning(_("ignoring rev-list options that would change how the "
1051 - "range is walked"));
1052 - revs.reverse = 1;
1053 - revs.topo_order = 1;
1054 - revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
1055 - revs.simplify_history = 0;
1056 - revs.boundary = 1;
1057 - }
1058 -
1059 - /*
1060 - * A squash needs a base to reparent onto, so the range has to exclude
1061 - * something, as in "<base>..<tip>". A revision range with no such
1062 - * bottom commit cannot be squashed.
1063 - */
1064 - for (i = 0; i < revs.cmdline.nr; i++)
1065 - if (revs.cmdline.rev[i].flags & UNINTERESTING)
1066 - break;
1067 - if (i == revs.cmdline.nr) {
1068 - ret = error(_("not a '<base>..<tip>' revision range"));
1069 - goto out;
1070 - }
1071 -
1072 - if (prepare_revision_walk(&revs) < 0) {
1073 - ret = error(_("error preparing revisions"));
1074 - goto out;
1075 - }
1076 -
1077 - /*
1078 - * Set boundary commits aside for the base check below, and put every
1079 - * in-range commit but the tip into the interior set. A ref pointing
1080 - * at an interior commit would dangle once the range is folded away.
1081 - */
1082 - while ((commit = get_revision(&revs))) {
1083 - if (commit->object.flags & BOUNDARY) {
1084 - commit_list_insert(commit, &boundaries);
1085 - continue;
1086 - }
1087 - if (!oldest)
1088 - oldest = commit;
1089 - if (tip)
1090 - oidset_insert(interior_out, &tip->object.oid);
1091 - tip = commit;
1092 - }
1093 -
1094 - if (!oldest) {
1095 - ret = error(_("the revision range is empty"));
1096 - goto out;
1097 - } else if (oldest == tip) {
1098 - ret = error(_("the revision range holds a single commit; "
1099 - "nothing to squash"));
1100 - goto out;
1101 - } else if (!oldest->parents) {
1102 - BUG("an in-range commit must have a parent");
1103 - }
1104 - base = oldest->parents->item;
1105 -
1106 - /*
1107 - * A boundary other than the base is an in-range commit reaching a
1108 - * commit outside the range, so the range has more than one base.
1109 - */
1110 - for (b = boundaries; b; b = b->next) {
1111 - if (b->item != base) {
1112 - ret = error(_("the revision range has more than one base; "
1113 - "cannot squash"));
1114 - goto out;
1115 - }
1116 - }
1117 -
1118 - *base_out = base;
1119 - *oldest_out = oldest;
1120 - *tip_out = tip;
1121 - ret = 0;
1122 -
1123 -out:
1124 - commit_list_free(boundaries);
1125 - reset_revision_walk();
1126 - release_revisions(&revs);
1127 - strvec_clear(&args);
1128 - return ret;
1129 -}
1130 -
1131 -static const char *autosquash_target(const char *subject)
1132 -{
1133 - const char *rest;
1134 -
1135 - while (skip_prefix(subject, "fixup! ", &rest) ||
1136 - skip_prefix(subject, "squash! ", &rest) ||
1137 - skip_prefix(subject, "amend! ", &rest))
1138 - subject = rest;
1139 - return subject;
1140 -}
1141 -
1142 -static int reject_dangling_fixups(struct repository *repo,
1143 - struct commit *base,
1144 - struct commit *tip,
1145 - struct commit *oldest,
1146 - struct commit **msg_source,
1147 - struct commit **amend_source)
1148 -{
1149 - struct todo_list todo = TODO_LIST_INIT;
1150 - struct replay_opts opts = REPLAY_OPTS_INIT;
1151 - struct rev_info revs;
1152 - struct commit *commit, *last_amend = NULL;
1153 - struct strvec args = STRVEC_INIT;
1154 - char *dangling_subject = NULL, *dangling_target = NULL;
1155 - bool mixed_target = false, all_fixups_one_target;
1156 - bool past_oldest_group = false;
1157 - int i, ret, nr_dangling = 0;
1158 -
1159 - *msg_source = oldest;
1160 - *amend_source = NULL;
1161 -
1162 - repo_init_revisions(repo, &revs, NULL);
1163 - strvec_push(&args, "ignored");
1164 - strvec_push(&args, "--reverse");
1165 - strvec_push(&args, "--topo-order");
1166 - strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid),
1167 - oid_to_hex(&tip->object.oid));
1168 - setup_revisions_from_strvec(&args, &revs, NULL);
1169 -
1170 - if (prepare_revision_walk(&revs) < 0) {
1171 - ret = error(_("error preparing revisions"));
1172 - goto out;
1173 - }
1174 - while ((commit = get_revision(&revs)))
1175 - strbuf_addf(&todo.buf, "pick %s\n",
1176 - oid_to_hex(&commit->object.oid));
1177 -
1178 - if (todo_list_parse_insn_buffer(repo, &opts, todo.buf.buf, &todo) < 0 ||
1179 - todo_list_rearrange_squash(&todo) < 0) {
1180 - ret = error(_("could not check the range for fixups"));
1181 - goto out;
1182 - }
1183 -
1184 - for (i = 0; i < todo.nr; i++) {
1185 - const char *message, *subject_start, *target;
1186 - char *subject;
1187 - size_t sublen;
1188 -
1189 - message = repo_logmsg_reencode(repo, todo.items[i].commit,
1190 - NULL, NULL);
1191 - sublen = find_commit_subject(message, &subject_start);
1192 -
1193 - if (todo.items[i].command != TODO_PICK) {
1194 - if (!past_oldest_group &&
1195 - starts_with(subject_start, "amend! "))
1196 - *amend_source = todo.items[i].commit;
1197 - repo_unuse_commit_buffer(repo, todo.items[i].commit, message);
1198 - continue;
1199 - }
1200 - if (i)
1201 - past_oldest_group = true;
1202 -
1203 - subject = xmemdupz(subject_start, sublen);
1204 - target = autosquash_target(subject);
1205 - if (target != subject) {
1206 - nr_dangling++;
1207 - if (!dangling_target) {
1208 - dangling_target = xstrdup(target);
1209 - dangling_subject = xstrdup(subject);
1210 - } else if (strcmp(dangling_target, target)) {
1211 - mixed_target = true;
1212 - }
1213 - if (starts_with(subject, "amend! "))
1214 - last_amend = todo.items[i].commit;
1215 - }
1216 - free(subject);
1217 - repo_unuse_commit_buffer(repo, todo.items[i].commit, message);
1218 - }
1219 -
1220 - all_fixups_one_target = nr_dangling == todo.nr && !mixed_target;
1221 - if (nr_dangling && !all_fixups_one_target) {
1222 - ret = error(_("cannot squash '%s': its target is not in the "
1223 - "range"), dangling_subject);
1224 - } else {
1225 - if (last_amend)
1226 - *msg_source = last_amend;
1227 - ret = 0;
1228 - }
1229 -
1230 -out:
1231 - free(dangling_subject);
1232 - free(dangling_target);
1233 - todo_list_release(&todo);
1234 - replay_opts_release(&opts);
1235 - reset_revision_walk();
1236 - release_revisions(&revs);
1237 - strvec_clear(&args);
1238 - return ret;
1239 -}
1240 -
1241 -struct interior_ref_cb {
1242 - const struct oidset *interior;
1243 - const char *name;
1244 -};
1245 -
1246 -static int find_interior_ref(const struct reference *ref, void *cb_data)
1247 -{
1248 - struct interior_ref_cb *data = cb_data;
1249 -
1250 - if (oidset_contains(data->interior, ref->oid)) {
1251 - data->name = xstrdup(ref->name);
1252 - return 1;
1253 - }
1254 -
1255 - return 0;
1256 -}
1257 -
1258 -static bool amend_replaces_target(struct todo_list *todo, int target)
1259 -{
1260 - int i;
1261 -
1262 - for (i = target + 1; i < todo->nr &&
1263 - todo->items[i].command != TODO_PICK; i++) {
1264 - if (todo->items[i].command == TODO_SQUASH)
1265 - return false;
1266 - if (todo->items[i].flags & TODO_REPLACE_FIXUP_MSG)
1267 - return true;
1268 - }
1269 - return false;
1270 -}
1271 -
1272 -static int build_squash_message(struct repository *repo,
1273 - struct commit *base,
1274 - struct commit *tip,
1275 - struct strbuf *out)
1276 -{
1277 - struct rev_info revs;
1278 - struct commit *commit;
1279 - struct strvec args = STRVEC_INIT;
1280 - struct todo_list todo = TODO_LIST_INIT;
1281 - struct replay_opts opts = REPLAY_OPTS_INIT;
1282 - int i, nr_commits, ret;
1283 -
1284 - repo_init_revisions(repo, &revs, NULL);
1285 - strvec_push(&args, "ignored");
1286 - strvec_push(&args, "--reverse");
1287 - strvec_push(&args, "--topo-order");
1288 - strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid),
1289 - oid_to_hex(&tip->object.oid));
1290 - setup_revisions_from_strvec(&args, &revs, NULL);
1291 -
1292 - if (prepare_revision_walk(&revs) < 0) {
1293 - ret = error(_("error preparing revisions"));
1294 - goto out;
1295 - }
1296 -
1297 - while ((commit = get_revision(&revs)))
1298 - strbuf_addf(&todo.buf, "pick %s\n",
1299 - oid_to_hex(&commit->object.oid));
1300 -
1301 - if (todo_list_parse_insn_buffer(repo, &opts, todo.buf.buf, &todo) < 0 ||
1302 - todo_list_rearrange_squash(&todo) < 0) {
1303 - ret = error(_("could not prepare the squash message"));
1304 - goto out;
1305 - }
1306 -
1307 - nr_commits = todo.nr;
1308 - for (i = 0; i < nr_commits; i++) {
1309 - struct todo_item *item = &todo.items[i];
1310 - const char *message, *body;
1311 - size_t commented_len;
1312 - bool skip, squashing;
1313 -
1314 - squashing = item->command == TODO_SQUASH ||
1315 - (item->flags & TODO_REPLACE_FIXUP_MSG);
1316 - if (item->command == TODO_PICK)
1317 - skip = amend_replaces_target(&todo, i);
1318 - else
1319 - skip = !squashing;
1320 -
1321 - message = repo_logmsg_reencode(repo, item->commit, NULL, NULL);
1322 - find_commit_subject(message, &body);
1323 -
1324 - if (skip)
1325 - commented_len = strlen(body);
1326 - else if (squashing)
1327 - commented_len = squash_subject_comment_len(body, 1);
1328 - else
1329 - commented_len = 0;
1330 -
1331 - if (!i)
1332 - add_squash_combination_header(out, nr_commits);
1333 - strbuf_addch(out, '\n');
1334 - add_squash_message_header(out, i + 1, skip);
1335 - strbuf_addstr(out, "\n\n");
1336 - strbuf_add_commented_lines(out, body, commented_len, comment_line_str);
1337 - strbuf_addstr(out, body + commented_len);
1338 - strbuf_complete_line(out);
1339 -
1340 - repo_unuse_commit_buffer(repo, item->commit, message);
1341 - }
1342 -
1343 - ret = 0;
1344 -
1345 -out:
1346 - todo_list_release(&todo);
1347 - replay_opts_release(&opts);
1348 - reset_revision_walk();
1349 - release_revisions(&revs);
1350 - strvec_clear(&args);
1351 - return ret;
1352 -}
1353 -
1354 -static int cmd_history_squash(int argc,
1355 - const char **argv,
1356 - const char *prefix,
1357 - struct repository *repo)
1358 -{
1359 - const char * const usage[] = {
1360 - GIT_HISTORY_SQUASH_USAGE,
1361 - NULL,
1362 - };
1363 - enum ref_action action = REF_ACTION_DEFAULT;
1364 - enum commit_tree_flags flags = 0;
1365 - int dry_run = 0;
1366 - struct option options[] = {
1367 - OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
1368 - N_("control which refs should be updated"),
1369 - PARSE_OPT_NONEG, parse_ref_action),
1370 - OPT_BOOL('n', "dry-run", &dry_run,
1371 - N_("perform a dry-run without updating any refs")),
1372 - OPT_BIT(0, "reedit-message", &flags,
1373 - N_("open an editor to modify the commit message"),
1374 - COMMIT_TREE_EDIT_MESSAGE),
1375 - OPT_END(),
1376 - };
1377 - struct strbuf reflog_msg = STRBUF_INIT;
1378 - struct strbuf message = STRBUF_INIT;
1379 - struct oidset interior = OIDSET_INIT;
1380 - struct commit *base, *oldest, *tip, *rewritten, *msg_source,
1381 - *amend_source;
1382 - const struct object_id *base_tree_oid, *tip_tree_oid;
1383 - const char *message_template = NULL;
1384 - struct commit_list *parents = NULL;
1385 - struct rev_info revs = { 0 };
1386 - int ret;
1387 -
1388 - argc = parse_options(argc, argv, prefix, options, usage,
1389 - PARSE_OPT_KEEP_UNKNOWN_OPT);
1390 - if (!argc) {
1391 - ret = error(_("command expects a revision range"));
1392 - goto out;
1393 - }
1394 - repo_config(repo, git_default_config, NULL);
1395 -
1396 - if (action == REF_ACTION_DEFAULT)
1397 - action = REF_ACTION_BRANCHES;
1398 -
1399 - ret = resolve_squash_range(repo, argv, &base, &oldest, &tip,
1400 - &interior);
1401 - if (ret < 0)
1402 - goto out;
1403 -
1404 - ret = reject_dangling_fixups(repo, base, tip, oldest, &msg_source,
1405 - &amend_source);
1406 - if (ret < 0)
1407 - goto out;
1408 - if (amend_source) {
1409 - const char *amend_message, *body;
1410 -
1411 - amend_message = repo_logmsg_reencode(repo, amend_source,
1412 - NULL, NULL);
1413 - find_commit_subject(amend_message, &body);
1414 - body = skip_blank_lines(body + commit_subject_length(body));
1415 - strbuf_addstr(&message, body);
1416 - message_template = message.buf;
1417 - repo_unuse_commit_buffer(repo, amend_source, amend_message);
1418 - }
1419 -
1420 - if (action == REF_ACTION_BRANCHES) {
1421 - struct interior_ref_cb cb = { .interior = &interior };
1422 -
1423 - refs_for_each_ref(get_main_ref_store(repo),
1424 - find_interior_ref, &cb);
1425 - if (cb.name) {
1426 - ret = error(_("'%s' points into the squashed range"),
1427 - cb.name);
1428 - advise_if_enabled(ADVICE_HISTORY_UPDATE_REFS,
1429 - _("Use --update-refs=head to rewrite only "
1430 - "the current branch and leave such refs "
1431 - "untouched."));
1432 - free((char *)cb.name);
1433 - goto out;
1434 - }
1435 - }
1436 -
1437 - if (flags & COMMIT_TREE_EDIT_MESSAGE) {
1438 - strbuf_reset(&message);
1439 - ret = build_squash_message(repo, base, tip, &message);
1440 - if (ret < 0)
1441 - goto out;
1442 - message_template = message.buf;
1443 - }
1444 -
1445 - ret = setup_revwalk(repo, action, tip, &revs);
1446 - if (ret < 0)
1447 - goto out;
1448 -
1449 - base_tree_oid = &repo_get_commit_tree(repo, base)->object.oid;
1450 - tip_tree_oid = &repo_get_commit_tree(repo, tip)->object.oid;
1451 - commit_list_append(base, &parents);
1452 -
1453 - ret = commit_tree_ext(repo, "squash", msg_source, message_template,
1454 - parents,
1455 - base_tree_oid, tip_tree_oid, &rewritten, flags);
1456 - if (ret < 0) {
1457 - ret = error(_("failed writing squashed commit"));
1458 - goto out;
1459 - }
1460 -
1461 - strbuf_addstr(&reflog_msg, "squash: updating ");
1462 - strbuf_join_argv(&reflog_msg, argc, argv, ' ');
1463 -
1464 - ret = handle_reference_updates(&revs, action, tip, rewritten,
1465 - reflog_msg.buf, dry_run,
1466 - REPLAY_EMPTY_COMMIT_ABORT);
1467 - if (ret < 0) {
1468 - ret = error(_("failed replaying descendants"));
1469 - goto out;
1470 - }
1471 -
1472 - ret = 0;
1473 -
1474 -out:
1475 - strbuf_release(&reflog_msg);
1476 - strbuf_release(&message);
1477 - oidset_clear(&interior);
1478 - commit_list_free(parents);
1479 - release_revisions(&revs);
1480 - return ret;
1481 -}
1482 -
1009 static int update_worktree(struct repository *repo,
1010 const struct commit *old_head,
1011 const struct commit *new_head,
@@ -1668,7 +1194,6 @@ int cmd_history(int argc,
1194 GIT_HISTORY_FIXUP_USAGE,
1195 GIT_HISTORY_REWORD_USAGE,
1196 GIT_HISTORY_SPLIT_USAGE,
1671 - GIT_HISTORY_SQUASH_USAGE,
1197 NULL,
1198 };
1199 parse_opt_subcommand_fn *fn = NULL;
@@ -1677,7 +1202,6 @@ int cmd_history(int argc,
1202 OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
1203 OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
1204 OPT_SUBCOMMAND("split", &fn, cmd_history_split),
1680 - OPT_SUBCOMMAND("squash", &fn, cmd_history_squash),
1205 OPT_END(),
1206 };
1207
sequencer.c
+31 -39
@@ -1880,38 +1880,18 @@ static int is_pick_or_similar(enum todo_command command)
1880 }
1881 }
1882
1883 +enum todo_item_flags {
1884 + TODO_EDIT_MERGE_MSG = (1 << 0),
1885 + TODO_REPLACE_FIXUP_MSG = (1 << 1),
1886 + TODO_EDIT_FIXUP_MSG = (1 << 2),
1887 +};
1888 +
1889 static const char first_commit_msg_str[] = N_("This is the 1st commit message:");
1890 static const char nth_commit_msg_fmt[] = N_("This is the commit message #%d:");
1891 static const char skip_first_commit_msg_str[] = N_("The 1st commit message will be skipped:");
1892 static const char skip_nth_commit_msg_fmt[] = N_("The commit message #%d will be skipped:");
1893 static const char combined_commit_msg_fmt[] = N_("This is a combination of %d commits.");
1894
1889 -void add_squash_combination_header(struct strbuf *buf, int n)
1890 -{
1891 - strbuf_addf(buf, "%s ", comment_line_str);
1892 - strbuf_addf(buf, _(combined_commit_msg_fmt), n);
1893 -}
1894 -
1895 -void add_squash_message_header(struct strbuf *buf, int n, int skip)
1896 -{
1897 - strbuf_addf(buf, "%s ", comment_line_str);
1898 - if (n == 1)
1899 - strbuf_addstr(buf, skip ? _(skip_first_commit_msg_str) :
1900 - _(first_commit_msg_str));
1901 - else
1902 - strbuf_addf(buf, skip ? _(skip_nth_commit_msg_fmt) :
1903 - _(nth_commit_msg_fmt), n);
1904 -}
1905 -
1906 -size_t squash_subject_comment_len(const char *body, int squashing)
1907 -{
1908 - if (starts_with(body, "amend!") ||
1909 - (squashing && (starts_with(body, "squash!") ||
1910 - starts_with(body, "fixup!"))))
1911 - return commit_subject_length(body);
1912 - return 0;
1913 -}
1914 -
1895 static int is_fixup_flag(enum todo_command command, unsigned flag)
1896 {
1897 return command == TODO_FIXUP && ((flag & TODO_REPLACE_FIXUP_MSG) ||
@@ -2032,13 +2012,20 @@ static int append_squash_message(struct strbuf *buf, const char *body,
2012 {
2013 struct replay_ctx *ctx = opts->ctx;
2014 const char *fixup_msg;
2035 - size_t commented_len, fixup_off;
2036 -
2037 - commented_len = squash_subject_comment_len(body,
2038 - command == TODO_SQUASH || seen_squash(ctx));
2015 + size_t commented_len = 0, fixup_off;
2016 + /*
2017 + * amend is non-interactive and not normally used with fixup!
2018 + * or squash! commits, so only comment out those subjects when
2019 + * squashing commit messages.
2020 + */
2021 + if (starts_with(body, "amend!") ||
2022 + ((command == TODO_SQUASH || seen_squash(ctx)) &&
2023 + (starts_with(body, "squash!") || starts_with(body, "fixup!"))))
2024 + commented_len = commit_subject_length(body);
2025
2040 - strbuf_addch(buf, '\n');
2041 - add_squash_message_header(buf, ++ctx->current_fixup_count + 1, 0);
2026 + strbuf_addf(buf, "\n%s ", comment_line_str);
2027 + strbuf_addf(buf, _(nth_commit_msg_fmt),
2028 + ++ctx->current_fixup_count + 1);
2029 strbuf_addstr(buf, "\n\n");
2030 strbuf_add_commented_lines(buf, body, commented_len, comment_line_str);
2031 /* buf->buf may be reallocated so store an offset into the buffer */
@@ -2103,8 +2090,9 @@ static int update_squash_messages(struct repository *r,
2090 eol = !starts_with(buf.buf, comment_line_str) ?
2091 buf.buf : strchrnul(buf.buf, '\n');
2092
2106 - add_squash_combination_header(&header,
2107 - ctx->current_fixup_count + 2);
2093 + strbuf_addf(&header, "%s ", comment_line_str);
2094 + strbuf_addf(&header, _(combined_commit_msg_fmt),
2095 + ctx->current_fixup_count + 2);
2096 strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);
2097 strbuf_release(&header);
2098 if (is_fixup_flag(command, flag) && !seen_squash(ctx))
@@ -2128,9 +2116,12 @@ static int update_squash_messages(struct repository *r,
2116 repo_unuse_commit_buffer(r, head_commit, head_message);
2117 return error(_("cannot write '%s'"), rebase_path_fixup_msg());
2118 }
2131 - add_squash_combination_header(&buf, 2);
2132 - strbuf_addch(&buf, '\n');
2133 - add_squash_message_header(&buf, 1, is_fixup_flag(command, flag));
2119 + strbuf_addf(&buf, "%s ", comment_line_str);
2120 + strbuf_addf(&buf, _(combined_commit_msg_fmt), 2);
2121 + strbuf_addf(&buf, "\n%s ", comment_line_str);
2122 + strbuf_addstr(&buf, is_fixup_flag(command, flag) ?
2123 + _(skip_first_commit_msg_str) :
2124 + _(first_commit_msg_str));
2125 strbuf_addstr(&buf, "\n\n");
2126 if (is_fixup_flag(command, flag))
2127 strbuf_add_commented_lines(&buf, body, strlen(body),
@@ -2149,8 +2140,9 @@ static int update_squash_messages(struct repository *r,
2140 if (command == TODO_SQUASH || is_fixup_flag(command, flag)) {
2141 res = append_squash_message(&buf, body, command, opts, flag);
2142 } else if (command == TODO_FIXUP) {
2152 - strbuf_addch(&buf, '\n');
2153 - add_squash_message_header(&buf, ++ctx->current_fixup_count + 1, 1);
2143 + strbuf_addf(&buf, "\n%s ", comment_line_str);
2144 + strbuf_addf(&buf, _(skip_nth_commit_msg_fmt),
2145 + ++ctx->current_fixup_count + 1);
2146 strbuf_addstr(&buf, "\n\n");
2147 strbuf_add_commented_lines(&buf, body, strlen(body),
2148 comment_line_str);
sequencer.h
-30
@@ -119,13 +119,6 @@ enum todo_command {
119 TODO_COMMENT
120 };
121
122 -/* Bits for the "flags" member of struct todo_item */
123 -enum todo_item_flags {
124 - TODO_EDIT_MERGE_MSG = (1 << 0),
125 - TODO_REPLACE_FIXUP_MSG = (1 << 1),
126 - TODO_EDIT_FIXUP_MSG = (1 << 2),
127 -};
128 -
122 struct todo_item {
123 enum todo_command command;
124 struct commit *commit;
@@ -215,29 +208,6 @@ int todo_list_rearrange_squash(struct todo_list *todo_list);
208 */
209 void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag);
210
218 -/*
219 - * Append the "This is a combination of N commits." banner that "git rebase
220 - * -i" writes at the top of a squashed commit's message, commented out with
221 - * the comment character.
222 - */
223 -void add_squash_combination_header(struct strbuf *buf, int n);
224 -
225 -/*
226 - * Append the header (1-based N) that "git rebase -i" writes above each message
227 - * when squashing, commented out with the comment character. With SKIP it reads
228 - * "The ... commit message will be skipped" for a message that is dropped (a
229 - * fixup), otherwise "This is the ... commit message".
230 - */
231 -void add_squash_message_header(struct strbuf *buf, int n, int skip);
232 -
233 -/*
234 - * Return the length of the leading subject of BODY when it should be commented
235 - * out in a squash message, or 0 otherwise. An "amend!" subject always
236 - * qualifies; "squash!" and "fixup!" subjects only when SQUASHING, since a
237 - * plain fixup chain keeps them.
238 - */
239 -size_t squash_subject_comment_len(const char *body, int squashing);
240 -
211 void append_conflicts_hint(struct index_state *istate,
212 struct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode);
213 enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg,
t/meson.build
-1
@@ -406,7 +406,6 @@ integration_tests = [
406 't3452-history-split.sh',
407 't3453-history-fixup.sh',
408 't3454-history-drop.sh',
409 - 't3455-history-squash.sh',
409 't3500-cherry.sh',
410 't3501-revert-cherry-pick.sh',
411 't3502-cherry-pick-merge.sh',
t/t3455-history-squash.sh deleted
-770
@@ -1,770 +0,0 @@
1 -#!/bin/sh
2 -
3 -test_description='tests for git-history squash subcommand'
4 -
5 -. ./test-lib.sh
6 -
7 -stage_file () {
8 - printf "%s\n" "$1" >file &&
9 - git add file
10 -}
11 -
12 -commit_with_message () {
13 - printf "%b" "$1" >msg &&
14 - git commit --allow-empty -qF msg
15 -}
16 -
17 -check_commit_count () {
18 - git rev-list --count "$1" >actual &&
19 - echo "$2" >expect &&
20 - test_cmp expect actual
21 -}
22 -
23 -check_log_subjects () {
24 - git log --format="%s" "$1" >actual &&
25 - cat >expect &&
26 - test_cmp expect actual
27 -}
28 -
29 -check_log_messages () {
30 - git log --format="%B" "$1" >actual &&
31 - cat >expect &&
32 - test_cmp expect actual
33 -}
34 -
35 -test_expect_success 'setup linear history touching two files' '
36 - test_commit base file a &&
37 - git tag start &&
38 - test_commit --no-tag one other x &&
39 - test_commit --no-tag two file c &&
40 - test_commit three file d
41 -'
42 -
43 -test_expect_success 'errors on missing range argument' '
44 - test_must_fail git history squash 2>err &&
45 - test_grep "expects a revision range" err
46 -'
47 -
48 -test_expect_success 'errors on an empty range' '
49 - test_must_fail git history squash HEAD..HEAD 2>err &&
50 - test_grep "the revision range is empty" err
51 -'
52 -
53 -test_expect_success 'errors on a single revision that is not a range' '
54 - test_must_fail git history squash HEAD 2>err &&
55 - test_grep "not a .*range" err &&
56 - test_must_fail git history squash HEAD~1 2>err &&
57 - test_grep "not a .*range" err
58 -'
59 -
60 -test_expect_success 'errors on a range holding a single commit' '
61 - git reset --hard three &&
62 - head_before=$(git rev-parse HEAD) &&
63 -
64 - test_must_fail git history squash "HEAD^!" 2>err &&
65 - test_grep "single commit; nothing to squash" err &&
66 - test_cmp_rev "$head_before" HEAD
67 -'
68 -
69 -test_expect_success 'accepts multiple revision arguments with an exclusion' '
70 - git reset --hard three &&
71 - git branch -f keep HEAD~2 &&
72 - tip_tree=$(git rev-parse HEAD^{tree}) &&
73 -
74 - git history squash start..HEAD ^keep &&
75 -
76 - git reflog -1 --format=%gs >actual &&
77 - echo "squash: updating start..HEAD ^keep" >expect &&
78 - test_cmp expect actual &&
79 -
80 - check_log_subjects start..HEAD <<-\EOF &&
81 - two
82 - one
83 - EOF
84 - test_cmp_rev keep HEAD~1 &&
85 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
86 -
87 - git branch -D keep
88 -'
89 -
90 -test_expect_success 'squashes a branch the current branch is not on' '
91 - git reset --hard three &&
92 - main=$(git symbolic-ref --short HEAD) &&
93 - head_before=$(git rev-parse HEAD) &&
94 - git checkout -b off-history start &&
95 - test_commit --no-tag off-one off a &&
96 - test_commit --no-tag off-two off b &&
97 - git checkout "$main" &&
98 -
99 - git history squash start..off-history &&
100 -
101 - check_commit_count start..off-history 1 &&
102 - test_cmp_rev "$head_before" HEAD &&
103 -
104 - git branch -D off-history
105 -'
106 -
107 -test_expect_success 'squashes a range into a single commit without changing the tree' '
108 - git reset --hard three &&
109 - head_before=$(git rev-parse HEAD) &&
110 - tip_tree=$(git rev-parse HEAD^{tree}) &&
111 -
112 - git history squash --dry-run start.. >out &&
113 - predicted=$(awk "/^update refs\/heads\// {print \$3}" out) &&
114 - test_cmp_rev "$head_before" HEAD &&
115 -
116 - git history squash start.. &&
117 -
118 - test "$predicted" = "$(git rev-parse HEAD)" &&
119 - check_commit_count start..HEAD 1 &&
120 - test_cmp_rev start HEAD^ &&
121 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
122 - check_log_subjects -1 <<-\EOF &&
123 - one
124 - EOF
125 - git reflog >reflog &&
126 - test_grep "squash: updating" reflog
127 -'
128 -
129 -test_expect_success 'sanitizes rev-list walk options, before and after --' '
130 - git reset --hard three &&
131 - tip_tree=$(git rev-parse HEAD^{tree}) &&
132 -
133 - git history squash --date-order start.. 2>err &&
134 - test_grep "ignoring rev-list options" err &&
135 - test_cmp_rev start HEAD^ &&
136 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
137 -
138 - git reset --hard three &&
139 - git history squash -- --reverse start.. 2>err &&
140 - test_grep "ignoring rev-list options" err &&
141 - test_cmp_rev start HEAD^ &&
142 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
143 -'
144 -
145 -test_expect_success 'squashes an interior range and replays descendants verbatim' '
146 - git reset --hard three &&
147 - final_tree=$(git rev-parse HEAD^{tree}) &&
148 -
149 - git history squash start..@~1 &&
150 -
151 - check_log_subjects start..HEAD <<-\EOF &&
152 - three
153 - one
154 - EOF
155 -
156 - test_cmp_rev start HEAD~2 &&
157 - test "$final_tree" = "$(git rev-parse HEAD^{tree})"
158 -'
159 -
160 -test_expect_success 'squashes when the base is the root commit' '
161 - git reset --hard three &&
162 - root=$(git rev-list --max-parents=0 HEAD) &&
163 - tip_tree=$(git rev-parse HEAD^{tree}) &&
164 -
165 - git history squash "$root.." &&
166 -
167 - check_commit_count "$root..HEAD" 1 &&
168 - test_cmp_rev "$root" HEAD^ &&
169 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
170 -'
171 -
172 -
173 -test_expect_success 'folds fixups whose target is in the range' '
174 - git reset --hard start &&
175 - test_commit --no-tag target file b &&
176 - git commit --allow-empty -m "fixup! target" &&
177 - git commit --allow-empty -m "fixup! target" &&
178 - test_commit --no-tag later file c &&
179 -
180 - git history squash start.. &&
181 -
182 - check_commit_count start..HEAD 1 &&
183 - check_log_subjects -1 <<-\EOF
184 - target
185 - EOF
186 -'
187 -
188 -test_expect_success 'refuses a below-range fixup! after an in-range commit' '
189 - git reset --hard start &&
190 - test_commit --no-tag inside file b &&
191 - test_commit --no-tag "fixup! outside" file c &&
192 - head_before=$(git rev-parse HEAD) &&
193 -
194 - test_must_fail git history squash start.. 2>err &&
195 - test_grep "target is not in the range" err &&
196 - test_cmp_rev "$head_before" HEAD
197 -'
198 -
199 -test_expect_success 'combines a run of fixups for one commit below the range' '
200 - git reset --hard start &&
201 - stage_file b && git commit -m "fixup! base" &&
202 - stage_file c && git commit -m "fixup! base" &&
203 -
204 - git history squash start.. &&
205 -
206 - check_commit_count start..HEAD 1 &&
207 - check_log_subjects -1 <<-\EOF
208 - fixup! base
209 - EOF
210 -'
211 -
212 -test_expect_success 'combining below-range fixups keeps the last amend! message' '
213 - git reset --hard start &&
214 - stage_file b && git commit -m "fixup! base" &&
215 - stage_file c &&
216 - commit_with_message "amend! base\n\namended body\n" &&
217 -
218 - git history squash start.. &&
219 -
220 - check_commit_count start..HEAD 1 &&
221 - check_log_messages -1 <<-\EOF
222 - amend! base
223 -
224 - amended body
225 -
226 - EOF
227 -'
228 -
229 -test_expect_success 'refuses fixups for two different commits below the range' '
230 - git reset --hard start &&
231 - stage_file b && git commit -m "fixup! aaa" &&
232 - stage_file c && git commit -m "fixup! bbb" &&
233 - head_before=$(git rev-parse HEAD) &&
234 -
235 - test_must_fail git history squash start.. 2>err &&
236 - test_grep "target is not in the range" err &&
237 - test_cmp_rev "$head_before" HEAD
238 -'
239 -
240 -test_expect_success 'the last amend! for the oldest commit replaces its message' '
241 - git reset --hard start &&
242 - test_commit --no-tag marker-oldest file b &&
243 - git commit --allow-empty -m "squash! marker-oldest" &&
244 - commit_with_message "amend! marker-oldest\n\nearlier message\n" &&
245 - commit_with_message \
246 - "amend! marker-oldest\n\namended subject\n\namended body\n" &&
247 - test_commit --no-tag marker-later file c &&
248 - commit_with_message "amend! marker-later\n\nwrong message\n" &&
249 -
250 - git history squash start.. &&
251 -
252 - check_commit_count start..HEAD 1 &&
253 - check_log_messages -1 <<-\EOF
254 - amended subject
255 -
256 - amended body
257 -
258 - EOF
259 -'
260 -
261 -test_expect_success 'preserves authorship of the oldest commit' '
262 - git reset --hard start &&
263 - GIT_AUTHOR_NAME=Squasher GIT_AUTHOR_EMAIL=squash@example.com \
264 - test_commit --no-tag oldest file b &&
265 - test_commit newest file c &&
266 -
267 - git history squash start.. &&
268 -
269 - git log -1 --format="%an <%ae>" >actual &&
270 - echo "Squasher <squash@example.com>" >expect &&
271 - test_cmp expect actual
272 -'
273 -
274 -test_expect_success '--reedit-message offers every folded-in message' '
275 - git reset --hard start &&
276 - stage_file b &&
277 - git commit -m "re-one subject" -m "re-one body line" &&
278 - test_commit --no-tag re-two file c &&
279 - test_commit re-three file d &&
280 -
281 - write_script editor <<-\EOF &&
282 - cat "$1" >edited &&
283 - echo combined >"$1"
284 - EOF
285 - test_set_editor "$(pwd)/editor" &&
286 - git history squash --reedit-message start.. &&
287 -
288 - cat >expect <<-EOF &&
289 - # This is a combination of 3 commits.
290 - # This is the 1st commit message:
291 -
292 - re-one subject
293 -
294 - re-one body line
295 -
296 - # This is the commit message #2:
297 -
298 - re-two
299 -
300 - # This is the commit message #3:
301 -
302 - re-three
303 -
304 - # Please enter the commit message for the squash changes. Lines starting
305 - # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
306 - # Changes to be committed:
307 - # modified: file
308 - #
309 - EOF
310 - test_cmp expect edited &&
311 - check_log_subjects -1 <<-\EOF
312 - combined
313 - EOF
314 -'
315 -
316 -test_expect_success '--reedit-message handles fixup!, squash! and amend! like rebase' '
317 - git reset --hard start &&
318 - test_commit --no-tag mark-base file b &&
319 - stage_file c &&
320 - commit_with_message "fixup! mark-base\n\nfixup body\n" &&
321 - stage_file d &&
322 - commit_with_message "squash! mark-base\n\nsquash remark\n" &&
323 - stage_file e &&
324 - commit_with_message "amend! mark-base\n\namended message\n" &&
325 -
326 - write_script editor <<-\EOF &&
327 - cat "$1" >edited
328 - EOF
329 - test_set_editor "$(pwd)/editor" &&
330 - git history squash --reedit-message start.. &&
331 -
332 - cat >expect <<-EOF &&
333 - # This is a combination of 4 commits.
334 - # This is the 1st commit message:
335 -
336 - mark-base
337 -
338 - # The commit message #2 will be skipped:
339 -
340 - # fixup! mark-base
341 - #
342 - # fixup body
343 -
344 - # This is the commit message #3:
345 -
346 - # squash! mark-base
347 -
348 - squash remark
349 -
350 - # This is the commit message #4:
351 -
352 - # amend! mark-base
353 -
354 - amended message
355 -
356 - # Please enter the commit message for the squash changes. Lines starting
357 - # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
358 - # Changes to be committed:
359 - # modified: file
360 - #
361 - EOF
362 - test_cmp expect edited &&
363 - check_log_messages -1 <<-\EOF
364 - mark-base
365 -
366 - squash remark
367 -
368 - amended message
369 -
370 - EOF
371 -'
372 -
373 -test_expect_success '--reedit-message groups fixups under their targets' '
374 - git reset --hard start &&
375 - test_commit --no-tag alpha file a1 &&
376 - test_commit --no-tag beta file b1 &&
377 - stage_file a2 &&
378 - commit_with_message "fixup! alpha\n" &&
379 - stage_file b2 &&
380 - commit_with_message "fixup! beta\n" &&
381 -
382 - write_script editor <<-\EOF &&
383 - cat "$1" >edited
384 - EOF
385 - test_set_editor "$(pwd)/editor" &&
386 - git history squash --reedit-message start.. &&
387 -
388 - cat >expect <<-EOF &&
389 - # This is a combination of 4 commits.
390 - # This is the 1st commit message:
391 -
392 - alpha
393 -
394 - # The commit message #2 will be skipped:
395 -
396 - # fixup! alpha
397 -
398 - # This is the commit message #3:
399 -
400 - beta
401 -
402 - # The commit message #4 will be skipped:
403 -
404 - # fixup! beta
405 -
406 - # Please enter the commit message for the squash changes. Lines starting
407 - # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
408 - # Changes to be committed:
409 - # modified: file
410 - #
411 - EOF
412 - test_cmp expect edited
413 -'
414 -
415 -test_expect_success '--reedit-message lets amend! replace its target message' '
416 - git reset --hard start &&
417 - test_commit --no-tag mark-base file b &&
418 - stage_file c &&
419 - commit_with_message "amend! mark-base\n\namended message\n" &&
420 - stage_file d &&
421 - commit_with_message "squash! mark-base\n\nsquash remark\n" &&
422 -
423 - write_script editor <<-\EOF &&
424 - cat "$1" >edited
425 - EOF
426 - test_set_editor "$(pwd)/editor" &&
427 - git history squash --reedit-message start.. &&
428 -
429 - cat >expect <<-EOF &&
430 - # This is a combination of 3 commits.
431 - # The 1st commit message will be skipped:
432 -
433 - # mark-base
434 -
435 - # This is the commit message #2:
436 -
437 - # amend! mark-base
438 -
439 - amended message
440 -
441 - # This is the commit message #3:
442 -
443 - # squash! mark-base
444 -
445 - squash remark
446 -
447 - # Please enter the commit message for the squash changes. Lines starting
448 - # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
449 - # Changes to be committed:
450 - # modified: file
451 - #
452 - EOF
453 - test_cmp expect edited &&
454 - check_log_messages -1 <<-\EOF
455 - amended message
456 -
457 - squash remark
458 -
459 - EOF
460 -'
461 -
462 -test_expect_success '--reedit-message aborts on an empty message' '
463 - git reset --hard three &&
464 - head_before=$(git rev-parse HEAD) &&
465 -
466 - write_script editor <<-\EOF &&
467 - >"$1"
468 - EOF
469 - test_set_editor "$(pwd)/editor" &&
470 - test_must_fail git history squash --reedit-message start.. &&
471 -
472 - test_cmp_rev "$head_before" HEAD
473 -'
474 -
475 -test_expect_success '--update-refs=head only moves HEAD' '
476 - git reset --hard three &&
477 - git branch -f other HEAD &&
478 - other_before=$(git rev-parse other) &&
479 -
480 - git history squash --update-refs=head start.. &&
481 -
482 - check_commit_count start..HEAD 1 &&
483 - test_cmp_rev "$other_before" other
484 -'
485 -
486 -test_expect_success 'refuses to fold a range a ref points into' '
487 - git reset --hard three &&
488 - git branch -f mid HEAD~1 &&
489 - head_before=$(git rev-parse HEAD) &&
490 -
491 - test_must_fail git history squash start.. 2>err &&
492 - test_grep "error: .* points into the squashed range" err &&
493 - test_grep "hint: .*--update-refs=head" err &&
494 - test_cmp_rev "$head_before" HEAD &&
495 -
496 - git branch -D mid
497 -'
498 -
499 -test_expect_success 'advice.historyUpdateRefs silences the hint' '
500 - git reset --hard three &&
501 - git branch -f mid HEAD~1 &&
502 - head_before=$(git rev-parse HEAD) &&
503 -
504 - test_must_fail git -c advice.historyUpdateRefs=false \
505 - history squash start.. 2>err &&
506 - test_grep "points into the squashed range" err &&
507 - test_grep ! "hint:" err &&
508 - test_cmp_rev "$head_before" HEAD &&
509 -
510 - git branch -D mid
511 -'
512 -
513 -test_expect_success '--update-refs=head folds past a ref pointing into the range' '
514 - git reset --hard three &&
515 - git branch -f mid HEAD~1 &&
516 - mid_before=$(git rev-parse mid) &&
517 -
518 - git history squash --update-refs=head start.. &&
519 -
520 - check_commit_count start..HEAD 1 &&
521 - test_cmp_rev "$mid_before" mid &&
522 -
523 - git branch -D mid
524 -'
525 -
526 -test_expect_success 'refuses to fold a range a tag points into' '
527 - git reset --hard three &&
528 - git tag -f mark HEAD~1 &&
529 - head_before=$(git rev-parse HEAD) &&
530 -
531 - test_must_fail git history squash start.. 2>err &&
532 - test_grep "refs/tags/mark" err &&
533 - test_grep "points into the squashed range" err &&
534 - test_cmp_rev "$head_before" HEAD &&
535 -
536 - git tag -d mark
537 -'
538 -
539 -test_expect_success 'squashes a range whose internal merge has a single base' '
540 - git reset --hard start &&
541 - main=$(git symbolic-ref --short HEAD) &&
542 - test_commit --no-tag before-side file b &&
543 - git checkout -b inner-side &&
544 - test_commit --no-tag on-inner-side inner x &&
545 - git checkout "$main" &&
546 - test_commit --no-tag after-side file c &&
547 - git merge --no-ff -m merge inner-side &&
548 - git branch -D inner-side &&
549 - test_commit --no-tag after-merge file d &&
550 - tip_tree=$(git rev-parse HEAD^{tree}) &&
551 -
552 - git history squash start.. &&
553 -
554 - check_commit_count start..HEAD 1 &&
555 - check_log_subjects -1 <<-\EOF &&
556 - before-side
557 - EOF
558 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
559 - test_path_is_file inner
560 -'
561 -
562 -test_expect_success 'folds a merge of a branch that forked at the base' '
563 - git reset --hard start &&
564 - main=$(git symbolic-ref --short HEAD) &&
565 - git checkout -b base-fork-side &&
566 - test_commit --no-tag base-fork-side side x &&
567 - git checkout "$main" &&
568 - test_commit --no-tag base-fork-main file b &&
569 - git merge --no-ff -m "merge base-fork-side" base-fork-side &&
570 - git branch -D base-fork-side &&
571 - test_commit --no-tag base-fork-tail file c &&
572 - tip_tree=$(git rev-parse HEAD^{tree}) &&
573 -
574 - git history squash start.. &&
575 -
576 - check_commit_count start..HEAD 1 &&
577 - test_cmp_rev start HEAD^ &&
578 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
579 - test_path_is_file side
580 -'
581 -
582 -test_expect_success 'refuses a merge whose other parent is outside the range' '
583 - git reset --hard start &&
584 - main=$(git symbolic-ref --short HEAD) &&
585 - git checkout -b outside-parent &&
586 - test_commit --no-tag outside-parent outside x &&
587 - git checkout "$main" &&
588 - test_commit --no-tag outside-main file b &&
589 - base=$(git rev-parse HEAD) &&
590 - test_commit --no-tag outside-mid file c &&
591 - git merge --no-ff -m "merge outside-parent" outside-parent &&
592 - git branch -D outside-parent &&
593 - merged=$(git rev-parse HEAD) &&
594 -
595 - test_must_fail git history squash "$base.." 2>err &&
596 - test_grep "more than one base" err &&
597 - test_cmp_rev "$merged" HEAD
598 -'
599 -
600 -test_expect_success 'folds a range whose tip is a merge commit' '
601 - git reset --hard start &&
602 - main=$(git symbolic-ref --short HEAD) &&
603 - test_commit --no-tag tipmerge-base file b &&
604 - git checkout -b tipmerge-side &&
605 - test_commit --no-tag tipmerge-side side x &&
606 - git checkout "$main" &&
607 - test_commit --no-tag tipmerge-main file c &&
608 - git merge --no-ff -m "merge tipmerge-side" tipmerge-side &&
609 - git branch -D tipmerge-side &&
610 - tip_tree=$(git rev-parse HEAD^{tree}) &&
611 -
612 - git history squash start.. &&
613 -
614 - check_commit_count start..HEAD 1 &&
615 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
616 - test_path_is_file side
617 -'
618 -
619 -test_expect_success 'folds a range whose base is a merge commit' '
620 - git reset --hard start &&
621 - main=$(git symbolic-ref --short HEAD) &&
622 - git checkout -b basemerge-side &&
623 - test_commit --no-tag basemerge-side side x &&
624 - git checkout "$main" &&
625 - test_commit --no-tag basemerge-main file b &&
626 - git merge --no-ff -m "merge basemerge-side" basemerge-side &&
627 - git branch -D basemerge-side &&
628 - base=$(git rev-parse HEAD) &&
629 - test_commit --no-tag basemerge-one file c &&
630 - test_commit --no-tag basemerge-two file d &&
631 - tip_tree=$(git rev-parse HEAD^{tree}) &&
632 -
633 - git history squash "$base.." &&
634 -
635 - check_commit_count "$base..HEAD" 1 &&
636 - test_cmp_rev "$base" HEAD^ &&
637 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
638 -'
639 -
640 -test_expect_success 'folds a range with two interior merges' '
641 - git reset --hard start &&
642 - main=$(git symbolic-ref --short HEAD) &&
643 - test_commit --no-tag two-merge-a file a1 &&
644 - git checkout -b two-merge-s1 &&
645 - test_commit --no-tag two-merge-s1 s1 x &&
646 - git checkout "$main" &&
647 - git merge --no-ff -m "merge s1" two-merge-s1 &&
648 - test_commit --no-tag two-merge-b file b1 &&
649 - git checkout -b two-merge-s2 &&
650 - test_commit --no-tag two-merge-s2 s2 y &&
651 - git checkout "$main" &&
652 - git merge --no-ff -m "merge s2" two-merge-s2 &&
653 - git branch -D two-merge-s1 two-merge-s2 &&
654 - tip_tree=$(git rev-parse HEAD^{tree}) &&
655 -
656 - git history squash start.. &&
657 -
658 - check_commit_count start..HEAD 1 &&
659 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
660 - test_path_is_file s1 &&
661 - test_path_is_file s2
662 -'
663 -
664 -test_expect_success 'folds a range with a nested merge' '
665 - git reset --hard start &&
666 - main=$(git symbolic-ref --short HEAD) &&
667 - git checkout -b nested-outer &&
668 - test_commit --no-tag nested-outer outer x &&
669 - git checkout -b nested-inner &&
670 - test_commit --no-tag nested-inner inner y &&
671 - git checkout nested-outer &&
672 - git merge --no-ff -m "merge inner" nested-inner &&
673 - git checkout "$main" &&
674 - test_commit --no-tag nested-main file b1 &&
675 - git merge --no-ff -m "merge outer" nested-outer &&
676 - git branch -D nested-outer nested-inner &&
677 - tip_tree=$(git rev-parse HEAD^{tree}) &&
678 -
679 - git history squash start.. &&
680 -
681 - check_commit_count start..HEAD 1 &&
682 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
683 - test_path_is_file outer &&
684 - test_path_is_file inner
685 -'
686 -
687 -test_expect_success 'folds a range with an octopus merge' '
688 - git reset --hard start &&
689 - main=$(git symbolic-ref --short HEAD) &&
690 - test_commit --no-tag octo-base file a1 &&
691 - git checkout -b octo-1 &&
692 - test_commit --no-tag octo-1 o1 x &&
693 - git checkout "$main" &&
694 - git checkout -b octo-2 &&
695 - test_commit --no-tag octo-2 o2 y &&
696 - git checkout "$main" &&
697 - git merge --no-ff -m octopus octo-1 octo-2 &&
698 - git branch -D octo-1 octo-2 &&
699 - tip_tree=$(git rev-parse HEAD^{tree}) &&
700 -
701 - git history squash start.. &&
702 -
703 - check_commit_count start..HEAD 1 &&
704 - test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
705 - test_path_is_file o1 &&
706 - test_path_is_file o2
707 -'
708 -
709 -test_expect_success 'refuses an octopus merge with an arm forked before the base' '
710 - git reset --hard start &&
711 - main=$(git symbolic-ref --short HEAD) &&
712 - git checkout -b octo-pre &&
713 - test_commit octo-pre-side pside x &&
714 - git checkout "$main" &&
715 - test_commit octo-pre-main file b1 &&
716 - octo_base=$(git rev-parse HEAD) &&
717 - git checkout -b octo-within &&
718 - test_commit --no-tag octo-within wside y &&
719 - git checkout "$main" &&
720 - git merge --no-ff -m octopus octo-pre octo-within &&
721 - merged=$(git rev-parse HEAD) &&
722 - git branch -D octo-pre octo-within &&
723 -
724 - test_must_fail git history squash "$octo_base.." 2>err &&
725 - test_grep "more than one base" err &&
726 - test_cmp_rev "$merged" HEAD
727 -'
728 -
729 -test_expect_success 'refuses when a descendant above the range is a merge' '
730 - git reset --hard start &&
731 - main=$(git symbolic-ref --short HEAD) &&
732 - test_commit --no-tag desc-one file b &&
733 - test_commit --no-tag desc-two file c &&
734 - git tag desc-tip &&
735 - git checkout -b desc-above &&
736 - test_commit --no-tag desc-above above x &&
737 - git checkout "$main" &&
738 - test_commit --no-tag desc-main file d &&
739 - git merge --no-ff -m "merge desc-above" desc-above &&
740 - git branch -D desc-above &&
741 - head_before=$(git rev-parse HEAD) &&
742 -
743 - test_must_fail git history squash start..desc-tip 2>err &&
744 - test_grep "merge commits is not supported" err &&
745 - test_cmp_rev "$head_before" HEAD
746 -'
747 -
748 -test_expect_success 'refuses to fold a range a ref points into at a merge' '
749 - git reset --hard start &&
750 - main=$(git symbolic-ref --short HEAD) &&
751 - test_commit --no-tag refmerge-base file b &&
752 - git checkout -b refmerge-side &&
753 - test_commit --no-tag refmerge-side side x &&
754 - git checkout "$main" &&
755 - test_commit --no-tag refmerge-main file c &&
756 - git merge --no-ff -m "interior merge" refmerge-side &&
757 - git branch -D refmerge-side &&
758 - git branch at-merge HEAD &&
759 - test_commit --no-tag refmerge-tail file d &&
760 - head_before=$(git rev-parse HEAD) &&
761 -
762 - test_must_fail git history squash start.. 2>err &&
763 - test_grep "at-merge" err &&
764 - test_grep "points into the squashed range" err &&
765 - test_cmp_rev "$head_before" HEAD &&
766 -
767 - git branch -D at-merge
768 -'
769 -
770 -test_done