|
17 | 17 | #include "read-cache.h" |
18 | 18 | #include "refs.h" |
19 | 19 | #include "replay.h" |
| 20 | +#include "reset.h" |
20 | 21 | #include "revision.h" |
21 | 22 | #include "sequencer.h" |
22 | 23 | #include "strvec.h" |
23 | 24 | #include "tree.h" |
| 25 | +#include "tree-walk.h" |
24 | 26 | #include "unpack-trees.h" |
25 | 27 | #include "wt-status.h" |
26 | 28 |
|
| 29 | +#define GIT_HISTORY_DROP_USAGE \ |
| 30 | + N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]") |
27 | 31 | #define GIT_HISTORY_FIXUP_USAGE \ |
28 | 32 | N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]") |
29 | 33 | #define GIT_HISTORY_REWORD_USAGE \ |
@@ -1001,19 +1005,201 @@ static int cmd_history_split(int argc, |
1001 | 1005 | return ret; |
1002 | 1006 | } |
1003 | 1007 |
|
| 1008 | +static int update_worktree(struct repository *repo, |
| 1009 | + const struct commit *old_head, |
| 1010 | + const struct commit *new_head, |
| 1011 | + bool dry_run) |
| 1012 | +{ |
| 1013 | + struct reset_working_tree_options opts = { |
| 1014 | + .oid_from = &old_head->object.oid, |
| 1015 | + .oid = &new_head->object.oid, |
| 1016 | + }; |
| 1017 | + if (dry_run) |
| 1018 | + opts.flags |= RESET_WORKING_TREE_DRY_RUN; |
| 1019 | + return reset_working_tree(repo, &opts); |
| 1020 | +} |
| 1021 | + |
| 1022 | +static int find_head_tree_change(struct repository *repo, |
| 1023 | + const struct replay_result *result, |
| 1024 | + struct commit **old_head, |
| 1025 | + struct commit **new_head, |
| 1026 | + bool *changed) |
| 1027 | +{ |
| 1028 | + const struct replay_ref_update *head_update = NULL; |
| 1029 | + struct commit *old_head_commit, *new_head_commit; |
| 1030 | + struct tree *old_head_tree, *new_head_tree; |
| 1031 | + const char *head_target; |
| 1032 | + int head_flags; |
| 1033 | + |
| 1034 | + *changed = false; |
| 1035 | + |
| 1036 | + head_target = refs_resolve_ref_unsafe(get_main_ref_store(repo), |
| 1037 | + "HEAD", RESOLVE_REF_NO_RECURSE, |
| 1038 | + NULL, &head_flags); |
| 1039 | + if (!head_target) |
| 1040 | + return error(_("cannot look up HEAD")); |
| 1041 | + if (!(head_flags & REF_ISSYMREF)) |
| 1042 | + head_target = "HEAD"; |
| 1043 | + |
| 1044 | + for (size_t i = 0; i < result->updates_nr; i++) { |
| 1045 | + if (!strcmp(result->updates[i].refname, head_target)) { |
| 1046 | + head_update = &result->updates[i]; |
| 1047 | + break; |
| 1048 | + } |
| 1049 | + } |
| 1050 | + |
| 1051 | + if (!head_update) |
| 1052 | + return 0; |
| 1053 | + |
| 1054 | + old_head_commit = lookup_commit_reference(repo, &head_update->old_oid); |
| 1055 | + new_head_commit = lookup_commit_reference(repo, &head_update->new_oid); |
| 1056 | + if (!old_head_commit || !new_head_commit) |
| 1057 | + return error(_("cannot resolve HEAD commit")); |
| 1058 | + |
| 1059 | + old_head_tree = repo_get_commit_tree(repo, old_head_commit); |
| 1060 | + new_head_tree = repo_get_commit_tree(repo, new_head_commit); |
| 1061 | + if (!old_head_tree || !new_head_tree) |
| 1062 | + return error(_("cannot resolve tree for HEAD")); |
| 1063 | + |
| 1064 | + if (oideq(&old_head_tree->object.oid, &new_head_tree->object.oid)) |
| 1065 | + return 0; |
| 1066 | + |
| 1067 | + *old_head = old_head_commit; |
| 1068 | + *new_head = new_head_commit; |
| 1069 | + *changed = true; |
| 1070 | + |
| 1071 | + return 0; |
| 1072 | +} |
| 1073 | + |
| 1074 | +static int cmd_history_drop(int argc, |
| 1075 | + const char **argv, |
| 1076 | + const char *prefix, |
| 1077 | + struct repository *repo) |
| 1078 | +{ |
| 1079 | + const char * const usage[] = { |
| 1080 | + GIT_HISTORY_DROP_USAGE, |
| 1081 | + NULL, |
| 1082 | + }; |
| 1083 | + enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP; |
| 1084 | + enum ref_action action = REF_ACTION_DEFAULT; |
| 1085 | + int dry_run = 0; |
| 1086 | + struct option options[] = { |
| 1087 | + OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)", |
| 1088 | + N_("control which refs should be updated"), |
| 1089 | + PARSE_OPT_NONEG, parse_ref_action), |
| 1090 | + OPT_BOOL('n', "dry-run", &dry_run, |
| 1091 | + N_("perform a dry-run without updating any refs")), |
| 1092 | + OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)", |
| 1093 | + N_("how to handle descendants that become empty"), |
| 1094 | + PARSE_OPT_NONEG, parse_opt_empty), |
| 1095 | + OPT_END(), |
| 1096 | + }; |
| 1097 | + struct strbuf reflog_msg = STRBUF_INIT; |
| 1098 | + struct commit *original, *rewritten; |
| 1099 | + struct rev_info revs = { 0 }; |
| 1100 | + struct replay_result result = { 0 }; |
| 1101 | + struct commit *old_head, *new_head; |
| 1102 | + bool head_moves = false; |
| 1103 | + int ret; |
| 1104 | + |
| 1105 | + argc = parse_options(argc, argv, prefix, options, usage, 0); |
| 1106 | + if (argc != 1) { |
| 1107 | + ret = error(_("command expects a single revision")); |
| 1108 | + goto out; |
| 1109 | + } |
| 1110 | + repo_config(repo, git_default_config, NULL); |
| 1111 | + |
| 1112 | + if (action == REF_ACTION_DEFAULT) |
| 1113 | + action = REF_ACTION_BRANCHES; |
| 1114 | + |
| 1115 | + original = lookup_commit_reference_by_name(argv[0]); |
| 1116 | + if (!original) { |
| 1117 | + ret = error(_("commit cannot be found: %s"), argv[0]); |
| 1118 | + goto out; |
| 1119 | + } |
| 1120 | + |
| 1121 | + if (!original->parents) { |
| 1122 | + ret = error(_("cannot drop root commit %s: " |
| 1123 | + "it has no parent to replay onto"), |
| 1124 | + argv[0]); |
| 1125 | + goto out; |
| 1126 | + } else if (original->parents->next) { |
| 1127 | + ret = error(_("cannot drop merge commit: %s"), argv[0]); |
| 1128 | + goto out; |
| 1129 | + } |
| 1130 | + |
| 1131 | + ret = setup_revwalk(repo, action, original, &revs); |
| 1132 | + if (ret) |
| 1133 | + goto out; |
| 1134 | + |
| 1135 | + rewritten = original->parents->item; |
| 1136 | + |
| 1137 | + ret = compute_pending_ref_updates(&revs, action, original, rewritten, |
| 1138 | + empty, &result); |
| 1139 | + if (ret) { |
| 1140 | + ret = error(_("failed replaying descendants")); |
| 1141 | + goto out; |
| 1142 | + } |
| 1143 | + |
| 1144 | + /* |
| 1145 | + * If HEAD will move as a result of the rewrite then we'll have to |
| 1146 | + * merge in the changes into the worktree and index. This merge can of |
| 1147 | + * course conflict, which will cause the whole operation to abort. |
| 1148 | + * |
| 1149 | + * If we had already updated the refs at that point then we'd have an |
| 1150 | + * inconsistent repository state. So we first perform a dry-run merge |
| 1151 | + * here before updating refs. |
| 1152 | + */ |
| 1153 | + if (!is_bare_repository()) { |
| 1154 | + ret = find_head_tree_change(repo, &result, &old_head, |
| 1155 | + &new_head, &head_moves); |
| 1156 | + if (ret < 0) |
| 1157 | + goto out; |
| 1158 | + |
| 1159 | + if (head_moves && update_worktree(repo, old_head, new_head, true) < 0) { |
| 1160 | + ret = error(_("dropping this commit would " |
| 1161 | + "overwrite local changes; aborting")); |
| 1162 | + goto out; |
| 1163 | + } |
| 1164 | + } |
| 1165 | + |
| 1166 | + strbuf_addf(&reflog_msg, "drop: dropping %s", argv[0]); |
| 1167 | + ret = apply_pending_ref_updates(repo, &result, reflog_msg.buf, dry_run); |
| 1168 | + if (ret < 0) { |
| 1169 | + ret = error(_("failed to update references")); |
| 1170 | + goto out; |
| 1171 | + } |
| 1172 | + |
| 1173 | + if (!dry_run && head_moves && update_worktree(repo, old_head, new_head, false) < 0) { |
| 1174 | + ret = error(_("could not update working tree to new commit %s"), |
| 1175 | + oid_to_hex(&new_head->object.oid)); |
| 1176 | + goto out; |
| 1177 | + } |
| 1178 | + |
| 1179 | + ret = 0; |
| 1180 | + |
| 1181 | +out: |
| 1182 | + replay_result_release(&result); |
| 1183 | + strbuf_release(&reflog_msg); |
| 1184 | + release_revisions(&revs); |
| 1185 | + return ret; |
| 1186 | +} |
| 1187 | + |
1004 | 1188 | int cmd_history(int argc, |
1005 | 1189 | const char **argv, |
1006 | 1190 | const char *prefix, |
1007 | 1191 | struct repository *repo) |
1008 | 1192 | { |
1009 | 1193 | const char * const usage[] = { |
| 1194 | + GIT_HISTORY_DROP_USAGE, |
1010 | 1195 | GIT_HISTORY_FIXUP_USAGE, |
1011 | 1196 | GIT_HISTORY_REWORD_USAGE, |
1012 | 1197 | GIT_HISTORY_SPLIT_USAGE, |
1013 | 1198 | NULL, |
1014 | 1199 | }; |
1015 | 1200 | parse_opt_subcommand_fn *fn = NULL; |
1016 | 1201 | struct option options[] = { |
| 1202 | + OPT_SUBCOMMAND("drop", &fn, cmd_history_drop), |
1017 | 1203 | OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup), |
1018 | 1204 | OPT_SUBCOMMAND("reword", &fn, cmd_history_reword), |
1019 | 1205 | OPT_SUBCOMMAND("split", &fn, cmd_history_split), |
|
0 commit comments