Git fork
at reftables-rust 470 lines 14 kB view raw
1/* 2 * "git replay" builtin command 3 */ 4 5#define USE_THE_REPOSITORY_VARIABLE 6#define DISABLE_SIGN_COMPARE_WARNINGS 7 8#include "git-compat-util.h" 9 10#include "builtin.h" 11#include "environment.h" 12#include "hex.h" 13#include "lockfile.h" 14#include "merge-ort.h" 15#include "object-name.h" 16#include "parse-options.h" 17#include "refs.h" 18#include "revision.h" 19#include "strmap.h" 20#include <oidset.h> 21#include <tree.h> 22 23static const char *short_commit_name(struct repository *repo, 24 struct commit *commit) 25{ 26 return repo_find_unique_abbrev(repo, &commit->object.oid, 27 DEFAULT_ABBREV); 28} 29 30static struct commit *peel_committish(struct repository *repo, const char *name) 31{ 32 struct object *obj; 33 struct object_id oid; 34 35 if (repo_get_oid(repo, name, &oid)) 36 return NULL; 37 obj = parse_object(repo, &oid); 38 return (struct commit *)repo_peel_to_type(repo, name, 0, obj, 39 OBJ_COMMIT); 40} 41 42static char *get_author(const char *message) 43{ 44 size_t len; 45 const char *a; 46 47 a = find_commit_header(message, "author", &len); 48 if (a) 49 return xmemdupz(a, len); 50 51 return NULL; 52} 53 54static struct commit *create_commit(struct repository *repo, 55 struct tree *tree, 56 struct commit *based_on, 57 struct commit *parent) 58{ 59 struct object_id ret; 60 struct object *obj = NULL; 61 struct commit_list *parents = NULL; 62 char *author; 63 char *sign_commit = NULL; /* FIXME: cli users might want to sign again */ 64 struct commit_extra_header *extra = NULL; 65 struct strbuf msg = STRBUF_INIT; 66 const char *out_enc = get_commit_output_encoding(); 67 const char *message = repo_logmsg_reencode(repo, based_on, 68 NULL, out_enc); 69 const char *orig_message = NULL; 70 const char *exclude_gpgsig[] = { "gpgsig", NULL }; 71 72 commit_list_insert(parent, &parents); 73 extra = read_commit_extra_headers(based_on, exclude_gpgsig); 74 find_commit_subject(message, &orig_message); 75 strbuf_addstr(&msg, orig_message); 76 author = get_author(message); 77 reset_ident_date(); 78 if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, 79 &ret, author, NULL, sign_commit, extra)) { 80 error(_("failed to write commit object")); 81 goto out; 82 } 83 84 obj = parse_object(repo, &ret); 85 86out: 87 repo_unuse_commit_buffer(the_repository, based_on, message); 88 free_commit_extra_headers(extra); 89 free_commit_list(parents); 90 strbuf_release(&msg); 91 free(author); 92 return (struct commit *)obj; 93} 94 95struct ref_info { 96 struct commit *onto; 97 struct strset positive_refs; 98 struct strset negative_refs; 99 int positive_refexprs; 100 int negative_refexprs; 101}; 102 103static void get_ref_information(struct repository *repo, 104 struct rev_cmdline_info *cmd_info, 105 struct ref_info *ref_info) 106{ 107 int i; 108 109 ref_info->onto = NULL; 110 strset_init(&ref_info->positive_refs); 111 strset_init(&ref_info->negative_refs); 112 ref_info->positive_refexprs = 0; 113 ref_info->negative_refexprs = 0; 114 115 /* 116 * When the user specifies e.g. 117 * git replay origin/main..mybranch 118 * git replay ^origin/next mybranch1 mybranch2 119 * we want to be able to determine where to replay the commits. In 120 * these examples, the branches are probably based on an old version 121 * of either origin/main or origin/next, so we want to replay on the 122 * newest version of that branch. In contrast we would want to error 123 * out if they ran 124 * git replay ^origin/master ^origin/next mybranch 125 * git replay mybranch~2..mybranch 126 * the first of those because there's no unique base to choose, and 127 * the second because they'd likely just be replaying commits on top 128 * of the same commit and not making any difference. 129 */ 130 for (i = 0; i < cmd_info->nr; i++) { 131 struct rev_cmdline_entry *e = cmd_info->rev + i; 132 struct object_id oid; 133 const char *refexpr = e->name; 134 char *fullname = NULL; 135 int can_uniquely_dwim = 1; 136 137 if (*refexpr == '^') 138 refexpr++; 139 if (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1) 140 can_uniquely_dwim = 0; 141 142 if (e->flags & BOTTOM) { 143 if (can_uniquely_dwim) 144 strset_add(&ref_info->negative_refs, fullname); 145 if (!ref_info->negative_refexprs) 146 ref_info->onto = lookup_commit_reference_gently(repo, 147 &e->item->oid, 1); 148 ref_info->negative_refexprs++; 149 } else { 150 if (can_uniquely_dwim) 151 strset_add(&ref_info->positive_refs, fullname); 152 ref_info->positive_refexprs++; 153 } 154 155 free(fullname); 156 } 157} 158 159static void determine_replay_mode(struct repository *repo, 160 struct rev_cmdline_info *cmd_info, 161 const char *onto_name, 162 char **advance_name, 163 struct commit **onto, 164 struct strset **update_refs) 165{ 166 struct ref_info rinfo; 167 168 get_ref_information(repo, cmd_info, &rinfo); 169 if (!rinfo.positive_refexprs) 170 die(_("need some commits to replay")); 171 172 die_for_incompatible_opt2(!!onto_name, "--onto", 173 !!*advance_name, "--advance"); 174 if (onto_name) { 175 *onto = peel_committish(repo, onto_name); 176 if (rinfo.positive_refexprs < 177 strset_get_size(&rinfo.positive_refs)) 178 die(_("all positive revisions given must be references")); 179 } else if (*advance_name) { 180 struct object_id oid; 181 char *fullname = NULL; 182 183 *onto = peel_committish(repo, *advance_name); 184 if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name), 185 &oid, &fullname, 0) == 1) { 186 free(*advance_name); 187 *advance_name = fullname; 188 } else { 189 die(_("argument to --advance must be a reference")); 190 } 191 if (rinfo.positive_refexprs > 1) 192 die(_("cannot advance target with multiple sources because ordering would be ill-defined")); 193 } else { 194 int positive_refs_complete = ( 195 rinfo.positive_refexprs == 196 strset_get_size(&rinfo.positive_refs)); 197 int negative_refs_complete = ( 198 rinfo.negative_refexprs == 199 strset_get_size(&rinfo.negative_refs)); 200 /* 201 * We need either positive_refs_complete or 202 * negative_refs_complete, but not both. 203 */ 204 if (rinfo.negative_refexprs > 0 && 205 positive_refs_complete == negative_refs_complete) 206 die(_("cannot implicitly determine whether this is an --advance or --onto operation")); 207 if (negative_refs_complete) { 208 struct hashmap_iter iter; 209 struct strmap_entry *entry; 210 const char *last_key = NULL; 211 212 if (rinfo.negative_refexprs == 0) 213 die(_("all positive revisions given must be references")); 214 else if (rinfo.negative_refexprs > 1) 215 die(_("cannot implicitly determine whether this is an --advance or --onto operation")); 216 else if (rinfo.positive_refexprs > 1) 217 die(_("cannot advance target with multiple source branches because ordering would be ill-defined")); 218 219 /* Only one entry, but we have to loop to get it */ 220 strset_for_each_entry(&rinfo.negative_refs, 221 &iter, entry) { 222 last_key = entry->key; 223 } 224 225 free(*advance_name); 226 *advance_name = xstrdup_or_null(last_key); 227 } else { /* positive_refs_complete */ 228 if (rinfo.negative_refexprs > 1) 229 die(_("cannot implicitly determine correct base for --onto")); 230 if (rinfo.negative_refexprs == 1) 231 *onto = rinfo.onto; 232 } 233 } 234 if (!*advance_name) { 235 *update_refs = xcalloc(1, sizeof(**update_refs)); 236 **update_refs = rinfo.positive_refs; 237 memset(&rinfo.positive_refs, 0, sizeof(**update_refs)); 238 } 239 strset_clear(&rinfo.negative_refs); 240 strset_clear(&rinfo.positive_refs); 241} 242 243static struct commit *mapped_commit(kh_oid_map_t *replayed_commits, 244 struct commit *commit, 245 struct commit *fallback) 246{ 247 khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid); 248 if (pos == kh_end(replayed_commits)) 249 return fallback; 250 return kh_value(replayed_commits, pos); 251} 252 253static struct commit *pick_regular_commit(struct repository *repo, 254 struct commit *pickme, 255 kh_oid_map_t *replayed_commits, 256 struct commit *onto, 257 struct merge_options *merge_opt, 258 struct merge_result *result) 259{ 260 struct commit *base, *replayed_base; 261 struct tree *pickme_tree, *base_tree; 262 263 base = pickme->parents->item; 264 replayed_base = mapped_commit(replayed_commits, base, onto); 265 266 result->tree = repo_get_commit_tree(repo, replayed_base); 267 pickme_tree = repo_get_commit_tree(repo, pickme); 268 base_tree = repo_get_commit_tree(repo, base); 269 270 merge_opt->branch1 = short_commit_name(repo, replayed_base); 271 merge_opt->branch2 = short_commit_name(repo, pickme); 272 merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2); 273 274 merge_incore_nonrecursive(merge_opt, 275 base_tree, 276 result->tree, 277 pickme_tree, 278 result); 279 280 free((char*)merge_opt->ancestor); 281 merge_opt->ancestor = NULL; 282 if (!result->clean) 283 return NULL; 284 return create_commit(repo, result->tree, pickme, replayed_base); 285} 286 287int cmd_replay(int argc, 288 const char **argv, 289 const char *prefix, 290 struct repository *repo) 291{ 292 const char *advance_name_opt = NULL; 293 char *advance_name = NULL; 294 struct commit *onto = NULL; 295 const char *onto_name = NULL; 296 int contained = 0; 297 298 struct rev_info revs; 299 struct commit *last_commit = NULL; 300 struct commit *commit; 301 struct merge_options merge_opt; 302 struct merge_result result; 303 struct strset *update_refs = NULL; 304 kh_oid_map_t *replayed_commits; 305 int ret = 0; 306 307 const char * const replay_usage[] = { 308 N_("(EXPERIMENTAL!) git replay " 309 "([--contained] --onto <newbase> | --advance <branch>) " 310 "<revision-range>..."), 311 NULL 312 }; 313 struct option replay_options[] = { 314 OPT_STRING(0, "advance", &advance_name_opt, 315 N_("branch"), 316 N_("make replay advance given branch")), 317 OPT_STRING(0, "onto", &onto_name, 318 N_("revision"), 319 N_("replay onto given commit")), 320 OPT_BOOL(0, "contained", &contained, 321 N_("advance all branches contained in revision-range")), 322 OPT_END() 323 }; 324 325 argc = parse_options(argc, argv, prefix, replay_options, replay_usage, 326 PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); 327 328 if (!onto_name && !advance_name_opt) { 329 error(_("option --onto or --advance is mandatory")); 330 usage_with_options(replay_usage, replay_options); 331 } 332 333 if (advance_name_opt && contained) 334 die(_("options '%s' and '%s' cannot be used together"), 335 "--advance", "--contained"); 336 advance_name = xstrdup_or_null(advance_name_opt); 337 338 repo_init_revisions(repo, &revs, prefix); 339 340 /* 341 * Set desired values for rev walking options here. If they 342 * are changed by some user specified option in setup_revisions() 343 * below, we will detect that below and then warn. 344 * 345 * TODO: In the future we might want to either die(), or allow 346 * some options changing these values if we think they could 347 * be useful. 348 */ 349 revs.reverse = 1; 350 revs.sort_order = REV_SORT_IN_GRAPH_ORDER; 351 revs.topo_order = 1; 352 revs.simplify_history = 0; 353 354 argc = setup_revisions(argc, argv, &revs, NULL); 355 if (argc > 1) { 356 ret = error(_("unrecognized argument: %s"), argv[1]); 357 goto cleanup; 358 } 359 360 /* 361 * Detect and warn if we override some user specified rev 362 * walking options. 363 */ 364 if (revs.reverse != 1) { 365 warning(_("some rev walking options will be overridden as " 366 "'%s' bit in 'struct rev_info' will be forced"), 367 "reverse"); 368 revs.reverse = 1; 369 } 370 if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) { 371 warning(_("some rev walking options will be overridden as " 372 "'%s' bit in 'struct rev_info' will be forced"), 373 "sort_order"); 374 revs.sort_order = REV_SORT_IN_GRAPH_ORDER; 375 } 376 if (revs.topo_order != 1) { 377 warning(_("some rev walking options will be overridden as " 378 "'%s' bit in 'struct rev_info' will be forced"), 379 "topo_order"); 380 revs.topo_order = 1; 381 } 382 if (revs.simplify_history != 0) { 383 warning(_("some rev walking options will be overridden as " 384 "'%s' bit in 'struct rev_info' will be forced"), 385 "simplify_history"); 386 revs.simplify_history = 0; 387 } 388 389 determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name, 390 &onto, &update_refs); 391 392 if (!onto) /* FIXME: Should handle replaying down to root commit */ 393 die("Replaying down to root commit is not supported yet!"); 394 395 if (prepare_revision_walk(&revs) < 0) { 396 ret = error(_("error preparing revisions")); 397 goto cleanup; 398 } 399 400 init_basic_merge_options(&merge_opt, repo); 401 memset(&result, 0, sizeof(result)); 402 merge_opt.show_rename_progress = 0; 403 last_commit = onto; 404 replayed_commits = kh_init_oid_map(); 405 while ((commit = get_revision(&revs))) { 406 const struct name_decoration *decoration; 407 khint_t pos; 408 int hr; 409 410 if (!commit->parents) 411 die(_("replaying down to root commit is not supported yet!")); 412 if (commit->parents->next) 413 die(_("replaying merge commits is not supported yet!")); 414 415 last_commit = pick_regular_commit(repo, commit, replayed_commits, 416 onto, &merge_opt, &result); 417 if (!last_commit) 418 break; 419 420 /* Record commit -> last_commit mapping */ 421 pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr); 422 if (hr == 0) 423 BUG("Duplicate rewritten commit: %s\n", 424 oid_to_hex(&commit->object.oid)); 425 kh_value(replayed_commits, pos) = last_commit; 426 427 /* Update any necessary branches */ 428 if (advance_name) 429 continue; 430 decoration = get_name_decoration(&commit->object); 431 if (!decoration) 432 continue; 433 while (decoration) { 434 if (decoration->type == DECORATION_REF_LOCAL && 435 (contained || strset_contains(update_refs, 436 decoration->name))) { 437 printf("update %s %s %s\n", 438 decoration->name, 439 oid_to_hex(&last_commit->object.oid), 440 oid_to_hex(&commit->object.oid)); 441 } 442 decoration = decoration->next; 443 } 444 } 445 446 /* In --advance mode, advance the target ref */ 447 if (result.clean == 1 && advance_name) { 448 printf("update %s %s %s\n", 449 advance_name, 450 oid_to_hex(&last_commit->object.oid), 451 oid_to_hex(&onto->object.oid)); 452 } 453 454 merge_finalize(&merge_opt, &result); 455 kh_destroy_oid_map(replayed_commits); 456 if (update_refs) { 457 strset_clear(update_refs); 458 free(update_refs); 459 } 460 ret = result.clean; 461 462cleanup: 463 release_revisions(&revs); 464 free(advance_name); 465 466 /* Return */ 467 if (ret < 0) 468 exit(128); 469 return ret ? 0 : 1; 470}