|
1 | 1 | #define USE_THE_REPOSITORY_VARIABLE |
2 | 2 |
|
3 | 3 | #include "builtin.h" |
| 4 | +#include "cache-tree.h" |
4 | 5 | #include "commit-reach.h" |
5 | 6 | #include "commit.h" |
6 | 7 | #include "config.h" |
7 | 8 | #include "editor.h" |
8 | 9 | #include "environment.h" |
9 | 10 | #include "gettext.h" |
10 | 11 | #include "hex.h" |
| 12 | +#include "oidmap.h" |
11 | 13 | #include "parse-options.h" |
| 14 | +#include "path.h" |
| 15 | +#include "read-cache.h" |
12 | 16 | #include "refs.h" |
13 | 17 | #include "replay.h" |
14 | 18 | #include "reset.h" |
15 | 19 | #include "revision.h" |
| 20 | +#include "run-command.h" |
16 | 21 | #include "sequencer.h" |
17 | 22 | #include "strvec.h" |
18 | 23 | #include "tree.h" |
19 | 24 | #include "wt-status.h" |
20 | 25 |
|
21 | 26 | #define GIT_HISTORY_REWORD_USAGE N_("git history reword <commit>") |
| 27 | +#define GIT_HISTORY_SPLIT_USAGE N_("git history split <commit> [--] [<pathspec>...]") |
22 | 28 |
|
23 | 29 | static int collect_commits(struct repository *repo, |
24 | 30 | struct commit *old_commit, |
@@ -323,18 +329,230 @@ static int cmd_history_reword(int argc, |
323 | 329 | return ret; |
324 | 330 | } |
325 | 331 |
|
| 332 | +static int split_commit(struct repository *repo, |
| 333 | + struct commit *original_commit, |
| 334 | + struct pathspec *pathspec, |
| 335 | + struct object_id *out) |
| 336 | +{ |
| 337 | + struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT; |
| 338 | + struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT; |
| 339 | + struct child_process read_tree_cmd = CHILD_PROCESS_INIT; |
| 340 | + struct index_state index = INDEX_STATE_INIT(repo); |
| 341 | + struct object_id original_commit_tree_oid, parent_tree_oid; |
| 342 | + const char *original_message, *original_body, *ptr; |
| 343 | + char original_commit_oid[GIT_MAX_HEXSZ + 1]; |
| 344 | + char *original_author = NULL; |
| 345 | + struct commit_list *parents = NULL; |
| 346 | + struct commit *first_commit; |
| 347 | + struct tree *split_tree; |
| 348 | + size_t len; |
| 349 | + int ret; |
| 350 | + |
| 351 | + if (original_commit->parents) |
| 352 | + parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item); |
| 353 | + else |
| 354 | + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree); |
| 355 | + original_commit_tree_oid = *get_commit_tree_oid(original_commit); |
| 356 | + |
| 357 | + /* |
| 358 | + * Construct the first commit. This is done by taking the original |
| 359 | + * commit parent's tree and selectively patching changes from the diff |
| 360 | + * between that parent and its child. |
| 361 | + */ |
| 362 | + repo_git_path_replace(repo, &index_file, "%s", "history-split.index"); |
| 363 | + |
| 364 | + read_tree_cmd.git_cmd = 1; |
| 365 | + strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf); |
| 366 | + strvec_push(&read_tree_cmd.args, "read-tree"); |
| 367 | + strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid)); |
| 368 | + ret = run_command(&read_tree_cmd); |
| 369 | + if (ret < 0) |
| 370 | + goto out; |
| 371 | + |
| 372 | + ret = read_index_from(&index, index_file.buf, repo->gitdir); |
| 373 | + if (ret < 0) { |
| 374 | + ret = error(_("failed reading temporary index")); |
| 375 | + goto out; |
| 376 | + } |
| 377 | + |
| 378 | + oid_to_hex_r(original_commit_oid, &original_commit->object.oid); |
| 379 | + ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts, |
| 380 | + original_commit_oid, pathspec); |
| 381 | + if (ret < 0) |
| 382 | + goto out; |
| 383 | + |
| 384 | + split_tree = write_in_core_index_as_tree(repo, &index); |
| 385 | + if (!split_tree) { |
| 386 | + ret = error(_("failed split tree")); |
| 387 | + goto out; |
| 388 | + } |
| 389 | + |
| 390 | + unlink(index_file.buf); |
| 391 | + |
| 392 | + /* |
| 393 | + * We disallow the cases where either the split-out commit or the |
| 394 | + * original commit would become empty. Consequently, if we see that the |
| 395 | + * new tree ID matches either of those trees we abort. |
| 396 | + */ |
| 397 | + if (oideq(&split_tree->object.oid, &parent_tree_oid)) { |
| 398 | + ret = error(_("split commit is empty")); |
| 399 | + goto out; |
| 400 | + } else if (oideq(&split_tree->object.oid, &original_commit_tree_oid)) { |
| 401 | + ret = error(_("split commit tree matches original commit")); |
| 402 | + goto out; |
| 403 | + } |
| 404 | + |
| 405 | + /* We retain authorship of the original commit. */ |
| 406 | + original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL); |
| 407 | + ptr = find_commit_header(original_message, "author", &len); |
| 408 | + if (ptr) |
| 409 | + original_author = xmemdupz(ptr, len); |
| 410 | + |
| 411 | + ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid, |
| 412 | + "", "split-out", &split_message); |
| 413 | + if (ret < 0) |
| 414 | + goto out; |
| 415 | + |
| 416 | + ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid, |
| 417 | + original_commit->parents, &out[0], original_author, NULL); |
| 418 | + if (ret < 0) { |
| 419 | + ret = error(_("failed writing split-out commit")); |
| 420 | + goto out; |
| 421 | + } |
| 422 | + |
| 423 | + /* |
| 424 | + * The second commit is much simpler to construct, as we can simply use |
| 425 | + * the original commit details, except that we adjust its parent to be |
| 426 | + * the newly split-out commit. |
| 427 | + */ |
| 428 | + find_commit_subject(original_message, &original_body); |
| 429 | + first_commit = lookup_commit_reference(repo, &out[0]); |
| 430 | + commit_list_append(first_commit, &parents); |
| 431 | + |
| 432 | + ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid, |
| 433 | + parents, &out[1], original_author, NULL); |
| 434 | + if (ret < 0) { |
| 435 | + ret = error(_("failed writing second commit")); |
| 436 | + goto out; |
| 437 | + } |
| 438 | + |
| 439 | + ret = 0; |
| 440 | + |
| 441 | +out: |
| 442 | + if (index_file.len) |
| 443 | + unlink(index_file.buf); |
| 444 | + strbuf_release(&split_message); |
| 445 | + strbuf_release(&index_file); |
| 446 | + free_commit_list(parents); |
| 447 | + free(original_author); |
| 448 | + release_index(&index); |
| 449 | + return ret; |
| 450 | +} |
| 451 | + |
| 452 | +static int cmd_history_split(int argc, |
| 453 | + const char **argv, |
| 454 | + const char *prefix, |
| 455 | + struct repository *repo) |
| 456 | +{ |
| 457 | + const char * const usage[] = { |
| 458 | + GIT_HISTORY_SPLIT_USAGE, |
| 459 | + NULL, |
| 460 | + }; |
| 461 | + struct option options[] = { |
| 462 | + OPT_END(), |
| 463 | + }; |
| 464 | + struct oidmap rewritten_commits = OIDMAP_INIT; |
| 465 | + struct commit *original_commit, *parent, *head; |
| 466 | + struct strvec commits = STRVEC_INIT; |
| 467 | + struct commit_list *from_list = NULL; |
| 468 | + struct object_id split_commits[2]; |
| 469 | + struct pathspec pathspec = { 0 }; |
| 470 | + int ret; |
| 471 | + |
| 472 | + argc = parse_options(argc, argv, prefix, options, usage, 0); |
| 473 | + if (argc < 1) { |
| 474 | + ret = error(_("command expects a revision")); |
| 475 | + goto out; |
| 476 | + } |
| 477 | + repo_config(repo, git_default_config, NULL); |
| 478 | + |
| 479 | + original_commit = lookup_commit_reference_by_name(argv[0]); |
| 480 | + if (!original_commit) { |
| 481 | + ret = error(_("commit to be split cannot be found: %s"), argv[0]); |
| 482 | + goto out; |
| 483 | + } |
| 484 | + |
| 485 | + parent = original_commit->parents ? original_commit->parents->item : NULL; |
| 486 | + if (parent && repo_parse_commit(repo, parent)) { |
| 487 | + ret = error(_("unable to parse commit %s"), |
| 488 | + oid_to_hex(&parent->object.oid)); |
| 489 | + goto out; |
| 490 | + } |
| 491 | + |
| 492 | + head = lookup_commit_reference_by_name("HEAD"); |
| 493 | + if (!head) { |
| 494 | + ret = error(_("could not resolve HEAD to a commit")); |
| 495 | + goto out; |
| 496 | + } |
| 497 | + |
| 498 | + commit_list_append(original_commit, &from_list); |
| 499 | + if (!repo_is_descendant_of(repo, head, from_list)) { |
| 500 | + ret = error(_("split commit must be reachable from current HEAD commit")); |
| 501 | + goto out; |
| 502 | + } |
| 503 | + |
| 504 | + parse_pathspec(&pathspec, 0, |
| 505 | + PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN, |
| 506 | + prefix, argv + 1); |
| 507 | + |
| 508 | + /* |
| 509 | + * Collect the list of commits that we'll have to reapply now already. |
| 510 | + * This ensures that we'll abort early on in case the range of commits |
| 511 | + * contains merges, which we do not yet handle. |
| 512 | + */ |
| 513 | + ret = collect_commits(repo, parent, head, &commits); |
| 514 | + if (ret < 0) |
| 515 | + goto out; |
| 516 | + |
| 517 | + /* |
| 518 | + * Then we split up the commit and replace the original commit with the |
| 519 | + * new ones. |
| 520 | + */ |
| 521 | + ret = split_commit(repo, original_commit, &pathspec, split_commits); |
| 522 | + if (ret < 0) |
| 523 | + goto out; |
| 524 | + |
| 525 | + replace_commits(&commits, &original_commit->object.oid, |
| 526 | + split_commits, ARRAY_SIZE(split_commits)); |
| 527 | + |
| 528 | + ret = apply_commits(repo, &commits, parent, head, "split"); |
| 529 | + if (ret < 0) |
| 530 | + goto out; |
| 531 | + |
| 532 | + ret = 0; |
| 533 | + |
| 534 | +out: |
| 535 | + oidmap_clear(&rewritten_commits, 0); |
| 536 | + free_commit_list(from_list); |
| 537 | + clear_pathspec(&pathspec); |
| 538 | + strvec_clear(&commits); |
| 539 | + return ret; |
| 540 | +} |
| 541 | + |
326 | 542 | int cmd_history(int argc, |
327 | 543 | const char **argv, |
328 | 544 | const char *prefix, |
329 | 545 | struct repository *repo) |
330 | 546 | { |
331 | 547 | const char * const usage[] = { |
332 | 548 | GIT_HISTORY_REWORD_USAGE, |
| 549 | + GIT_HISTORY_SPLIT_USAGE, |
333 | 550 | NULL, |
334 | 551 | }; |
335 | 552 | parse_opt_subcommand_fn *fn = NULL; |
336 | 553 | struct option options[] = { |
337 | 554 | OPT_SUBCOMMAND("reword", &fn, cmd_history_reword), |
| 555 | + OPT_SUBCOMMAND("split", &fn, cmd_history_split), |
338 | 556 | OPT_END(), |
339 | 557 | }; |
340 | 558 |
|
|
0 commit comments