|
1 | 1 | #define USE_THE_REPOSITORY_VARIABLE |
2 | 2 |
|
3 | 3 | #include "builtin.h" |
| 4 | +#include "cache-tree.h" |
4 | 5 | #include "commit.h" |
5 | 6 | #include "commit-reach.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 "lockfile.h" |
| 13 | +#include "oidmap.h" |
11 | 14 | #include "parse-options.h" |
| 15 | +#include "path.h" |
| 16 | +#include "read-cache.h" |
12 | 17 | #include "refs.h" |
13 | 18 | #include "replay.h" |
14 | 19 | #include "revision.h" |
15 | 20 | #include "sequencer.h" |
16 | 21 | #include "strvec.h" |
17 | 22 | #include "tree.h" |
| 23 | +#include "unpack-trees.h" |
18 | 24 | #include "wt-status.h" |
19 | 25 |
|
20 | 26 | #define GIT_HISTORY_REWORD_USAGE \ |
21 | 27 | N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]") |
| 28 | +#define GIT_HISTORY_SPLIT_USAGE \ |
| 29 | + N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]") |
22 | 30 |
|
23 | 31 | static void change_data_free(void *util, const char *str UNUSED) |
24 | 32 | { |
@@ -484,18 +492,260 @@ static int cmd_history_reword(int argc, |
484 | 492 | return ret; |
485 | 493 | } |
486 | 494 |
|
| 495 | +static int write_ondisk_index(struct repository *repo, |
| 496 | + struct object_id *oid, |
| 497 | + const char *path) |
| 498 | +{ |
| 499 | + struct unpack_trees_options opts = { 0 }; |
| 500 | + struct lock_file lock = LOCK_INIT; |
| 501 | + struct tree_desc tree_desc; |
| 502 | + struct index_state index; |
| 503 | + struct tree *tree; |
| 504 | + int ret; |
| 505 | + |
| 506 | + index_state_init(&index, repo); |
| 507 | + |
| 508 | + opts.head_idx = -1; |
| 509 | + opts.src_index = &index; |
| 510 | + opts.dst_index = &index; |
| 511 | + |
| 512 | + tree = repo_parse_tree_indirect(repo, oid); |
| 513 | + init_tree_desc(&tree_desc, &tree->object.oid, tree->buffer, tree->size); |
| 514 | + |
| 515 | + if (unpack_trees(1, &tree_desc, &opts)) { |
| 516 | + ret = error(_("unable to populate index with tree")); |
| 517 | + goto out; |
| 518 | + } |
| 519 | + |
| 520 | + prime_cache_tree(repo, &index, tree); |
| 521 | + |
| 522 | + if (hold_lock_file_for_update(&lock, path, 0) < 0) { |
| 523 | + ret = error_errno(_("unable to acquire index lock")); |
| 524 | + goto out; |
| 525 | + } |
| 526 | + |
| 527 | + if (write_locked_index(&index, &lock, COMMIT_LOCK)) { |
| 528 | + ret = error(_("unable to write new index file")); |
| 529 | + goto out; |
| 530 | + } |
| 531 | + |
| 532 | + ret = 0; |
| 533 | + |
| 534 | +out: |
| 535 | + rollback_lock_file(&lock); |
| 536 | + release_index(&index); |
| 537 | + return ret; |
| 538 | +} |
| 539 | + |
| 540 | +static int split_commit(struct repository *repo, |
| 541 | + struct commit *original, |
| 542 | + struct pathspec *pathspec, |
| 543 | + struct commit **out) |
| 544 | +{ |
| 545 | + struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT; |
| 546 | + struct strbuf index_file = STRBUF_INIT; |
| 547 | + struct index_state index = INDEX_STATE_INIT(repo); |
| 548 | + const struct object_id *original_commit_tree_oid; |
| 549 | + const struct object_id *old_tree_oid, *new_tree_oid; |
| 550 | + struct object_id parent_tree_oid; |
| 551 | + char original_commit_oid[GIT_MAX_HEXSZ + 1]; |
| 552 | + struct commit *first_commit, *second_commit; |
| 553 | + struct commit_list *parents = NULL; |
| 554 | + struct tree *split_tree; |
| 555 | + int ret; |
| 556 | + |
| 557 | + if (original->parents) { |
| 558 | + if (repo_parse_commit(repo, original->parents->item)) { |
| 559 | + ret = error(_("unable to parse parent commit %s"), |
| 560 | + oid_to_hex(&original->parents->item->object.oid)); |
| 561 | + goto out; |
| 562 | + } |
| 563 | + |
| 564 | + parent_tree_oid = *get_commit_tree_oid(original->parents->item); |
| 565 | + } else { |
| 566 | + oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree); |
| 567 | + } |
| 568 | + original_commit_tree_oid = get_commit_tree_oid(original); |
| 569 | + |
| 570 | + /* |
| 571 | + * Construct the first commit. This is done by taking the original |
| 572 | + * commit parent's tree and selectively patching changes from the diff |
| 573 | + * between that parent and its child. |
| 574 | + */ |
| 575 | + repo_git_path_replace(repo, &index_file, "%s", "history-split.index"); |
| 576 | + |
| 577 | + ret = write_ondisk_index(repo, &parent_tree_oid, index_file.buf); |
| 578 | + if (ret < 0) |
| 579 | + goto out; |
| 580 | + |
| 581 | + ret = read_index_from(&index, index_file.buf, repo->gitdir); |
| 582 | + if (ret < 0) { |
| 583 | + ret = error(_("failed reading temporary index")); |
| 584 | + goto out; |
| 585 | + } |
| 586 | + |
| 587 | + oid_to_hex_r(original_commit_oid, &original->object.oid); |
| 588 | + ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts, |
| 589 | + original_commit_oid, pathspec, ADD_P_DISALLOW_EDIT); |
| 590 | + if (ret < 0) |
| 591 | + goto out; |
| 592 | + |
| 593 | + split_tree = write_in_core_index_as_tree(repo, &index); |
| 594 | + if (!split_tree) { |
| 595 | + ret = error(_("failed split tree")); |
| 596 | + goto out; |
| 597 | + } |
| 598 | + |
| 599 | + unlink(index_file.buf); |
| 600 | + strbuf_release(&index_file); |
| 601 | + |
| 602 | + /* |
| 603 | + * We disallow the cases where either the split-out commit or the |
| 604 | + * original commit would become empty. Consequently, if we see that the |
| 605 | + * new tree ID matches either of those trees we abort. |
| 606 | + */ |
| 607 | + if (oideq(&split_tree->object.oid, &parent_tree_oid)) { |
| 608 | + ret = error(_("split commit is empty")); |
| 609 | + goto out; |
| 610 | + } else if (oideq(&split_tree->object.oid, original_commit_tree_oid)) { |
| 611 | + ret = error(_("split commit tree matches original commit")); |
| 612 | + goto out; |
| 613 | + } |
| 614 | + |
| 615 | + /* |
| 616 | + * The first commit is constructed from the split-out tree. The base |
| 617 | + * that shall be diffed against is the parent of the original commit. |
| 618 | + */ |
| 619 | + ret = commit_tree_with_edited_message_ext(repo, "split-out", original, |
| 620 | + original->parents, &parent_tree_oid, |
| 621 | + &split_tree->object.oid, &first_commit); |
| 622 | + if (ret < 0) { |
| 623 | + ret = error(_("failed writing first commit")); |
| 624 | + goto out; |
| 625 | + } |
| 626 | + |
| 627 | + /* |
| 628 | + * The second commit is constructed from the original tree. The base to |
| 629 | + * diff against and the parent in this case is the first split-out |
| 630 | + * commit. |
| 631 | + */ |
| 632 | + commit_list_append(first_commit, &parents); |
| 633 | + |
| 634 | + old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid; |
| 635 | + new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid; |
| 636 | + |
| 637 | + ret = commit_tree_with_edited_message_ext(repo, "split-out", original, |
| 638 | + parents, old_tree_oid, |
| 639 | + new_tree_oid, &second_commit); |
| 640 | + if (ret < 0) { |
| 641 | + ret = error(_("failed writing second commit")); |
| 642 | + goto out; |
| 643 | + } |
| 644 | + |
| 645 | + *out = second_commit; |
| 646 | + ret = 0; |
| 647 | + |
| 648 | +out: |
| 649 | + if (index_file.len) |
| 650 | + unlink(index_file.buf); |
| 651 | + strbuf_release(&index_file); |
| 652 | + free_commit_list(parents); |
| 653 | + release_index(&index); |
| 654 | + return ret; |
| 655 | +} |
| 656 | + |
| 657 | +static int cmd_history_split(int argc, |
| 658 | + const char **argv, |
| 659 | + const char *prefix, |
| 660 | + struct repository *repo) |
| 661 | +{ |
| 662 | + const char * const usage[] = { |
| 663 | + GIT_HISTORY_SPLIT_USAGE, |
| 664 | + NULL, |
| 665 | + }; |
| 666 | + enum ref_action action = REF_ACTION_DEFAULT; |
| 667 | + int dry_run = 0; |
| 668 | + struct option options[] = { |
| 669 | + OPT_CALLBACK_F(0, "update-refs", &action, N_("<refs>"), |
| 670 | + N_("control ref update behavior (branches|head|print)"), |
| 671 | + PARSE_OPT_NONEG, parse_ref_action), |
| 672 | + OPT_BOOL('n', "dry-run", &dry_run, |
| 673 | + N_("perform a dry-run without updating any refs")), |
| 674 | + OPT_END(), |
| 675 | + }; |
| 676 | + struct commit *original, *rewritten = NULL; |
| 677 | + struct strbuf reflog_msg = STRBUF_INIT; |
| 678 | + struct pathspec pathspec = { 0 }; |
| 679 | + struct rev_info revs = { 0 }; |
| 680 | + int ret; |
| 681 | + |
| 682 | + argc = parse_options(argc, argv, prefix, options, usage, 0); |
| 683 | + if (argc < 1) { |
| 684 | + ret = error(_("command expects a committish")); |
| 685 | + goto out; |
| 686 | + } |
| 687 | + repo_config(repo, git_default_config, NULL); |
| 688 | + |
| 689 | + if (action == REF_ACTION_DEFAULT) |
| 690 | + action = REF_ACTION_BRANCHES; |
| 691 | + |
| 692 | + parse_pathspec(&pathspec, 0, |
| 693 | + PATHSPEC_PREFER_FULL | |
| 694 | + PATHSPEC_SYMLINK_LEADING_PATH | |
| 695 | + PATHSPEC_PREFIX_ORIGIN, |
| 696 | + prefix, argv + 1); |
| 697 | + |
| 698 | + original = lookup_commit_reference_by_name(argv[0]); |
| 699 | + if (!original) { |
| 700 | + ret = error(_("commit cannot be found: %s"), argv[0]); |
| 701 | + goto out; |
| 702 | + } |
| 703 | + |
| 704 | + ret = setup_revwalk(repo, action, original, &revs); |
| 705 | + if (ret < 0) |
| 706 | + goto out; |
| 707 | + |
| 708 | + if (original->parents && original->parents->next) { |
| 709 | + ret = error(_("cannot split up merge commit")); |
| 710 | + goto out; |
| 711 | + } |
| 712 | + |
| 713 | + ret = split_commit(repo, original, &pathspec, &rewritten); |
| 714 | + if (ret < 0) |
| 715 | + goto out; |
| 716 | + |
| 717 | + strbuf_addf(&reflog_msg, "split: updating %s", argv[0]); |
| 718 | + |
| 719 | + ret = handle_reference_updates(&revs, action, original, rewritten, |
| 720 | + reflog_msg.buf, dry_run); |
| 721 | + if (ret < 0) { |
| 722 | + ret = error(_("failed replaying descendants")); |
| 723 | + goto out; |
| 724 | + } |
| 725 | + |
| 726 | + ret = 0; |
| 727 | + |
| 728 | +out: |
| 729 | + strbuf_release(&reflog_msg); |
| 730 | + clear_pathspec(&pathspec); |
| 731 | + release_revisions(&revs); |
| 732 | + return ret; |
| 733 | +} |
| 734 | + |
487 | 735 | int cmd_history(int argc, |
488 | 736 | const char **argv, |
489 | 737 | const char *prefix, |
490 | 738 | struct repository *repo) |
491 | 739 | { |
492 | 740 | const char * const usage[] = { |
493 | 741 | GIT_HISTORY_REWORD_USAGE, |
| 742 | + GIT_HISTORY_SPLIT_USAGE, |
494 | 743 | NULL, |
495 | 744 | }; |
496 | 745 | parse_opt_subcommand_fn *fn = NULL; |
497 | 746 | struct option options[] = { |
498 | 747 | OPT_SUBCOMMAND("reword", &fn, cmd_history_reword), |
| 748 | + OPT_SUBCOMMAND("split", &fn, cmd_history_split), |
499 | 749 | OPT_END(), |
500 | 750 | }; |
501 | 751 |
|
|
0 commit comments