|
16 | 16 | #include "git2/checkout.h" |
17 | 17 | #include "git2/index.h" |
18 | 18 | #include "git2/transaction.h" |
| 19 | +#include "git2/merge.h" |
19 | 20 | #include "signature.h" |
20 | 21 |
|
21 | 22 | static int create_error(int error, const char *msg) |
@@ -555,6 +556,308 @@ int git_stash_save( |
555 | 556 | return error; |
556 | 557 | } |
557 | 558 |
|
| 559 | +static int retrieve_stash_commit( |
| 560 | + git_commit **commit, |
| 561 | + git_repository *repo, |
| 562 | + size_t index) |
| 563 | +{ |
| 564 | + git_reference *stash = NULL; |
| 565 | + git_reflog *reflog = NULL; |
| 566 | + int error; |
| 567 | + size_t max; |
| 568 | + const git_reflog_entry *entry; |
| 569 | + |
| 570 | + if ((error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE)) < 0) |
| 571 | + goto cleanup; |
| 572 | + |
| 573 | + if ((error = git_reflog_read(&reflog, repo, GIT_REFS_STASH_FILE)) < 0) |
| 574 | + goto cleanup; |
| 575 | + |
| 576 | + max = git_reflog_entrycount(reflog); |
| 577 | + if (index > max - 1) { |
| 578 | + error = GIT_ENOTFOUND; |
| 579 | + giterr_set(GITERR_STASH, "No stashed state at position %" PRIuZ, index); |
| 580 | + goto cleanup; |
| 581 | + } |
| 582 | + |
| 583 | + entry = git_reflog_entry_byindex(reflog, index); |
| 584 | + if ((error = git_commit_lookup(commit, repo, git_reflog_entry_id_new(entry))) < 0) |
| 585 | + goto cleanup; |
| 586 | + |
| 587 | +cleanup: |
| 588 | + git_reference_free(stash); |
| 589 | + git_reflog_free(reflog); |
| 590 | + return error; |
| 591 | +} |
| 592 | + |
| 593 | +static int retrieve_stash_trees( |
| 594 | + git_tree **stash_tree, |
| 595 | + git_tree **base_tree, |
| 596 | + git_tree **index_tree, |
| 597 | + git_tree **index_parent_tree, |
| 598 | + git_tree **untracked_tree, |
| 599 | + git_commit *stash_commit) |
| 600 | +{ |
| 601 | + git_commit *base_commit = NULL; |
| 602 | + git_commit *index_commit = NULL; |
| 603 | + git_commit *index_parent_commit = NULL; |
| 604 | + git_commit *untracked_commit = NULL; |
| 605 | + int error; |
| 606 | + |
| 607 | + if ((error = git_commit_tree(stash_tree, stash_commit)) < 0) |
| 608 | + goto cleanup; |
| 609 | + |
| 610 | + if ((error = git_commit_parent(&base_commit, stash_commit, 0)) < 0) |
| 611 | + goto cleanup; |
| 612 | + if ((error = git_commit_tree(base_tree, base_commit)) < 0) |
| 613 | + goto cleanup; |
| 614 | + |
| 615 | + if ((error = git_commit_parent(&index_commit, stash_commit, 1)) < 0) |
| 616 | + goto cleanup; |
| 617 | + if ((error = git_commit_tree(index_tree, index_commit)) < 0) |
| 618 | + goto cleanup; |
| 619 | + |
| 620 | + if ((error = git_commit_parent(&index_parent_commit, index_commit, 0)) < 0) |
| 621 | + goto cleanup; |
| 622 | + if ((error = git_commit_tree(index_parent_tree, index_parent_commit)) < 0) |
| 623 | + goto cleanup; |
| 624 | + |
| 625 | + if (git_commit_parentcount(stash_commit) == 3) { |
| 626 | + if ((error = git_commit_parent(&untracked_commit, stash_commit, 2)) < 0) |
| 627 | + goto cleanup; |
| 628 | + if ((error = git_commit_tree(untracked_tree, untracked_commit)) < 0) |
| 629 | + goto cleanup; |
| 630 | + } else { |
| 631 | + *untracked_tree = NULL; |
| 632 | + } |
| 633 | + |
| 634 | +cleanup: |
| 635 | + git_commit_free(untracked_commit); |
| 636 | + git_commit_free(index_parent_commit); |
| 637 | + git_commit_free(index_commit); |
| 638 | + git_commit_free(base_commit); |
| 639 | + return error; |
| 640 | +} |
| 641 | + |
| 642 | +static int apply_index( |
| 643 | + git_tree **unstashed_tree, |
| 644 | + git_repository *repo, |
| 645 | + git_tree *start_index_tree, |
| 646 | + git_tree *index_parent_tree, |
| 647 | + git_tree *index_tree) |
| 648 | +{ |
| 649 | + git_index* unstashed_index = NULL; |
| 650 | + git_merge_options options = GIT_MERGE_OPTIONS_INIT; |
| 651 | + int error; |
| 652 | + git_oid oid; |
| 653 | + |
| 654 | + if ((error = git_merge_trees( |
| 655 | + &unstashed_index, repo, index_parent_tree, |
| 656 | + start_index_tree, index_tree, &options)) < 0) |
| 657 | + goto cleanup; |
| 658 | + |
| 659 | + if ((error = git_index_write_tree_to(&oid, unstashed_index, repo)) < 0) |
| 660 | + goto cleanup; |
| 661 | + |
| 662 | + if ((error = git_tree_lookup(unstashed_tree, repo, &oid)) < 0) |
| 663 | + goto cleanup; |
| 664 | + |
| 665 | +cleanup: |
| 666 | + git_index_free(unstashed_index); |
| 667 | + return error; |
| 668 | +} |
| 669 | + |
| 670 | +static int apply_untracked( |
| 671 | + git_repository *repo, |
| 672 | + git_tree *untracked_tree) |
| 673 | +{ |
| 674 | + git_checkout_options options = GIT_CHECKOUT_OPTIONS_INIT; |
| 675 | + int run; |
| 676 | + size_t i, count; |
| 677 | + int error; |
| 678 | + |
| 679 | + /* |
| 680 | + The untracked tree only contains the untracked / ignores files so checking |
| 681 | + it out would remove all other files in the workdir. Since git_checkout_tree() |
| 682 | + does not have a mode to leave removed files alone, we emulate it by checking |
| 683 | + out files from the untracked tree 1 by 1. |
| 684 | +
|
| 685 | + A dry run is first needed to ensure we don't end up with only the first |
| 686 | + files checked out due to a conflict later on. |
| 687 | + */ |
| 688 | + |
| 689 | + options.paths.count = 1; |
| 690 | + for (run = 0; run <= 1; ++run) { |
| 691 | + if (run) { |
| 692 | + options.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE | GIT_CHECKOUT_DONT_UPDATE_INDEX; |
| 693 | + } else { |
| 694 | + options.checkout_strategy = GIT_CHECKOUT_NONE | GIT_CHECKOUT_DONT_UPDATE_INDEX; |
| 695 | + } |
| 696 | + for (i = 0, count = git_tree_entrycount(untracked_tree); i < count; ++i) { |
| 697 | + const git_tree_entry *entry = git_tree_entry_byindex(untracked_tree, i); |
| 698 | + |
| 699 | + const char* name = git_tree_entry_name(entry); |
| 700 | + options.paths.strings = (char**)&name; |
| 701 | + if ((error = git_checkout_tree( |
| 702 | + repo, (git_object*)untracked_tree, &options)) < 0) |
| 703 | + return error; |
| 704 | + } |
| 705 | + } |
| 706 | + |
| 707 | + return GIT_OK; |
| 708 | +} |
| 709 | + |
| 710 | +static int apply_modified( |
| 711 | + git_repository *repo, |
| 712 | + git_tree *base_tree, |
| 713 | + git_tree *start_index_tree, |
| 714 | + git_tree *stash_tree, |
| 715 | + int update_index) |
| 716 | +{ |
| 717 | + git_index *index = NULL; |
| 718 | + git_merge_options merge_options = GIT_MERGE_OPTIONS_INIT; |
| 719 | + git_checkout_options checkout_options = GIT_CHECKOUT_OPTIONS_INIT; |
| 720 | + int error; |
| 721 | + |
| 722 | + if ((error = git_merge_trees( |
| 723 | + &index, repo, base_tree, |
| 724 | + start_index_tree, stash_tree, &merge_options)) < 0) |
| 725 | + goto cleanup; |
| 726 | + |
| 727 | + checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE; |
| 728 | + if (!update_index) { |
| 729 | + checkout_options.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; |
| 730 | + } |
| 731 | + if ((error = git_checkout_index(repo, index, &checkout_options)) < 0) |
| 732 | + goto cleanup; |
| 733 | + |
| 734 | +cleanup: |
| 735 | + git_index_free(index); |
| 736 | + return error; |
| 737 | +} |
| 738 | + |
| 739 | +static int unstage_modified_files( |
| 740 | + git_repository *repo, |
| 741 | + git_index *repo_index, |
| 742 | + git_tree *unstashed_tree, |
| 743 | + git_tree *start_index_tree) |
| 744 | +{ |
| 745 | + git_diff *diff = NULL; |
| 746 | + git_diff_options options = GIT_DIFF_OPTIONS_INIT; |
| 747 | + size_t i, count; |
| 748 | + int error; |
| 749 | + |
| 750 | + if (unstashed_tree) { |
| 751 | + if ((error = git_index_read_tree(repo_index, unstashed_tree)) < 0) |
| 752 | + goto cleanup; |
| 753 | + } else { |
| 754 | + options.flags = GIT_DIFF_FORCE_BINARY; |
| 755 | + if ((error = git_diff_tree_to_index(&diff, repo, start_index_tree, |
| 756 | + repo_index, &options)) < 0) |
| 757 | + goto cleanup; |
| 758 | + |
| 759 | + /* |
| 760 | + This behavior is not 100% similar to "git stash apply" as the latter uses |
| 761 | + "git-read-tree --reset {treeish}" which preserves the stat()s from the |
| 762 | + index instead of replacing them with the tree ones for identical files. |
| 763 | + */ |
| 764 | + |
| 765 | + if ((error = git_index_read_tree(repo_index, start_index_tree)) < 0) |
| 766 | + goto cleanup; |
| 767 | + |
| 768 | + for (i = 0, count = git_diff_num_deltas(diff); i < count; ++i) { |
| 769 | + const git_diff_delta* delta = git_diff_get_delta(diff, i); |
| 770 | + if (delta->status == GIT_DELTA_ADDED) { |
| 771 | + if ((error = git_index_add_bypath( |
| 772 | + repo_index, delta->new_file.path)) < 0) |
| 773 | + goto cleanup; |
| 774 | + } |
| 775 | + } |
| 776 | + } |
| 777 | + |
| 778 | +cleanup: |
| 779 | + git_diff_free(diff); |
| 780 | + return error; |
| 781 | +} |
| 782 | + |
| 783 | +int git_stash_apply( |
| 784 | + git_repository *repo, |
| 785 | + size_t index, |
| 786 | + int reinstate_index) |
| 787 | +{ |
| 788 | + git_commit *stash_commit = NULL; |
| 789 | + git_tree *stash_tree = NULL; |
| 790 | + git_tree *base_tree = NULL; |
| 791 | + git_tree *index_tree = NULL; |
| 792 | + git_tree *index_parent_tree = NULL; |
| 793 | + git_tree *untracked_tree = NULL; |
| 794 | + git_index *repo_index = NULL; |
| 795 | + git_tree *start_index_tree = NULL; |
| 796 | + git_tree *unstashed_tree = NULL; |
| 797 | + int error; |
| 798 | + |
| 799 | + /* Retrieve commit corresponding to the given stash */ |
| 800 | + if ((error = retrieve_stash_commit(&stash_commit, repo, index)) < 0) |
| 801 | + goto cleanup; |
| 802 | + |
| 803 | + /* Retrieve all trees in the stash */ |
| 804 | + if ((error = retrieve_stash_trees( |
| 805 | + &stash_tree, &base_tree, &index_tree, |
| 806 | + &index_parent_tree, &untracked_tree, stash_commit)) < 0) |
| 807 | + goto cleanup; |
| 808 | + |
| 809 | + /* Load repo index */ |
| 810 | + if ((error = git_repository_index(&repo_index, repo)) < 0) |
| 811 | + goto cleanup; |
| 812 | + |
| 813 | + /* Create tree from index */ |
| 814 | + if ((error = build_tree_from_index(&start_index_tree, repo_index)) < 0) |
| 815 | + goto cleanup; |
| 816 | + |
| 817 | + /* Restore index if required */ |
| 818 | + if (reinstate_index && |
| 819 | + git_oid_cmp(git_tree_id(base_tree), git_tree_id(index_tree)) && |
| 820 | + git_oid_cmp(git_tree_id(start_index_tree), git_tree_id(index_tree))) { |
| 821 | + |
| 822 | + if ((error = apply_index( |
| 823 | + &unstashed_tree, repo, start_index_tree, |
| 824 | + index_parent_tree, index_tree)) < 0) |
| 825 | + goto cleanup; |
| 826 | + } |
| 827 | + |
| 828 | + /* If applicable, restore untracked / ignored files in workdir */ |
| 829 | + if (untracked_tree) { |
| 830 | + if ((error = apply_untracked(repo, untracked_tree)) < 0) |
| 831 | + goto cleanup; |
| 832 | + } |
| 833 | + |
| 834 | + /* Restore modified files in workdir */ |
| 835 | + if ((error = apply_modified( |
| 836 | + repo, base_tree, start_index_tree, stash_tree, unstashed_tree ? 0 : 1)) < 0) |
| 837 | + goto cleanup; |
| 838 | + |
| 839 | + /* Unstage modified files from index */ |
| 840 | + if ((error = unstage_modified_files( |
| 841 | + repo, repo_index, unstashed_tree, start_index_tree)) < 0) |
| 842 | + goto cleanup; |
| 843 | + |
| 844 | + /* Write updated index */ |
| 845 | + if ((error = git_index_write(repo_index)) < 0) |
| 846 | + goto cleanup; |
| 847 | + |
| 848 | +cleanup: |
| 849 | + git_tree_free(unstashed_tree); |
| 850 | + git_tree_free(start_index_tree); |
| 851 | + git_index_free(repo_index); |
| 852 | + git_tree_free(untracked_tree); |
| 853 | + git_tree_free(index_parent_tree); |
| 854 | + git_tree_free(index_tree); |
| 855 | + git_tree_free(base_tree); |
| 856 | + git_tree_free(stash_tree); |
| 857 | + git_commit_free(stash_commit); |
| 858 | + return error; |
| 859 | +} |
| 860 | + |
558 | 861 | int git_stash_foreach( |
559 | 862 | git_repository *repo, |
560 | 863 | git_stash_cb callback, |
@@ -653,3 +956,16 @@ int git_stash_drop( |
653 | 956 | git_reflog_free(reflog); |
654 | 957 | return error; |
655 | 958 | } |
| 959 | + |
| 960 | +int git_stash_pop( |
| 961 | + git_repository *repo, |
| 962 | + size_t index, |
| 963 | + int reinstate_index) |
| 964 | +{ |
| 965 | + int error; |
| 966 | + |
| 967 | + if ((error = git_stash_apply(repo, index, reinstate_index)) < 0) |
| 968 | + return error; |
| 969 | + |
| 970 | + return git_stash_drop(repo, index); |
| 971 | +} |
0 commit comments