From bf8dd3f53d25b7f6032b971d3d73931da05982cb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Latour Date: Fri, 14 Nov 2014 12:32:47 +0900 Subject: [PATCH 01/17] Added git_stash_apply() and git_stash_pop() APIs --- AUTHORS | 1 + include/git2/stash.h | 68 +++++++- src/stash.c | 371 +++++++++++++++++++++++++++++++++++++++++++ tests/stash/apply.c | 215 +++++++++++++++++++++++++ 4 files changed, 651 insertions(+), 4 deletions(-) create mode 100644 tests/stash/apply.c diff --git a/AUTHORS b/AUTHORS index 2eaec0ff6..61e2113ec 100644 --- a/AUTHORS +++ b/AUTHORS @@ -49,6 +49,7 @@ Microsoft Corporation Olivier Ramonat Peter Drahoš Pierre Habouzit +Pierre-Olivier Latour Przemyslaw Pawelczyk Ramsay Jones Robert G. Jakabosky diff --git a/include/git2/stash.h b/include/git2/stash.h index 280c647e8..f69ba307a 100644 --- a/include/git2/stash.h +++ b/include/git2/stash.h @@ -70,6 +70,47 @@ GIT_EXTERN(int) git_stash_save( const char *message, unsigned int flags); +typedef enum { + GIT_APPLY_DEFAULT = 0, + + /* Try to reinstate not only the working tree's changes, + * but also the index's ones. + */ + GIT_APPLY_REINSTATE_INDEX = (1 << 0), +} git_apply_flags; + +/** + * Apply a single stashed state from the stash list. + * + * If any untracked or ignored file saved in the stash already exist in the + * workdir, the function will return GIT_EEXISTS and both the workdir and index + * will be left untouched. + * + * If local changes in the workdir would be overwritten when applying + * modifications saved in the stash, the function will return GIT_EMERGECONFLICT + * and the index will be left untouched. The workdir files will be left + * unmodified as well but restored untracked or ignored files that were saved + * in the stash will be left around in the workdir. + * + * If passing the GIT_APPLY_REINSTATE_INDEX flag and there would be conflicts + * when reinstating the index, the function will return GIT_EUNMERGED and both + * the workdir and index will be left untouched. + * + * @param repo The owning repository. + * + * @param index The position within the stash list. 0 points to the + * most recent stashed state. + * + * @param flags Flags to control the applying process. (see GIT_APPLY_* above) + * + * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the given + * index, or error code. (see details above) + */ +GIT_EXTERN(int) git_stash_apply( + git_repository *repo, + size_t index, + unsigned int flags); + /** * This is a callback function you can provide to iterate over all the * stashed states that will be invoked per entry. @@ -79,7 +120,7 @@ GIT_EXTERN(int) git_stash_save( * @param message The stash message. * @param stash_id The commit oid of the stashed state. * @param payload Extra parameter to callback function. - * @return 0 to continue iterating or non-zero to stop + * @return 0 to continue iterating or non-zero to stop. */ typedef int (*git_stash_cb)( size_t index, @@ -99,7 +140,7 @@ typedef int (*git_stash_cb)( * * @param payload Extra parameter to callback function. * - * @return 0 on success, non-zero callback return value, or error code + * @return 0 on success, non-zero callback return value, or error code. */ GIT_EXTERN(int) git_stash_foreach( git_repository *repo, @@ -114,13 +155,32 @@ GIT_EXTERN(int) git_stash_foreach( * @param index The position within the stash list. 0 points to the * most recent stashed state. * - * @return 0 on success, or error code + * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the given + * index, or error code. */ - GIT_EXTERN(int) git_stash_drop( git_repository *repo, size_t index); +/** + * Apply a single stashed state from the stash list and remove it from the list + * if successful. + * + * @param repo The owning repository. + * + * @param index The position within the stash list. 0 points to the + * most recent stashed state. + * + * @param flags Flags to control the applying process. (see GIT_APPLY_* above) + * + * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the given + * index, or error code. (see git_stash_apply() above for details) +*/ +GIT_EXTERN(int) git_stash_pop( + git_repository *repo, + size_t index, + unsigned int flags); + /** @} */ GIT_END_DECL #endif diff --git a/src/stash.c b/src/stash.c index 8aa48cafe..dab32552b 100644 --- a/src/stash.c +++ b/src/stash.c @@ -16,6 +16,7 @@ #include "git2/checkout.h" #include "git2/index.h" #include "git2/transaction.h" +#include "git2/merge.h" #include "signature.h" static int create_error(int error, const char *msg) @@ -553,6 +554,363 @@ cleanup: return error; } +static int retrieve_stash_commit( + git_commit **commit, + git_repository *repo, + size_t index) +{ + git_reference *stash = NULL; + git_reflog *reflog = NULL; + int error; + size_t max; + const git_reflog_entry *entry; + + if ((error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE)) < 0) + goto cleanup; + + if ((error = git_reflog_read(&reflog, repo, GIT_REFS_STASH_FILE)) < 0) + goto cleanup; + + max = git_reflog_entrycount(reflog); + if (index > max - 1) { + error = GIT_ENOTFOUND; + giterr_set(GITERR_STASH, "No stashed state at position %" PRIuZ, index); + goto cleanup; + } + + entry = git_reflog_entry_byindex(reflog, index); + if ((error = git_commit_lookup(commit, repo, git_reflog_entry_id_new(entry))) < 0) + goto cleanup; + +cleanup: + git_reference_free(stash); + git_reflog_free(reflog); + return error; +} + +static int retrieve_stash_trees( + git_tree **out_stash_tree, + git_tree **out_base_tree, + git_tree **out_index_tree, + git_tree **out_index_parent_tree, + git_tree **out_untracked_tree, + git_commit *stash_commit) +{ + git_tree *stash_tree = NULL; + git_commit *base_commit = NULL; + git_tree *base_tree = NULL; + git_commit *index_commit = NULL; + git_tree *index_tree = NULL; + git_commit *index_parent_commit = NULL; + git_tree *index_parent_tree = NULL; + git_commit *untracked_commit = NULL; + git_tree *untracked_tree = NULL; + int error; + + if ((error = git_commit_tree(&stash_tree, stash_commit)) < 0) + goto cleanup; + + if ((error = git_commit_parent(&base_commit, stash_commit, 0)) < 0) + goto cleanup; + if ((error = git_commit_tree(&base_tree, base_commit)) < 0) + goto cleanup; + + if ((error = git_commit_parent(&index_commit, stash_commit, 1)) < 0) + goto cleanup; + if ((error = git_commit_tree(&index_tree, index_commit)) < 0) + goto cleanup; + + if ((error = git_commit_parent(&index_parent_commit, index_commit, 0)) < 0) + goto cleanup; + if ((error = git_commit_tree(&index_parent_tree, index_parent_commit)) < 0) + goto cleanup; + + if (git_commit_parentcount(stash_commit) == 3) { + if ((error = git_commit_parent(&untracked_commit, stash_commit, 2)) < 0) + goto cleanup; + if ((error = git_commit_tree(&untracked_tree, untracked_commit)) < 0) + goto cleanup; + } + + *out_stash_tree = stash_tree; + *out_base_tree = base_tree; + *out_index_tree = index_tree; + *out_index_parent_tree = index_parent_tree; + *out_untracked_tree = untracked_tree; + +cleanup: + git_commit_free(untracked_commit); + git_commit_free(index_parent_commit); + git_commit_free(index_commit); + git_commit_free(base_commit); + if (error < 0) { + git_tree_free(stash_tree); + git_tree_free(base_tree); + git_tree_free(index_tree); + git_tree_free(index_parent_tree); + git_tree_free(untracked_tree); + } + return error; +} + +static int apply_index( + git_tree **unstashed_tree, + git_repository *repo, + git_tree *start_index_tree, + git_tree *index_parent_tree, + git_tree *index_tree) +{ + git_index* unstashed_index = NULL; + git_merge_options options = GIT_MERGE_OPTIONS_INIT; + int error; + git_oid oid; + + if ((error = git_merge_trees( + &unstashed_index, repo, index_parent_tree, + start_index_tree, index_tree, &options)) < 0) + goto cleanup; + + if ((error = git_index_write_tree_to(&oid, unstashed_index, repo)) < 0) + goto cleanup; + + if ((error = git_tree_lookup(unstashed_tree, repo, &oid)) < 0) + goto cleanup; + +cleanup: + git_index_free(unstashed_index); + return error; +} + +static int apply_untracked( + git_repository *repo, + git_tree *untracked_tree) +{ + git_checkout_options options = GIT_CHECKOUT_OPTIONS_INIT; + size_t i, count; + unsigned int status; + int error; + + for (i = 0, count = git_tree_entrycount(untracked_tree); i < count; ++i) { + const git_tree_entry *entry = git_tree_entry_byindex(untracked_tree, i); + const char* path = git_tree_entry_name(entry); + error = git_status_file(&status, repo, path); + if (!error) { + giterr_set(GITERR_STASH, "Untracked or ignored file '%s' already exists", path); + return GIT_EEXISTS; + } + } + + /* + The untracked tree only contains the untracked / ignores files so checking + it out would remove all other files in the workdir. Since git_checkout_tree() + does not have a mode to leave removed files alone, we emulate it by checking + out files from the untracked tree one by one. + */ + + options.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_DONT_UPDATE_INDEX; + options.paths.count = 1; + for (i = 0, count = git_tree_entrycount(untracked_tree); i < count; ++i) { + const git_tree_entry *entry = git_tree_entry_byindex(untracked_tree, i); + + const char* name = git_tree_entry_name(entry); + options.paths.strings = (char**)&name; + if ((error = git_checkout_tree( + repo, (git_object*)untracked_tree, &options)) < 0) + return error; + } + + return 0; +} + +static int checkout_modified_notify_callback( + git_checkout_notify_t why, + const char *path, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, + void *payload) +{ + unsigned int status; + int error; + + GIT_UNUSED(why); + GIT_UNUSED(baseline); + GIT_UNUSED(target); + GIT_UNUSED(workdir); + + if ((error = git_status_file(&status, payload, path)) < 0) + return error; + + if (status & GIT_STATUS_WT_MODIFIED) { + giterr_set(GITERR_STASH, "Local changes to '%s' would be overwritten", path); + return GIT_EMERGECONFLICT; + } + + return 0; +} + +static int apply_modified( + int *has_conflicts, + git_repository *repo, + git_tree *base_tree, + git_tree *start_index_tree, + git_tree *stash_tree, + unsigned int flags) +{ + git_index *index = NULL; + git_merge_options merge_options = GIT_MERGE_OPTIONS_INIT; + git_checkout_options checkout_options = GIT_CHECKOUT_OPTIONS_INIT; + int error; + + if ((error = git_merge_trees( + &index, repo, base_tree, + start_index_tree, stash_tree, &merge_options)) < 0) + goto cleanup; + + checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS; + if ((flags & GIT_APPLY_REINSTATE_INDEX) && !git_index_has_conflicts(index)) { + /* No need to update the index if it will be overridden later on */ + checkout_options.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + } + checkout_options.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICT; + checkout_options.notify_cb = checkout_modified_notify_callback; + checkout_options.notify_payload = repo; + checkout_options.our_label = "Updated upstream"; + checkout_options.their_label = "Stashed changes"; + if ((error = git_checkout_index(repo, index, &checkout_options)) < 0) + goto cleanup; + + *has_conflicts = git_index_has_conflicts(index); + +cleanup: + git_index_free(index); + return error; +} + +static int unstage_modified_files( + git_repository *repo, + git_index *repo_index, + git_tree *unstashed_tree, + git_tree *start_index_tree) +{ + git_diff *diff = NULL; + git_diff_options options = GIT_DIFF_OPTIONS_INIT; + size_t i, count; + int error; + + if (unstashed_tree) { + if ((error = git_index_read_tree(repo_index, unstashed_tree)) < 0) + goto cleanup; + } else { + options.flags = GIT_DIFF_FORCE_BINARY; + if ((error = git_diff_tree_to_index(&diff, repo, start_index_tree, + repo_index, &options)) < 0) + goto cleanup; + + /* + This behavior is not 100% similar to "git stash apply" as the latter uses + "git-read-tree --reset {treeish}" which preserves the stat()s from the + index instead of replacing them with the tree ones for identical files. + */ + + if ((error = git_index_read_tree(repo_index, start_index_tree)) < 0) + goto cleanup; + + for (i = 0, count = git_diff_num_deltas(diff); i < count; ++i) { + const git_diff_delta* delta = git_diff_get_delta(diff, i); + if (delta->status == GIT_DELTA_ADDED) { + if ((error = git_index_add_bypath( + repo_index, delta->new_file.path)) < 0) + goto cleanup; + } + } + } + +cleanup: + git_diff_free(diff); + return error; +} + +int git_stash_apply( + git_repository *repo, + size_t index, + unsigned int flags) +{ + git_commit *stash_commit = NULL; + git_tree *stash_tree = NULL; + git_tree *base_tree = NULL; + git_tree *index_tree = NULL; + git_tree *index_parent_tree = NULL; + git_tree *untracked_tree = NULL; + git_index *repo_index = NULL; + git_tree *start_index_tree = NULL; + git_tree *unstashed_tree = NULL; + int has_conflicts; + int error; + + /* Retrieve commit corresponding to the given stash */ + if ((error = retrieve_stash_commit(&stash_commit, repo, index)) < 0) + goto cleanup; + + /* Retrieve all trees in the stash */ + if ((error = retrieve_stash_trees( + &stash_tree, &base_tree, &index_tree, + &index_parent_tree, &untracked_tree, stash_commit)) < 0) + goto cleanup; + + /* Load repo index */ + if ((error = git_repository_index(&repo_index, repo)) < 0) + goto cleanup; + + /* Create tree from index */ + if ((error = build_tree_from_index(&start_index_tree, repo_index)) < 0) + goto cleanup; + + /* Restore index if required */ + if ((flags & GIT_APPLY_REINSTATE_INDEX) && + git_oid_cmp(git_tree_id(base_tree), git_tree_id(index_tree)) && + git_oid_cmp(git_tree_id(start_index_tree), git_tree_id(index_tree))) { + + if ((error = apply_index( + &unstashed_tree, repo, start_index_tree, + index_parent_tree, index_tree)) < 0) + goto cleanup; + } + + /* If applicable, restore untracked / ignored files in workdir */ + if (untracked_tree) { + if ((error = apply_untracked(repo, untracked_tree)) < 0) + goto cleanup; + } + + /* Restore modified files in workdir */ + if ((error = apply_modified( + &has_conflicts, repo, base_tree, start_index_tree, + stash_tree, flags)) < 0) + goto cleanup; + + /* Unstage modified files from index unless there were merge conflicts */ + if (!has_conflicts && (error = unstage_modified_files( + repo, repo_index, unstashed_tree, start_index_tree)) < 0) + goto cleanup; + + /* Write updated index */ + if ((error = git_index_write(repo_index)) < 0) + goto cleanup; + +cleanup: + git_tree_free(unstashed_tree); + git_tree_free(start_index_tree); + git_index_free(repo_index); + git_tree_free(untracked_tree); + git_tree_free(index_parent_tree); + git_tree_free(index_tree); + git_tree_free(base_tree); + git_tree_free(stash_tree); + git_commit_free(stash_commit); + return error; +} + int git_stash_foreach( git_repository *repo, git_stash_cb callback, @@ -651,3 +1009,16 @@ cleanup: git_reflog_free(reflog); return error; } + +int git_stash_pop( + git_repository *repo, + size_t index, + unsigned int flags) +{ + int error; + + if ((error = git_stash_apply(repo, index, flags)) < 0) + return error; + + return git_stash_drop(repo, index); +} diff --git a/tests/stash/apply.c b/tests/stash/apply.c new file mode 100644 index 000000000..5f3cc9a16 --- /dev/null +++ b/tests/stash/apply.c @@ -0,0 +1,215 @@ +#include "clar_libgit2.h" +#include "fileops.h" +#include "stash_helpers.h" + +static git_signature *signature; +static git_repository *repo; +static git_index *repo_index; + +void test_stash_apply__initialize(void) +{ + git_oid oid; + + cl_git_pass(git_signature_new(&signature, "nulltoken", "emeric.fermas@gmail.com", 1323847743, 60)); /* Wed Dec 14 08:29:03 2011 +0100 */ + + cl_git_pass(git_repository_init(&repo, "stash", 0)); + cl_git_pass(git_repository_index(&repo_index, repo)); + + cl_git_mkfile("stash/what", "hello\n"); + cl_git_mkfile("stash/how", "small\n"); + cl_git_mkfile("stash/who", "world\n"); + + cl_git_pass(git_index_add_bypath(repo_index, "what")); + cl_git_pass(git_index_add_bypath(repo_index, "how")); + cl_git_pass(git_index_add_bypath(repo_index, "who")); + + cl_repo_commit_from_index(NULL, repo, signature, 0, "Initial commit"); + + cl_git_rewritefile("stash/what", "goodbye\n"); + cl_git_rewritefile("stash/who", "funky world\n"); + cl_git_mkfile("stash/when", "tomorrow\n"); + + cl_git_pass(git_index_add_bypath(repo_index, "who")); + + /* Pre-stash state */ + assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_INDEX_MODIFIED); + assert_status(repo, "when", GIT_STATUS_WT_NEW); + + cl_git_pass(git_stash_save(&oid, repo, signature, NULL, GIT_STASH_INCLUDE_UNTRACKED)); + + /* Post-stash state */ + assert_status(repo, "what", GIT_STATUS_CURRENT); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_CURRENT); + assert_status(repo, "when", GIT_ENOTFOUND); +} + +void test_stash_apply__cleanup(void) +{ + git_signature_free(signature); + signature = NULL; + + git_index_free(repo_index); + repo_index = NULL; + + git_repository_free(repo); + repo = NULL; + + cl_git_pass(git_futils_rmdir_r("stash", NULL, GIT_RMDIR_REMOVE_FILES)); + cl_fixture_cleanup("sorry-it-is-a-non-bare-only-party"); +} + +void test_stash_apply__with_default(void) +{ + cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT)); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); + assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_WT_MODIFIED); + assert_status(repo, "when", GIT_STATUS_WT_NEW); +} + +void test_stash_apply__with_reinstate_index(void) +{ + cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX)); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); + assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_INDEX_MODIFIED); + assert_status(repo, "when", GIT_STATUS_WT_NEW); +} + +void test_stash_apply__conflict_index_with_default(void) +{ + const git_index_entry *ancestor; + const git_index_entry *our; + const git_index_entry *their; + + cl_git_rewritefile("stash/who", "nothing\n"); + cl_git_pass(git_index_add_bypath(repo_index, "who")); + cl_git_pass(git_index_write(repo_index)); + + cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT)); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 1); + assert_status(repo, "what", GIT_STATUS_INDEX_MODIFIED); + assert_status(repo, "how", GIT_STATUS_CURRENT); + cl_git_pass(git_index_conflict_get(&ancestor, &our, &their, repo_index, "who")); /* unmerged */ + assert_status(repo, "when", GIT_STATUS_WT_NEW); +} + +void test_stash_apply__conflict_index_with_reinstate_index(void) +{ + cl_git_rewritefile("stash/who", "nothing\n"); + cl_git_pass(git_index_add_bypath(repo_index, "who")); + cl_git_pass(git_index_write(repo_index)); + + cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX), GIT_EUNMERGED); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); + assert_status(repo, "what", GIT_STATUS_CURRENT); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_INDEX_MODIFIED); + assert_status(repo, "when", GIT_ENOTFOUND); +} + +void test_stash_apply__conflict_untracked_with_default(void) +{ + cl_git_mkfile("stash/when", "nothing\n"); + + cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT), GIT_EEXISTS); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); + assert_status(repo, "what", GIT_STATUS_CURRENT); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_CURRENT); + assert_status(repo, "when", GIT_STATUS_WT_NEW); +} + +void test_stash_apply__conflict_untracked_with_reinstate_index(void) +{ + cl_git_mkfile("stash/when", "nothing\n"); + + cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX), GIT_EEXISTS); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); + assert_status(repo, "what", GIT_STATUS_CURRENT); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_CURRENT); + assert_status(repo, "when", GIT_STATUS_WT_NEW); +} + +void test_stash_apply__conflict_workdir_with_default(void) +{ + cl_git_rewritefile("stash/what", "ciao\n"); + + cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT), GIT_EMERGECONFLICT); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); + assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_CURRENT); + assert_status(repo, "when", GIT_STATUS_WT_NEW); +} + +void test_stash_apply__conflict_workdir_with_reinstate_index(void) +{ + cl_git_rewritefile("stash/what", "ciao\n"); + + cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); + assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_CURRENT); + assert_status(repo, "when", GIT_STATUS_WT_NEW); +} + +void test_stash_apply__conflict_commit_with_default(void) +{ + const git_index_entry *ancestor; + const git_index_entry *our; + const git_index_entry *their; + + cl_git_rewritefile("stash/what", "ciao\n"); + cl_git_pass(git_index_add_bypath(repo_index, "what")); + cl_repo_commit_from_index(NULL, repo, signature, 0, "Other commit"); + + cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT)); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 1); + cl_git_pass(git_index_conflict_get(&ancestor, &our, &their, repo_index, "what")); /* unmerged */ + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_INDEX_MODIFIED); + assert_status(repo, "when", GIT_STATUS_WT_NEW); +} + +void test_stash_apply__conflict_commit_with_reinstate_index(void) +{ + const git_index_entry *ancestor; + const git_index_entry *our; + const git_index_entry *their; + + cl_git_rewritefile("stash/what", "ciao\n"); + cl_git_pass(git_index_add_bypath(repo_index, "what")); + cl_repo_commit_from_index(NULL, repo, signature, 0, "Other commit"); + + cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX)); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 1); + cl_git_pass(git_index_conflict_get(&ancestor, &our, &their, repo_index, "what")); /* unmerged */ + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_INDEX_MODIFIED); + assert_status(repo, "when", GIT_STATUS_WT_NEW); +} + +void test_stash_apply__pop(void) +{ + cl_git_pass(git_stash_pop(repo, 0, GIT_APPLY_DEFAULT)); + + cl_git_fail_with(git_stash_pop(repo, 0, GIT_APPLY_DEFAULT), GIT_ENOTFOUND); +} From 9ebb5a3ff31a58a25714fbc98704b0fda0982cdb Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 18 Feb 2015 22:53:40 -0500 Subject: [PATCH 02/17] merge: merge iterators --- src/merge.c | 86 +++++++++++++++++++++++++++--------- src/merge.h | 18 ++++++-- tests/merge/trees/treediff.c | 14 +++++- 3 files changed, 93 insertions(+), 25 deletions(-) diff --git a/src/merge.c b/src/merge.c index bd676aacf..5e7727429 100644 --- a/src/merge.c +++ b/src/merge.c @@ -1451,11 +1451,11 @@ static int merge_diff_list_insert_unmodified( int git_merge_diff_list__find_differences( git_merge_diff_list *diff_list, - const git_tree *ancestor_tree, - const git_tree *our_tree, - const git_tree *their_tree) + git_iterator *ancestor_iter, + git_iterator *our_iter, + git_iterator *their_iter) { - git_iterator *iterators[3] = {0}; + git_iterator *iterators[3] = { ancestor_iter, our_iter, their_iter }; const git_index_entry *items[3] = {0}, *best_cur_item, *cur_items[3]; git_vector_cmp entry_compare = git_index_entry_cmp; struct merge_diff_df_data df_data = {0}; @@ -1463,12 +1463,7 @@ int git_merge_diff_list__find_differences( size_t i, j; int error = 0; - assert(diff_list && (our_tree || their_tree)); - - if ((error = git_iterator_for_tree(&iterators[TREE_IDX_ANCESTOR], (git_tree *)ancestor_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || - (error = git_iterator_for_tree(&iterators[TREE_IDX_OURS], (git_tree *)our_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || - (error = git_iterator_for_tree(&iterators[TREE_IDX_THEIRS], (git_tree *)their_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0) - goto done; + assert(diff_list && (our_iter || their_iter)); /* Set up the iterators */ for (i = 0; i < 3; i++) { @@ -1544,9 +1539,6 @@ int git_merge_diff_list__find_differences( } done: - for (i = 0; i < 3; i++) - git_iterator_free(iterators[i]); - if (error == GIT_ITEROVER) error = 0; @@ -1757,14 +1749,28 @@ on_error: return error; } -int git_merge_trees( +static git_iterator *iterator_given_or_empty(git_iterator **empty, git_iterator *given) +{ + if (given) + return given; + + if (git_iterator_for_nothing(empty, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL) < 0) + return NULL; + + return *empty; +} + +int git_merge__iterators( git_index **out, git_repository *repo, - const git_tree *ancestor_tree, - const git_tree *our_tree, - const git_tree *their_tree, + git_iterator *ancestor_iter, + git_iterator *our_iter, + git_iterator *theirs_iter, const git_merge_options *given_opts) { + git_iterator *empty_ancestor = NULL, + *empty_ours = NULL, + *empty_theirs = NULL; git_merge_diff_list *diff_list; git_merge_options opts; git_merge_diff *conflict; @@ -1772,11 +1778,12 @@ int git_merge_trees( size_t i; int error = 0; - assert(out && repo && (our_tree || their_tree)); + assert(out && repo); *out = NULL; - GITERR_CHECK_VERSION(given_opts, GIT_MERGE_OPTIONS_VERSION, "git_merge_options"); + GITERR_CHECK_VERSION( + given_opts, GIT_MERGE_OPTIONS_VERSION, "git_merge_options"); if ((error = merge_normalize_opts(repo, &opts, given_opts)) < 0) return error; @@ -1784,7 +1791,12 @@ int git_merge_trees( diff_list = git_merge_diff_list__alloc(repo); GITERR_CHECK_ALLOC(diff_list); - if ((error = git_merge_diff_list__find_differences(diff_list, ancestor_tree, our_tree, their_tree)) < 0 || + ancestor_iter = iterator_given_or_empty(&empty_ancestor, ancestor_iter); + our_iter = iterator_given_or_empty(&empty_ours, our_iter); + theirs_iter = iterator_given_or_empty(&empty_theirs, theirs_iter); + + if ((error = git_merge_diff_list__find_differences( + diff_list, ancestor_iter, our_iter, theirs_iter)) < 0 || (error = git_merge_diff_list__find_renames(repo, diff_list, &opts)) < 0) goto done; @@ -1808,10 +1820,44 @@ int git_merge_trees( done: git_merge_diff_list__free(diff_list); + git_iterator_free(empty_ancestor); + git_iterator_free(empty_ours); + git_iterator_free(empty_theirs); return error; } +int git_merge_trees( + git_index **out, + git_repository *repo, + const git_tree *ancestor_tree, + const git_tree *our_tree, + const git_tree *their_tree, + const git_merge_options *merge_opts) +{ + git_iterator *ancestor_iter = NULL, *our_iter = NULL, *their_iter = NULL; + int error; + + if ((error = git_iterator_for_tree(&ancestor_iter, (git_tree *)ancestor_tree, + GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || + (error = git_iterator_for_tree(&our_iter, (git_tree *)our_tree, + GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || + (error = git_iterator_for_tree(&their_iter, (git_tree *)their_tree, + GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0) + goto done; + + error = git_merge__iterators( + out, repo, ancestor_iter, our_iter, their_iter, merge_opts); + +done: + git_iterator_free(ancestor_iter); + git_iterator_free(our_iter); + git_iterator_free(their_iter); + + return error; +} + + int git_merge_commits( git_index **out, git_repository *repo, diff --git a/src/merge.h b/src/merge.h index fe4505f8b..3caf617c6 100644 --- a/src/merge.h +++ b/src/merge.h @@ -10,6 +10,7 @@ #include "vector.h" #include "commit_list.h" #include "pool.h" +#include "iterator.h" #include "git2/merge.h" #include "git2/types.h" @@ -121,10 +122,11 @@ int git_merge__bases_many( git_merge_diff_list *git_merge_diff_list__alloc(git_repository *repo); -int git_merge_diff_list__find_differences(git_merge_diff_list *merge_diff_list, - const git_tree *ancestor_tree, - const git_tree *ours_tree, - const git_tree *theirs_tree); +int git_merge_diff_list__find_differences( + git_merge_diff_list *merge_diff_list, + git_iterator *ancestor_iterator, + git_iterator *ours_iter, + git_iterator *theirs_iter); int git_merge_diff_list__find_renames(git_repository *repo, git_merge_diff_list *merge_diff_list, const git_merge_options *opts); @@ -138,6 +140,14 @@ int git_merge__setup( const git_annotated_commit *heads[], size_t heads_len); +int git_merge__iterators( + git_index **out, + git_repository *repo, + git_iterator *ancestor_iter, + git_iterator *our_iter, + git_iterator *their_iter, + const git_merge_options *given_opts); + int git_merge__check_result(git_repository *repo, git_index *index_new); int git_merge__append_conflicts_to_merge_msg(git_repository *repo, git_index *index); diff --git a/tests/merge/trees/treediff.c b/tests/merge/trees/treediff.c index eea651de0..b96c4c4db 100644 --- a/tests/merge/trees/treediff.c +++ b/tests/merge/trees/treediff.c @@ -43,6 +43,7 @@ static void test_find_differences( git_merge_diff_list *merge_diff_list = git_merge_diff_list__alloc(repo); git_oid ancestor_oid, ours_oid, theirs_oid; git_tree *ancestor_tree, *ours_tree, *theirs_tree; + git_iterator *ancestor_iter, *ours_iter, *theirs_iter; git_merge_options opts = GIT_MERGE_OPTIONS_INIT; opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES; @@ -66,7 +67,14 @@ static void test_find_differences( cl_git_pass(git_tree_lookup(&ours_tree, repo, &ours_oid)); cl_git_pass(git_tree_lookup(&theirs_tree, repo, &theirs_oid)); - cl_git_pass(git_merge_diff_list__find_differences(merge_diff_list, ancestor_tree, ours_tree, theirs_tree)); + cl_git_pass(git_iterator_for_tree(&ancestor_iter, ancestor_tree, + GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)); + cl_git_pass(git_iterator_for_tree(&ours_iter, ours_tree, + GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)); + cl_git_pass(git_iterator_for_tree(&theirs_iter, theirs_tree, + GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)); + + cl_git_pass(git_merge_diff_list__find_differences(merge_diff_list, ancestor_iter, ours_iter, theirs_iter)); cl_git_pass(git_merge_diff_list__find_renames(repo, merge_diff_list, &opts)); /* @@ -77,6 +85,10 @@ static void test_find_differences( cl_assert(merge_test_merge_conflicts(&merge_diff_list->conflicts, treediff_conflict_data, treediff_conflict_data_len)); + git_iterator_free(ancestor_iter); + git_iterator_free(ours_iter); + git_iterator_free(theirs_iter); + git_tree_free(ancestor_tree); git_tree_free(ours_tree); git_tree_free(theirs_tree); From 73dce1f688ca6ec8f5c56697dc57af36d15a1fe5 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 16 Mar 2015 18:57:57 -0400 Subject: [PATCH 03/17] checkout: allow baseline to be specified as index Allow the baseline to be specified as an index, so that users need not write their index to a tree just to checkout with that as the baseline. --- include/git2/checkout.h | 1 + src/checkout.c | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/include/git2/checkout.h b/include/git2/checkout.h index ed39bd3cb..58f190719 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -273,6 +273,7 @@ typedef struct git_checkout_options { git_strarray paths; git_tree *baseline; /**< expected content of workdir, defaults to HEAD */ + git_index *baseline_index; /**< expected content of workdir, expressed as an index. */ const char *target_directory; /**< alternative checkout path to workdir */ diff --git a/src/checkout.c b/src/checkout.c index a647ce0b9..6a1d28136 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -2397,7 +2397,7 @@ static int checkout_data_init( &data->can_symlink, repo, GIT_CVAR_SYMLINKS)) < 0) goto cleanup; - if (!data->opts.baseline) { + if (!data->opts.baseline && !data->opts.baseline_index) { data->opts_free_baseline = true; error = checkout_lookup_head_tree(&data->opts.baseline, repo); @@ -2501,12 +2501,21 @@ int git_checkout_iterator( (error = git_iterator_for_workdir_ext( &workdir, data.repo, data.opts.target_directory, index, NULL, iterflags | GIT_ITERATOR_DONT_AUTOEXPAND, - data.pfx, data.pfx)) < 0 || - (error = git_iterator_for_tree( - &baseline, data.opts.baseline, - iterflags, data.pfx, data.pfx)) < 0) + data.pfx, data.pfx)) < 0) goto cleanup; + if (data.opts.baseline_index) { + if ((error = git_iterator_for_index( + &baseline, data.opts.baseline_index, + iterflags, data.pfx, data.pfx)) < 0) + goto cleanup; + } else { + if ((error = git_iterator_for_tree( + &baseline, data.opts.baseline, + iterflags, data.pfx, data.pfx)) < 0) + goto cleanup; + } + /* Should not have case insensitivity mismatch */ assert(git_iterator_ignore_case(workdir) == git_iterator_ignore_case(baseline)); From 35d3976151fca0028523320a994df314241e5851 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 18 Mar 2015 00:25:18 -0400 Subject: [PATCH 04/17] index: introduce git_index_read_index --- src/index.c | 98 ++++++++++++++++++++++++++++++++++++++++ src/index.h | 2 + tests/index/read_index.c | 73 ++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 tests/index/read_index.c diff --git a/src/index.c b/src/index.c index 0d2a03e72..7c0c31052 100644 --- a/src/index.c +++ b/src/index.c @@ -2451,6 +2451,104 @@ int git_index_read_tree(git_index *index, const git_tree *tree) return error; } +int git_index_read_index( + git_index *index, + const git_index *new_index) +{ + git_vector new_entries = GIT_VECTOR_INIT, + remove_entries = GIT_VECTOR_INIT; + git_iterator *index_iterator = NULL; + git_iterator *new_iterator = NULL; + const git_index_entry *old_entry, *new_entry; + git_index_entry *entry; + size_t i; + int error; + + if ((error = git_vector_init(&new_entries, new_index->entries.length, index->entries._cmp)) < 0 || + (error = git_vector_init(&remove_entries, index->entries.length, NULL)) < 0) + goto done; + + if ((error = git_iterator_for_index(&index_iterator, + index, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || + (error = git_iterator_for_index(&new_iterator, + (git_index *)new_index, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0) + goto done; + + if (((error = git_iterator_current(&old_entry, index_iterator)) < 0 && + error != GIT_ITEROVER) || + ((error = git_iterator_current(&new_entry, new_iterator)) < 0 && + error != GIT_ITEROVER)) + goto done; + + while (true) { + int diff; + + if (old_entry && new_entry) + diff = git_index_entry_cmp(old_entry, new_entry); + else if (!old_entry && new_entry) + diff = 1; + else if (old_entry && !new_entry) + diff = -1; + else + break; + + if (diff < 0) { + git_vector_insert(&remove_entries, (git_index_entry *)old_entry); + } else if (diff > 0) { + if ((error = index_entry_dup(&entry, git_index_owner(index), new_entry)) < 0) + goto done; + + git_vector_insert(&new_entries, entry); + } else { + /* Path and stage are equal, if the OID is equal, keep it to + * keep the stat cache data. + */ + if (git_oid_equal(&old_entry->id, &new_entry->id)) { + git_vector_insert(&new_entries, (git_index_entry *)old_entry); + } else { + if ((error = index_entry_dup(&entry, git_index_owner(index), new_entry)) < 0) + goto done; + + git_vector_insert(&new_entries, entry); + git_vector_insert(&remove_entries, (git_index_entry *)old_entry); + } + } + + if (diff <= 0) { + if ((error = git_iterator_advance(&old_entry, index_iterator)) < 0 && + error != GIT_ITEROVER) + goto done; + } + + if (diff >= 0) { + if ((error = git_iterator_advance(&new_entry, new_iterator)) < 0 && + error != GIT_ITEROVER) + goto done; + } + } + + git_index_name_clear(index); + git_index_reuc_clear(index); + + git_vector_swap(&new_entries, &index->entries); + + git_vector_foreach(&remove_entries, i, entry) { + if (index->tree) + git_tree_cache_invalidate_path(index->tree, entry->path); + + index_entry_free(entry); + } + + error = 0; + +done: + git_vector_free(&new_entries); + git_vector_free(&remove_entries); + git_iterator_free(index_iterator); + git_iterator_free(new_iterator); + return error; +} + git_repository *git_index_owner(const git_index *index) { return INDEX_OWNER(index); diff --git a/src/index.h b/src/index.h index 6d2904fdc..0f6f4e86e 100644 --- a/src/index.h +++ b/src/index.h @@ -93,6 +93,8 @@ extern int git_index_snapshot_find( size_t *at_pos, git_vector *snap, git_vector_cmp entry_srch, const char *path, size_t path_len, int stage); +/* Replace an index with a new index */ +int git_index_read_index(git_index *index, const git_index *new_index); typedef struct { git_index *index; diff --git a/tests/index/read_index.c b/tests/index/read_index.c new file mode 100644 index 000000000..82a771d54 --- /dev/null +++ b/tests/index/read_index.c @@ -0,0 +1,73 @@ +#include "clar_libgit2.h" +#include "posix.h" +#include "index.h" + +static git_repository *_repo; +static git_index *_index; + +void test_index_read_index__initialize(void) +{ + git_object *head; + git_reference *head_ref; + + _repo = cl_git_sandbox_init("testrepo"); + cl_git_pass(git_revparse_ext(&head, &head_ref, _repo, "HEAD")); + cl_git_pass(git_reset(_repo, head, GIT_RESET_HARD, NULL)); + cl_git_pass(git_repository_index(&_index, _repo)); + + git_reference_free(head_ref); + git_object_free(head); +} + +void test_index_read_index__cleanup(void) +{ + git_index_free(_index); + cl_git_sandbox_cleanup(); +} + +void test_index_read_index__maintains_stat_cache(void) +{ + git_index *new_index; + git_oid index_id; + git_index_entry new_entry; + const git_index_entry *e; + git_tree *tree; + size_t i; + + cl_assert_equal_i(4, git_index_entrycount(_index)); + + /* write-tree */ + cl_git_pass(git_index_write_tree(&index_id, _index)); + + /* read-tree, then read index */ + git_tree_lookup(&tree, _repo, &index_id); + cl_git_pass(git_index_new(&new_index)); + cl_git_pass(git_index_read_tree(new_index, tree)); + git_tree_free(tree); + + /* add a new entry that will not have stat data */ + memset(&new_entry, 0, sizeof(git_index_entry)); + new_entry.path = "Hello"; + git_oid_fromstr(&new_entry.id, "0123456789012345678901234567890123456789"); + new_entry.file_size = 1234; + new_entry.mode = 0100644; + cl_git_pass(git_index_add(new_index, &new_entry)); + cl_assert_equal_i(5, git_index_entrycount(new_index)); + + cl_git_pass(git_index_read_index(_index, new_index)); + git_index_free(new_index); + + cl_assert_equal_i(5, git_index_entrycount(_index)); + + for (i = 0; i < git_index_entrycount(_index); i++) { + e = git_index_get_byindex(_index, i); + + if (strcmp(e->path, "Hello") == 0) { + cl_assert_equal_i(0, e->ctime.seconds); + cl_assert_equal_i(0, e->mtime.seconds); + } else { + cl_assert(0 != e->ctime.seconds); + cl_assert(0 != e->mtime.seconds); + } + } +} From 7f26b1b9cfcf07a820503dc7597c24914514a242 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 18 Feb 2015 13:24:07 -0500 Subject: [PATCH 05/17] stash: use git_commit_summary for a summary --- src/stash.c | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/stash.c b/src/stash.c index dab32552b..f8bba3a72 100644 --- a/src/stash.c +++ b/src/stash.c @@ -8,6 +8,7 @@ #include "common.h" #include "repository.h" #include "commit.h" +#include "message.h" #include "tree.h" #include "reflog.h" #include "git2/diff.h" @@ -50,23 +51,14 @@ static int append_abbreviated_oid(git_buf *out, const git_oid *b_commit) static int append_commit_description(git_buf *out, git_commit* commit) { - const char *message; - size_t pos = 0, len; + const char *summary = git_commit_summary(commit); + GITERR_CHECK_ALLOC(summary); if (append_abbreviated_oid(out, git_commit_id(commit)) < 0) return -1; - message = git_commit_message(commit); - len = strlen(message); - - /* TODO: Replace with proper commit short message - * when git_commit_message_short() is implemented. - */ - while (pos < len && message[pos] != '\n') - pos++; - git_buf_putc(out, ' '); - git_buf_put(out, message, pos); + git_buf_puts(out, summary); git_buf_putc(out, '\n'); return git_buf_oom(out) ? -1 : 0; From d0dd3fcee7943321784f5745d34130cacd5d9fb4 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 18 Feb 2015 15:16:05 -0500 Subject: [PATCH 06/17] stash apply: check out a tree, not piecewise --- src/stash.c | 43 +++++++++++-------------------------------- tests/stash/apply.c | 4 ++-- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/src/stash.c b/src/stash.c index f8bba3a72..a316d18c2 100644 --- a/src/stash.c +++ b/src/stash.c @@ -652,7 +652,7 @@ static int apply_index( git_tree *index_parent_tree, git_tree *index_tree) { - git_index* unstashed_index = NULL; + git_index *unstashed_index = NULL; git_merge_options options = GIT_MERGE_OPTIONS_INIT; int error; git_oid oid; @@ -675,43 +675,22 @@ cleanup: static int apply_untracked( git_repository *repo, + git_tree *start_index_tree, git_tree *untracked_tree) { git_checkout_options options = GIT_CHECKOUT_OPTIONS_INIT; - size_t i, count; - unsigned int status; + git_index *merged_index = NULL; int error; - for (i = 0, count = git_tree_entrycount(untracked_tree); i < count; ++i) { - const git_tree_entry *entry = git_tree_entry_byindex(untracked_tree, i); - const char* path = git_tree_entry_name(entry); - error = git_status_file(&status, repo, path); - if (!error) { - giterr_set(GITERR_STASH, "Untracked or ignored file '%s' already exists", path); - return GIT_EEXISTS; - } - } + options.checkout_strategy = + GIT_CHECKOUT_SAFE | GIT_CHECKOUT_DONT_UPDATE_INDEX; - /* - The untracked tree only contains the untracked / ignores files so checking - it out would remove all other files in the workdir. Since git_checkout_tree() - does not have a mode to leave removed files alone, we emulate it by checking - out files from the untracked tree one by one. - */ + if ((error = git_merge_trees(&merged_index, + repo, NULL, start_index_tree, untracked_tree, NULL)) == 0) + error = git_checkout_index(repo, merged_index, &options); - options.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_DONT_UPDATE_INDEX; - options.paths.count = 1; - for (i = 0, count = git_tree_entrycount(untracked_tree); i < count; ++i) { - const git_tree_entry *entry = git_tree_entry_byindex(untracked_tree, i); - - const char* name = git_tree_entry_name(entry); - options.paths.strings = (char**)&name; - if ((error = git_checkout_tree( - repo, (git_object*)untracked_tree, &options)) < 0) - return error; - } - - return 0; + git_index_free(merged_index); + return error; } static int checkout_modified_notify_callback( @@ -871,7 +850,7 @@ int git_stash_apply( /* If applicable, restore untracked / ignored files in workdir */ if (untracked_tree) { - if ((error = apply_untracked(repo, untracked_tree)) < 0) + if ((error = apply_untracked(repo, start_index_tree, untracked_tree)) < 0) goto cleanup; } diff --git a/tests/stash/apply.c b/tests/stash/apply.c index 5f3cc9a16..c0cdcabb9 100644 --- a/tests/stash/apply.c +++ b/tests/stash/apply.c @@ -121,7 +121,7 @@ void test_stash_apply__conflict_untracked_with_default(void) { cl_git_mkfile("stash/when", "nothing\n"); - cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT), GIT_EEXISTS); + cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_CURRENT); @@ -134,7 +134,7 @@ void test_stash_apply__conflict_untracked_with_reinstate_index(void) { cl_git_mkfile("stash/when", "nothing\n"); - cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX), GIT_EEXISTS); + cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_CURRENT); From 90f8408dacf0da9def636a042813822b078df735 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 18 Feb 2015 16:33:50 -0500 Subject: [PATCH 07/17] stash: ensure a reflog has entries --- src/stash.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stash.c b/src/stash.c index a316d18c2..899156d8d 100644 --- a/src/stash.c +++ b/src/stash.c @@ -564,7 +564,7 @@ static int retrieve_stash_commit( goto cleanup; max = git_reflog_entrycount(reflog); - if (index > max - 1) { + if (!max || index > max - 1) { error = GIT_ENOTFOUND; giterr_set(GITERR_STASH, "No stashed state at position %" PRIuZ, index); goto cleanup; @@ -949,7 +949,7 @@ int git_stash_drop( max = git_reflog_entrycount(reflog); - if (index > max - 1) { + if (!max || index > max - 1) { error = GIT_ENOTFOUND; giterr_set(GITERR_STASH, "No stashed state at position %" PRIuZ, index); goto cleanup; From f0957589ee57cf5a98e4aa3ce742fb68c3e36f19 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 4 Mar 2015 23:55:42 -0500 Subject: [PATCH 08/17] stash: refactor to use merge_iterators --- include/git2/stash.h | 12 +- src/stash.c | 265 +++++++++++++++---------------------------- tests/stash/apply.c | 24 ++-- 3 files changed, 111 insertions(+), 190 deletions(-) diff --git a/include/git2/stash.h b/include/git2/stash.h index f69ba307a..428d9f250 100644 --- a/include/git2/stash.h +++ b/include/git2/stash.h @@ -97,10 +97,9 @@ typedef enum { * the workdir and index will be left untouched. * * @param repo The owning repository. - * * @param index The position within the stash list. 0 points to the - * most recent stashed state. - * + * most recent stashed state. + * @param checkout_options Options to control how files are checked out * @param flags Flags to control the applying process. (see GIT_APPLY_* above) * * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the given @@ -109,6 +108,7 @@ typedef enum { GIT_EXTERN(int) git_stash_apply( git_repository *repo, size_t index, + const git_checkout_options *checkout_options, unsigned int flags); /** @@ -167,10 +167,9 @@ GIT_EXTERN(int) git_stash_drop( * if successful. * * @param repo The owning repository. - * * @param index The position within the stash list. 0 points to the - * most recent stashed state. - * + * most recent stashed state. + * @param checkout_options Options to control how files are checked out * @param flags Flags to control the applying process. (see GIT_APPLY_* above) * * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the given @@ -179,6 +178,7 @@ GIT_EXTERN(int) git_stash_drop( GIT_EXTERN(int) git_stash_pop( git_repository *repo, size_t index, + const git_checkout_options *checkout_options, unsigned int flags); /** @} */ diff --git a/src/stash.c b/src/stash.c index 899156d8d..d53c22c4d 100644 --- a/src/stash.c +++ b/src/stash.c @@ -18,7 +18,10 @@ #include "git2/index.h" #include "git2/transaction.h" #include "git2/merge.h" +#include "index.h" #include "signature.h" +#include "iterator.h" +#include "merge.h" static int create_error(int error, const char *msg) { @@ -645,187 +648,81 @@ cleanup: return error; } -static int apply_index( - git_tree **unstashed_tree, +static int merge_index_and_tree( + git_index **out, git_repository *repo, - git_tree *start_index_tree, - git_tree *index_parent_tree, - git_tree *index_tree) + git_tree *ancestor_tree, + git_index *ours_index, + git_tree *theirs_tree) { - git_index *unstashed_index = NULL; - git_merge_options options = GIT_MERGE_OPTIONS_INIT; + git_iterator *ancestor = NULL, *ours = NULL, *theirs = NULL; + const git_iterator_flag_t flags = GIT_ITERATOR_DONT_IGNORE_CASE; int error; - git_oid oid; - if ((error = git_merge_trees( - &unstashed_index, repo, index_parent_tree, - start_index_tree, index_tree, &options)) < 0) - goto cleanup; + if ((error = git_iterator_for_tree(&ancestor, ancestor_tree, flags, NULL, NULL)) < 0 || + (error = git_iterator_for_index(&ours, ours_index, flags, NULL, NULL)) < 0 || + (error = git_iterator_for_tree(&theirs, theirs_tree, flags, NULL, NULL)) < 0) + goto done; - if ((error = git_index_write_tree_to(&oid, unstashed_index, repo)) < 0) - goto cleanup; + error = git_merge__iterators(out, repo, ancestor, ours, theirs, NULL); - if ((error = git_tree_lookup(unstashed_tree, repo, &oid)) < 0) - goto cleanup; - -cleanup: - git_index_free(unstashed_index); +done: + git_iterator_free(ancestor); + git_iterator_free(ours); + git_iterator_free(theirs); return error; } -static int apply_untracked( - git_repository *repo, - git_tree *start_index_tree, - git_tree *untracked_tree) -{ - git_checkout_options options = GIT_CHECKOUT_OPTIONS_INIT; - git_index *merged_index = NULL; - int error; - - options.checkout_strategy = - GIT_CHECKOUT_SAFE | GIT_CHECKOUT_DONT_UPDATE_INDEX; - - if ((error = git_merge_trees(&merged_index, - repo, NULL, start_index_tree, untracked_tree, NULL)) == 0) - error = git_checkout_index(repo, merged_index, &options); - - git_index_free(merged_index); - return error; -} - -static int checkout_modified_notify_callback( - git_checkout_notify_t why, - const char *path, - const git_diff_file *baseline, - const git_diff_file *target, - const git_diff_file *workdir, - void *payload) -{ - unsigned int status; - int error; - - GIT_UNUSED(why); - GIT_UNUSED(baseline); - GIT_UNUSED(target); - GIT_UNUSED(workdir); - - if ((error = git_status_file(&status, payload, path)) < 0) - return error; - - if (status & GIT_STATUS_WT_MODIFIED) { - giterr_set(GITERR_STASH, "Local changes to '%s' would be overwritten", path); - return GIT_EMERGECONFLICT; - } - - return 0; -} - -static int apply_modified( - int *has_conflicts, - git_repository *repo, - git_tree *base_tree, - git_tree *start_index_tree, - git_tree *stash_tree, - unsigned int flags) -{ - git_index *index = NULL; - git_merge_options merge_options = GIT_MERGE_OPTIONS_INIT; - git_checkout_options checkout_options = GIT_CHECKOUT_OPTIONS_INIT; - int error; - - if ((error = git_merge_trees( - &index, repo, base_tree, - start_index_tree, stash_tree, &merge_options)) < 0) - goto cleanup; - - checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS; - if ((flags & GIT_APPLY_REINSTATE_INDEX) && !git_index_has_conflicts(index)) { - /* No need to update the index if it will be overridden later on */ - checkout_options.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; - } - checkout_options.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICT; - checkout_options.notify_cb = checkout_modified_notify_callback; - checkout_options.notify_payload = repo; - checkout_options.our_label = "Updated upstream"; - checkout_options.their_label = "Stashed changes"; - if ((error = git_checkout_index(repo, index, &checkout_options)) < 0) - goto cleanup; - - *has_conflicts = git_index_has_conflicts(index); - -cleanup: - git_index_free(index); - return error; -} - -static int unstage_modified_files( - git_repository *repo, - git_index *repo_index, - git_tree *unstashed_tree, - git_tree *start_index_tree) -{ - git_diff *diff = NULL; - git_diff_options options = GIT_DIFF_OPTIONS_INIT; - size_t i, count; - int error; - - if (unstashed_tree) { - if ((error = git_index_read_tree(repo_index, unstashed_tree)) < 0) - goto cleanup; +static void normalize_checkout_options( + git_checkout_options *checkout_opts, + const git_checkout_options *given_checkout_opts) +{ + if (given_checkout_opts != NULL) { + memcpy(checkout_opts, given_checkout_opts, sizeof(git_checkout_options)); } else { - options.flags = GIT_DIFF_FORCE_BINARY; - if ((error = git_diff_tree_to_index(&diff, repo, start_index_tree, - repo_index, &options)) < 0) - goto cleanup; + git_checkout_options default_checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + default_checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; - /* - This behavior is not 100% similar to "git stash apply" as the latter uses - "git-read-tree --reset {treeish}" which preserves the stat()s from the - index instead of replacing them with the tree ones for identical files. - */ - - if ((error = git_index_read_tree(repo_index, start_index_tree)) < 0) - goto cleanup; - - for (i = 0, count = git_diff_num_deltas(diff); i < count; ++i) { - const git_diff_delta* delta = git_diff_get_delta(diff, i); - if (delta->status == GIT_DELTA_ADDED) { - if ((error = git_index_add_bypath( - repo_index, delta->new_file.path)) < 0) - goto cleanup; - } - } + memcpy(checkout_opts, &default_checkout_opts, sizeof(git_checkout_options)); } -cleanup: - git_diff_free(diff); - return error; + if (!checkout_opts->our_label) + checkout_opts->our_label = "Updated upstream"; + + if (!checkout_opts->their_label) + checkout_opts->their_label = "Stashed changes"; } int git_stash_apply( git_repository *repo, size_t index, + const git_checkout_options *given_checkout_opts, unsigned int flags) { + git_checkout_options checkout_opts; + unsigned int checkout_strategy; git_commit *stash_commit = NULL; git_tree *stash_tree = NULL; - git_tree *base_tree = NULL; + git_tree *stash_parent_tree = NULL; git_tree *index_tree = NULL; git_tree *index_parent_tree = NULL; git_tree *untracked_tree = NULL; git_index *repo_index = NULL; - git_tree *start_index_tree = NULL; - git_tree *unstashed_tree = NULL; - int has_conflicts; + git_index *unstashed_index = NULL; + git_index *modified_index = NULL; + git_index *untracked_index = NULL; int error; + normalize_checkout_options(&checkout_opts, given_checkout_opts); + checkout_strategy = checkout_opts.checkout_strategy; + /* Retrieve commit corresponding to the given stash */ if ((error = retrieve_stash_commit(&stash_commit, repo, index)) < 0) goto cleanup; /* Retrieve all trees in the stash */ if ((error = retrieve_stash_trees( - &stash_tree, &base_tree, &index_tree, + &stash_tree, &stash_parent_tree, &index_tree, &index_parent_tree, &untracked_tree, stash_commit)) < 0) goto cleanup; @@ -833,50 +730,73 @@ int git_stash_apply( if ((error = git_repository_index(&repo_index, repo)) < 0) goto cleanup; - /* Create tree from index */ - if ((error = build_tree_from_index(&start_index_tree, repo_index)) < 0) - goto cleanup; - /* Restore index if required */ if ((flags & GIT_APPLY_REINSTATE_INDEX) && - git_oid_cmp(git_tree_id(base_tree), git_tree_id(index_tree)) && - git_oid_cmp(git_tree_id(start_index_tree), git_tree_id(index_tree))) { + git_oid_cmp(git_tree_id(stash_parent_tree), git_tree_id(index_tree))) { - if ((error = apply_index( - &unstashed_tree, repo, start_index_tree, - index_parent_tree, index_tree)) < 0) + if ((error = merge_index_and_tree( + &unstashed_index, repo, index_parent_tree, repo_index, index_tree)) < 0) goto cleanup; - } - /* If applicable, restore untracked / ignored files in workdir */ - if (untracked_tree) { - if ((error = apply_untracked(repo, start_index_tree, untracked_tree)) < 0) + + /* TODO: GIT_EMERGECONFLICT */ + if (git_index_has_conflicts(unstashed_index)) { + error = GIT_EUNMERGED; goto cleanup; + } } /* Restore modified files in workdir */ - if ((error = apply_modified( - &has_conflicts, repo, base_tree, start_index_tree, - stash_tree, flags)) < 0) + if ((error = merge_index_and_tree( + &modified_index, repo, stash_parent_tree, repo_index, stash_tree)) < 0) goto cleanup; - /* Unstage modified files from index unless there were merge conflicts */ - if (!has_conflicts && (error = unstage_modified_files( - repo, repo_index, unstashed_tree, start_index_tree)) < 0) + /* If applicable, restore untracked / ignored files in workdir */ + if (untracked_tree && + (error = merge_index_and_tree(&untracked_index, repo, NULL, repo_index, untracked_tree)) < 0) goto cleanup; - /* Write updated index */ - if ((error = git_index_write(repo_index)) < 0) + if (untracked_index) { + checkout_opts.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + + if ((error = git_checkout_index(repo, untracked_index, &checkout_opts)) < 0) + goto cleanup; + + checkout_opts.checkout_strategy = checkout_strategy; + } + + + /* If there are conflicts in the modified index, then we need to actually + * check that out as the repo's index. Otherwise, we don't update the + * index. + */ + + if (!git_index_has_conflicts(modified_index)) + checkout_opts.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + + /* Check out the modified index using the existing repo index as baseline, + * so that existing modifications in the index can be rewritten even when + * checking out safely. + */ + checkout_opts.baseline_index = repo_index; + + if ((error = git_checkout_index(repo, modified_index, &checkout_opts)) < 0) goto cleanup; + if (unstashed_index && !git_index_has_conflicts(modified_index)) { + if ((error = git_index_read_index(repo_index, unstashed_index)) < 0) + goto cleanup; + } + cleanup: - git_tree_free(unstashed_tree); - git_tree_free(start_index_tree); + git_index_free(untracked_index); + git_index_free(modified_index); + git_index_free(unstashed_index); git_index_free(repo_index); git_tree_free(untracked_tree); git_tree_free(index_parent_tree); git_tree_free(index_tree); - git_tree_free(base_tree); + git_tree_free(stash_parent_tree); git_tree_free(stash_tree); git_commit_free(stash_commit); return error; @@ -984,11 +904,12 @@ cleanup: int git_stash_pop( git_repository *repo, size_t index, + const git_checkout_options *checkout_options, unsigned int flags) { int error; - if ((error = git_stash_apply(repo, index, flags)) < 0) + if ((error = git_stash_apply(repo, index, checkout_options, flags)) < 0) return error; return git_stash_drop(repo, index); diff --git a/tests/stash/apply.c b/tests/stash/apply.c index c0cdcabb9..efe7852b8 100644 --- a/tests/stash/apply.c +++ b/tests/stash/apply.c @@ -63,7 +63,7 @@ void test_stash_apply__cleanup(void) void test_stash_apply__with_default(void) { - cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT)); + cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); @@ -74,7 +74,7 @@ void test_stash_apply__with_default(void) void test_stash_apply__with_reinstate_index(void) { - cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX)); + cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); @@ -93,7 +93,7 @@ void test_stash_apply__conflict_index_with_default(void) cl_git_pass(git_index_add_bypath(repo_index, "who")); cl_git_pass(git_index_write(repo_index)); - cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT)); + cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 1); assert_status(repo, "what", GIT_STATUS_INDEX_MODIFIED); @@ -108,7 +108,7 @@ void test_stash_apply__conflict_index_with_reinstate_index(void) cl_git_pass(git_index_add_bypath(repo_index, "who")); cl_git_pass(git_index_write(repo_index)); - cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX), GIT_EUNMERGED); + cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX), GIT_EUNMERGED); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_CURRENT); @@ -121,7 +121,7 @@ void test_stash_apply__conflict_untracked_with_default(void) { cl_git_mkfile("stash/when", "nothing\n"); - cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT), GIT_EMERGECONFLICT); + cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_CURRENT); @@ -134,7 +134,7 @@ void test_stash_apply__conflict_untracked_with_reinstate_index(void) { cl_git_mkfile("stash/when", "nothing\n"); - cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); + cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_CURRENT); @@ -147,7 +147,7 @@ void test_stash_apply__conflict_workdir_with_default(void) { cl_git_rewritefile("stash/what", "ciao\n"); - cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT), GIT_EMERGECONFLICT); + cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); @@ -160,7 +160,7 @@ void test_stash_apply__conflict_workdir_with_reinstate_index(void) { cl_git_rewritefile("stash/what", "ciao\n"); - cl_git_fail_with(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); + cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); @@ -179,7 +179,7 @@ void test_stash_apply__conflict_commit_with_default(void) cl_git_pass(git_index_add_bypath(repo_index, "what")); cl_repo_commit_from_index(NULL, repo, signature, 0, "Other commit"); - cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_DEFAULT)); + cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 1); cl_git_pass(git_index_conflict_get(&ancestor, &our, &their, repo_index, "what")); /* unmerged */ @@ -198,7 +198,7 @@ void test_stash_apply__conflict_commit_with_reinstate_index(void) cl_git_pass(git_index_add_bypath(repo_index, "what")); cl_repo_commit_from_index(NULL, repo, signature, 0, "Other commit"); - cl_git_pass(git_stash_apply(repo, 0, GIT_APPLY_REINSTATE_INDEX)); + cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 1); cl_git_pass(git_index_conflict_get(&ancestor, &our, &their, repo_index, "what")); /* unmerged */ @@ -209,7 +209,7 @@ void test_stash_apply__conflict_commit_with_reinstate_index(void) void test_stash_apply__pop(void) { - cl_git_pass(git_stash_pop(repo, 0, GIT_APPLY_DEFAULT)); + cl_git_pass(git_stash_pop(repo, 0, NULL, GIT_APPLY_DEFAULT)); - cl_git_fail_with(git_stash_pop(repo, 0, GIT_APPLY_DEFAULT), GIT_ENOTFOUND); + cl_git_fail_with(git_stash_pop(repo, 0, NULL, GIT_APPLY_DEFAULT), GIT_ENOTFOUND); } From 958950b6e077f09821c164d0897e66eca553375c Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 1 May 2015 13:53:46 -0400 Subject: [PATCH 09/17] stash: document merge conflicts --- include/git2/stash.h | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/include/git2/stash.h b/include/git2/stash.h index 428d9f250..070aa9b76 100644 --- a/include/git2/stash.h +++ b/include/git2/stash.h @@ -82,19 +82,16 @@ typedef enum { /** * Apply a single stashed state from the stash list. * - * If any untracked or ignored file saved in the stash already exist in the - * workdir, the function will return GIT_EEXISTS and both the workdir and index - * will be left untouched. - * - * If local changes in the workdir would be overwritten when applying - * modifications saved in the stash, the function will return GIT_EMERGECONFLICT - * and the index will be left untouched. The workdir files will be left - * unmodified as well but restored untracked or ignored files that were saved - * in the stash will be left around in the workdir. + * If local changes in the working directory conflict with changes in the + * stash then GIT_EMERGECONFLICT will be returned. In this case, the index + * will always remain unmodified and all files in the working directory will + * remain unmodified. However, if you are restoring untracked files or + * ignored files and there is a conflict when applying the modified files, + * then those files will remain in the working directory. * * If passing the GIT_APPLY_REINSTATE_INDEX flag and there would be conflicts - * when reinstating the index, the function will return GIT_EUNMERGED and both - * the workdir and index will be left untouched. + * when reinstating the index, the function will return GIT_EMERGECONFLICT + * and both the working directory and index will be left unmodified. * * @param repo The owning repository. * @param index The position within the stash list. 0 points to the @@ -102,8 +99,9 @@ typedef enum { * @param checkout_options Options to control how files are checked out * @param flags Flags to control the applying process. (see GIT_APPLY_* above) * - * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the given - * index, or error code. (see details above) + * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the + * given index, GIT_EMERGECONFLICT if changes exist in the working + * directory, or an error code */ GIT_EXTERN(int) git_stash_apply( git_repository *repo, From f78bb2afb3a7f23264c4adf2e9debfa415ca8107 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Wed, 18 Mar 2015 01:54:04 -0400 Subject: [PATCH 10/17] stash: return GIT_EMERGECONFLICT on merge conflict --- src/stash.c | 4 +--- tests/stash/apply.c | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/stash.c b/src/stash.c index d53c22c4d..e3e025771 100644 --- a/src/stash.c +++ b/src/stash.c @@ -738,10 +738,8 @@ int git_stash_apply( &unstashed_index, repo, index_parent_tree, repo_index, index_tree)) < 0) goto cleanup; - - /* TODO: GIT_EMERGECONFLICT */ if (git_index_has_conflicts(unstashed_index)) { - error = GIT_EUNMERGED; + error = GIT_EMERGECONFLICT; goto cleanup; } } diff --git a/tests/stash/apply.c b/tests/stash/apply.c index efe7852b8..74416cd95 100644 --- a/tests/stash/apply.c +++ b/tests/stash/apply.c @@ -108,7 +108,7 @@ void test_stash_apply__conflict_index_with_reinstate_index(void) cl_git_pass(git_index_add_bypath(repo_index, "who")); cl_git_pass(git_index_write(repo_index)); - cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX), GIT_EUNMERGED); + cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_CURRENT); From 12149a20ef7cfb67563c639f2583bce0cadb4df9 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 20 Apr 2015 20:05:23 -0400 Subject: [PATCH 11/17] stash apply: default to at least GIT_CHECKOUT_SAFE --- include/git2/stash.h | 4 +++- src/stash.c | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/include/git2/stash.h b/include/git2/stash.h index 070aa9b76..bb17933bc 100644 --- a/include/git2/stash.h +++ b/include/git2/stash.h @@ -96,7 +96,9 @@ typedef enum { * @param repo The owning repository. * @param index The position within the stash list. 0 points to the * most recent stashed state. - * @param checkout_options Options to control how files are checked out + * @param checkout_options Options to control how files are checked out. + * A minimum strategy of `GIT_CHECKOUT_SAFE` is + * implied. * @param flags Flags to control the applying process. (see GIT_APPLY_* above) * * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the diff --git a/src/stash.c b/src/stash.c index e3e025771..3f60ee017 100644 --- a/src/stash.c +++ b/src/stash.c @@ -681,11 +681,12 @@ static void normalize_checkout_options( memcpy(checkout_opts, given_checkout_opts, sizeof(git_checkout_options)); } else { git_checkout_options default_checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; - default_checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; - memcpy(checkout_opts, &default_checkout_opts, sizeof(git_checkout_options)); } + if ((checkout_opts->checkout_strategy & (GIT_CHECKOUT_SAFE | GIT_CHECKOUT_FORCE)) == 0) + checkout_opts->checkout_strategy = GIT_CHECKOUT_SAFE; + if (!checkout_opts->our_label) checkout_opts->our_label = "Updated upstream"; From 249616685eba07cd14ec00aa75e8d6904fcfcbe4 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 20 Apr 2015 20:06:02 -0400 Subject: [PATCH 12/17] stash: test checkout notify callbacks --- tests/stash/apply.c | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/stash/apply.c b/tests/stash/apply.c index 74416cd95..2f7e6f205 100644 --- a/tests/stash/apply.c +++ b/tests/stash/apply.c @@ -213,3 +213,55 @@ void test_stash_apply__pop(void) cl_git_fail_with(git_stash_pop(repo, 0, NULL, GIT_APPLY_DEFAULT), GIT_ENOTFOUND); } + +struct seen_paths { + bool what; + bool how; + bool who; + bool when; +}; + +int checkout_notify( + git_checkout_notify_t why, + const char *path, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, + void *payload) +{ + struct seen_paths *seen_paths = (struct seen_paths *)payload; + + if (strcmp(path, "what") == 0) + seen_paths->what = 1; + else if (strcmp(path, "how") == 0) + seen_paths->how = 1; + else if (strcmp(path, "who") == 0) + seen_paths->who = 1; + else if (strcmp(path, "when") == 0) + seen_paths->when = 1; + + return 0; +} + +void test_stash_apply__executes_notify_cb(void) +{ + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + struct seen_paths seen_paths = {0}; + + checkout_opts.notify_cb = checkout_notify; + checkout_opts.notify_flags = GIT_CHECKOUT_NOTIFY_ALL; + checkout_opts.notify_payload = &seen_paths; + + cl_git_pass(git_stash_apply(repo, 0, &checkout_opts, GIT_APPLY_DEFAULT)); + + cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); + assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); + assert_status(repo, "how", GIT_STATUS_CURRENT); + assert_status(repo, "who", GIT_STATUS_WT_MODIFIED); + assert_status(repo, "when", GIT_STATUS_WT_NEW); + + cl_assert_equal_b(true, seen_paths.what); + cl_assert_equal_b(false, seen_paths.how); + cl_assert_equal_b(true, seen_paths.who); + cl_assert_equal_b(true, seen_paths.when); +} From 19c80a6fd1cfc0300e3a99f72fb8056431ba521e Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 1 May 2015 18:07:10 -0400 Subject: [PATCH 13/17] stash_apply: provide its own options structure --- include/git2/stash.h | 67 ++++++++++++++++++++++++++++++----------- src/stash.c | 63 +++++++++++++++++++++----------------- tests/core/structinit.c | 5 +++ tests/stash/apply.c | 55 ++++++++++++++++++++++----------- 4 files changed, 128 insertions(+), 62 deletions(-) diff --git a/include/git2/stash.h b/include/git2/stash.h index bb17933bc..1f773dcb0 100644 --- a/include/git2/stash.h +++ b/include/git2/stash.h @@ -70,14 +70,50 @@ GIT_EXTERN(int) git_stash_save( const char *message, unsigned int flags); +/** Stash application flags. */ typedef enum { - GIT_APPLY_DEFAULT = 0, + GIT_STASH_APPLY_DEFAULT = 0, /* Try to reinstate not only the working tree's changes, - * but also the index's ones. + * but also the index's changes. */ - GIT_APPLY_REINSTATE_INDEX = (1 << 0), -} git_apply_flags; + GIT_STASH_APPLY_REINSTATE_INDEX = (1 << 0), +} git_stash_apply_flags; + +/** Stash application options structure. + * + * Initialize with the `GIT_STASH_APPLY_OPTIONS_INIT` macro to set + * sensible defaults; for example: + * + * git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; + */ +typedef struct git_stash_apply_options { + unsigned int version; + + /** See `git_stash_apply_flags_t`, above. */ + git_stash_apply_flags flags; + + /** Options to use when writing files to the working directory. */ + git_checkout_options checkout_options; +} git_stash_apply_options; + +#define GIT_STASH_APPLY_OPTIONS_VERSION 1 +#define GIT_STASH_APPLY_OPTIONS_INIT { \ + GIT_STASH_APPLY_OPTIONS_VERSION, \ + GIT_STASH_APPLY_DEFAULT, \ + GIT_CHECKOUT_OPTIONS_INIT } + +/** + * Initializes a `git_stash_apply_options` with default values. Equivalent to + * creating an instance with GIT_STASH_APPLY_OPTIONS_INIT. + * + * @param opts the `git_stash_apply_options` instance to initialize. + * @param version the version of the struct; you should pass + * `GIT_STASH_APPLY_OPTIONS_INIT` here. + * @return Zero on success; -1 on failure. + */ +int git_stash_apply_init_options( + git_stash_apply_options *opts, unsigned int version); /** * Apply a single stashed state from the stash list. @@ -89,17 +125,17 @@ typedef enum { * ignored files and there is a conflict when applying the modified files, * then those files will remain in the working directory. * - * If passing the GIT_APPLY_REINSTATE_INDEX flag and there would be conflicts - * when reinstating the index, the function will return GIT_EMERGECONFLICT - * and both the working directory and index will be left unmodified. + * If passing the GIT_STASH_APPLY_REINSTATE_INDEX flag and there would be + * conflicts when reinstating the index, the function will return + * GIT_EMERGECONFLICT and both the working directory and index will be left + * unmodified. + * + * Note that a minimum checkout strategy of `GIT_CHECKOUT_SAFE` is implied. * * @param repo The owning repository. * @param index The position within the stash list. 0 points to the * most recent stashed state. - * @param checkout_options Options to control how files are checked out. - * A minimum strategy of `GIT_CHECKOUT_SAFE` is - * implied. - * @param flags Flags to control the applying process. (see GIT_APPLY_* above) + * @param options Options to control how stashes are applied. * * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the * given index, GIT_EMERGECONFLICT if changes exist in the working @@ -108,8 +144,7 @@ typedef enum { GIT_EXTERN(int) git_stash_apply( git_repository *repo, size_t index, - const git_checkout_options *checkout_options, - unsigned int flags); + const git_stash_apply_options *options); /** * This is a callback function you can provide to iterate over all the @@ -169,8 +204,7 @@ GIT_EXTERN(int) git_stash_drop( * @param repo The owning repository. * @param index The position within the stash list. 0 points to the * most recent stashed state. - * @param checkout_options Options to control how files are checked out - * @param flags Flags to control the applying process. (see GIT_APPLY_* above) + * @param options Options to control how stashes are applied. * * @return 0 on success, GIT_ENOTFOUND if there's no stashed state for the given * index, or error code. (see git_stash_apply() above for details) @@ -178,8 +212,7 @@ GIT_EXTERN(int) git_stash_drop( GIT_EXTERN(int) git_stash_pop( git_repository *repo, size_t index, - const git_checkout_options *checkout_options, - unsigned int flags); + const git_stash_apply_options *options); /** @} */ GIT_END_DECL diff --git a/src/stash.c b/src/stash.c index 3f60ee017..dade06f94 100644 --- a/src/stash.c +++ b/src/stash.c @@ -673,34 +673,40 @@ done: return error; } -static void normalize_checkout_options( - git_checkout_options *checkout_opts, - const git_checkout_options *given_checkout_opts) +static void normalize_apply_options( + git_stash_apply_options *opts, + const git_stash_apply_options *given_apply_opts) { - if (given_checkout_opts != NULL) { - memcpy(checkout_opts, given_checkout_opts, sizeof(git_checkout_options)); + if (given_apply_opts != NULL) { + memcpy(opts, given_apply_opts, sizeof(git_stash_apply_options)); } else { - git_checkout_options default_checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; - memcpy(checkout_opts, &default_checkout_opts, sizeof(git_checkout_options)); + git_stash_apply_options default_apply_opts = GIT_STASH_APPLY_OPTIONS_INIT; + memcpy(opts, &default_apply_opts, sizeof(git_stash_apply_options)); } - if ((checkout_opts->checkout_strategy & (GIT_CHECKOUT_SAFE | GIT_CHECKOUT_FORCE)) == 0) - checkout_opts->checkout_strategy = GIT_CHECKOUT_SAFE; + if ((opts->checkout_options.checkout_strategy & (GIT_CHECKOUT_SAFE | GIT_CHECKOUT_FORCE)) == 0) + opts->checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE; - if (!checkout_opts->our_label) - checkout_opts->our_label = "Updated upstream"; + if (!opts->checkout_options.our_label) + opts->checkout_options.our_label = "Updated upstream"; - if (!checkout_opts->their_label) - checkout_opts->their_label = "Stashed changes"; + if (!opts->checkout_options.their_label) + opts->checkout_options.their_label = "Stashed changes"; +} + +int git_stash_apply_init_options(git_stash_apply_options *opts, unsigned int version) +{ + GIT_INIT_STRUCTURE_FROM_TEMPLATE( + opts, version, git_stash_apply_options, GIT_STASH_APPLY_OPTIONS_INIT); + return 0; } int git_stash_apply( git_repository *repo, size_t index, - const git_checkout_options *given_checkout_opts, - unsigned int flags) + const git_stash_apply_options *given_opts) { - git_checkout_options checkout_opts; + git_stash_apply_options opts; unsigned int checkout_strategy; git_commit *stash_commit = NULL; git_tree *stash_tree = NULL; @@ -714,8 +720,10 @@ int git_stash_apply( git_index *untracked_index = NULL; int error; - normalize_checkout_options(&checkout_opts, given_checkout_opts); - checkout_strategy = checkout_opts.checkout_strategy; + GITERR_CHECK_VERSION(given_opts, GIT_STASH_APPLY_OPTIONS_VERSION, "git_stash_apply_options"); + + normalize_apply_options(&opts, given_opts); + checkout_strategy = opts.checkout_options.checkout_strategy; /* Retrieve commit corresponding to the given stash */ if ((error = retrieve_stash_commit(&stash_commit, repo, index)) < 0) @@ -732,7 +740,7 @@ int git_stash_apply( goto cleanup; /* Restore index if required */ - if ((flags & GIT_APPLY_REINSTATE_INDEX) && + if ((opts.flags & GIT_STASH_APPLY_REINSTATE_INDEX) && git_oid_cmp(git_tree_id(stash_parent_tree), git_tree_id(index_tree))) { if ((error = merge_index_and_tree( @@ -756,12 +764,12 @@ int git_stash_apply( goto cleanup; if (untracked_index) { - checkout_opts.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + opts.checkout_options.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; - if ((error = git_checkout_index(repo, untracked_index, &checkout_opts)) < 0) + if ((error = git_checkout_index(repo, untracked_index, &opts.checkout_options)) < 0) goto cleanup; - checkout_opts.checkout_strategy = checkout_strategy; + opts.checkout_options.checkout_strategy = checkout_strategy; } @@ -771,15 +779,15 @@ int git_stash_apply( */ if (!git_index_has_conflicts(modified_index)) - checkout_opts.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + opts.checkout_options.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; /* Check out the modified index using the existing repo index as baseline, * so that existing modifications in the index can be rewritten even when * checking out safely. */ - checkout_opts.baseline_index = repo_index; + opts.checkout_options.baseline_index = repo_index; - if ((error = git_checkout_index(repo, modified_index, &checkout_opts)) < 0) + if ((error = git_checkout_index(repo, modified_index, &opts.checkout_options)) < 0) goto cleanup; if (unstashed_index && !git_index_has_conflicts(modified_index)) { @@ -903,12 +911,11 @@ cleanup: int git_stash_pop( git_repository *repo, size_t index, - const git_checkout_options *checkout_options, - unsigned int flags) + const git_stash_apply_options *options) { int error; - if ((error = git_stash_apply(repo, index, checkout_options, flags)) < 0) + if ((error = git_stash_apply(repo, index, options)) < 0) return error; return git_stash_drop(repo, index); diff --git a/tests/core/structinit.c b/tests/core/structinit.c index 25254b713..e9f7b4a74 100644 --- a/tests/core/structinit.c +++ b/tests/core/structinit.c @@ -131,6 +131,11 @@ void test_core_structinit__compare(void) git_revert_options, GIT_REVERT_OPTIONS_VERSION, \ GIT_REVERT_OPTIONS_INIT, git_revert_init_options); + /* stash apply */ + CHECK_MACRO_FUNC_INIT_EQUAL( \ + git_stash_apply_options, GIT_STASH_APPLY_OPTIONS_VERSION, \ + GIT_STASH_APPLY_OPTIONS_INIT, git_stash_apply_init_options); + /* status */ CHECK_MACRO_FUNC_INIT_EQUAL( \ git_status_options, GIT_STATUS_OPTIONS_VERSION, \ diff --git a/tests/stash/apply.c b/tests/stash/apply.c index 2f7e6f205..de330e9d3 100644 --- a/tests/stash/apply.c +++ b/tests/stash/apply.c @@ -63,7 +63,7 @@ void test_stash_apply__cleanup(void) void test_stash_apply__with_default(void) { - cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT)); + cl_git_pass(git_stash_apply(repo, 0, NULL)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); @@ -74,7 +74,11 @@ void test_stash_apply__with_default(void) void test_stash_apply__with_reinstate_index(void) { - cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX)); + git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; + + opts.flags = GIT_STASH_APPLY_REINSTATE_INDEX; + + cl_git_pass(git_stash_apply(repo, 0, &opts)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); @@ -93,7 +97,7 @@ void test_stash_apply__conflict_index_with_default(void) cl_git_pass(git_index_add_bypath(repo_index, "who")); cl_git_pass(git_index_write(repo_index)); - cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT)); + cl_git_pass(git_stash_apply(repo, 0, NULL)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 1); assert_status(repo, "what", GIT_STATUS_INDEX_MODIFIED); @@ -104,11 +108,15 @@ void test_stash_apply__conflict_index_with_default(void) void test_stash_apply__conflict_index_with_reinstate_index(void) { + git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; + + opts.flags = GIT_STASH_APPLY_REINSTATE_INDEX; + cl_git_rewritefile("stash/who", "nothing\n"); cl_git_pass(git_index_add_bypath(repo_index, "who")); cl_git_pass(git_index_write(repo_index)); - cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); + cl_git_fail_with(git_stash_apply(repo, 0, &opts), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_CURRENT); @@ -119,9 +127,11 @@ void test_stash_apply__conflict_index_with_reinstate_index(void) void test_stash_apply__conflict_untracked_with_default(void) { + git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; + cl_git_mkfile("stash/when", "nothing\n"); - cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT), GIT_EMERGECONFLICT); + cl_git_fail_with(git_stash_apply(repo, 0, &opts), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_CURRENT); @@ -132,9 +142,13 @@ void test_stash_apply__conflict_untracked_with_default(void) void test_stash_apply__conflict_untracked_with_reinstate_index(void) { + git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; + + opts.flags = GIT_STASH_APPLY_REINSTATE_INDEX; + cl_git_mkfile("stash/when", "nothing\n"); - cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); + cl_git_fail_with(git_stash_apply(repo, 0, &opts), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_CURRENT); @@ -147,7 +161,7 @@ void test_stash_apply__conflict_workdir_with_default(void) { cl_git_rewritefile("stash/what", "ciao\n"); - cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT), GIT_EMERGECONFLICT); + cl_git_fail_with(git_stash_apply(repo, 0, NULL), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); @@ -158,9 +172,13 @@ void test_stash_apply__conflict_workdir_with_default(void) void test_stash_apply__conflict_workdir_with_reinstate_index(void) { + git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; + + opts.flags = GIT_STASH_APPLY_REINSTATE_INDEX; + cl_git_rewritefile("stash/what", "ciao\n"); - cl_git_fail_with(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX), GIT_EMERGECONFLICT); + cl_git_fail_with(git_stash_apply(repo, 0, &opts), GIT_EMERGECONFLICT); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); @@ -179,7 +197,7 @@ void test_stash_apply__conflict_commit_with_default(void) cl_git_pass(git_index_add_bypath(repo_index, "what")); cl_repo_commit_from_index(NULL, repo, signature, 0, "Other commit"); - cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_DEFAULT)); + cl_git_pass(git_stash_apply(repo, 0, NULL)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 1); cl_git_pass(git_index_conflict_get(&ancestor, &our, &their, repo_index, "what")); /* unmerged */ @@ -190,15 +208,18 @@ void test_stash_apply__conflict_commit_with_default(void) void test_stash_apply__conflict_commit_with_reinstate_index(void) { + git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; const git_index_entry *ancestor; const git_index_entry *our; const git_index_entry *their; + opts.flags = GIT_STASH_APPLY_REINSTATE_INDEX; + cl_git_rewritefile("stash/what", "ciao\n"); cl_git_pass(git_index_add_bypath(repo_index, "what")); cl_repo_commit_from_index(NULL, repo, signature, 0, "Other commit"); - cl_git_pass(git_stash_apply(repo, 0, NULL, GIT_APPLY_REINSTATE_INDEX)); + cl_git_pass(git_stash_apply(repo, 0, &opts)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 1); cl_git_pass(git_index_conflict_get(&ancestor, &our, &their, repo_index, "what")); /* unmerged */ @@ -209,9 +230,9 @@ void test_stash_apply__conflict_commit_with_reinstate_index(void) void test_stash_apply__pop(void) { - cl_git_pass(git_stash_pop(repo, 0, NULL, GIT_APPLY_DEFAULT)); + cl_git_pass(git_stash_pop(repo, 0, NULL)); - cl_git_fail_with(git_stash_pop(repo, 0, NULL, GIT_APPLY_DEFAULT), GIT_ENOTFOUND); + cl_git_fail_with(git_stash_pop(repo, 0, NULL), GIT_ENOTFOUND); } struct seen_paths { @@ -245,14 +266,14 @@ int checkout_notify( void test_stash_apply__executes_notify_cb(void) { - git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; struct seen_paths seen_paths = {0}; - checkout_opts.notify_cb = checkout_notify; - checkout_opts.notify_flags = GIT_CHECKOUT_NOTIFY_ALL; - checkout_opts.notify_payload = &seen_paths; + opts.checkout_options.notify_cb = checkout_notify; + opts.checkout_options.notify_flags = GIT_CHECKOUT_NOTIFY_ALL; + opts.checkout_options.notify_payload = &seen_paths; - cl_git_pass(git_stash_apply(repo, 0, &checkout_opts, GIT_APPLY_DEFAULT)); + cl_git_pass(git_stash_apply(repo, 0, &opts)); cl_assert_equal_i(git_index_has_conflicts(repo_index), 0); assert_status(repo, "what", GIT_STATUS_WT_MODIFIED); From 4ea3eebf4b53274b885f749f543101f6633c665e Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Fri, 1 May 2015 18:34:38 -0400 Subject: [PATCH 14/17] stash_apply: provide progress callbacks --- include/git2/stash.h | 38 +++++++++++++++++++++++++++++++++++++ src/stash.c | 26 ++++++++++++++++++++++--- tests/stash/apply.c | 45 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/include/git2/stash.h b/include/git2/stash.h index 1f773dcb0..526db0ba2 100644 --- a/include/git2/stash.h +++ b/include/git2/stash.h @@ -80,6 +80,40 @@ typedef enum { GIT_STASH_APPLY_REINSTATE_INDEX = (1 << 0), } git_stash_apply_flags; +typedef enum { + GIT_STASH_APPLY_PROGRESS_NONE = 0, + + /** Loading the stashed data from the object database. */ + GIT_STASH_APPLY_PROGRESS_LOADING_STASH, + + /** The stored index is being analyzed. */ + GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX, + + /** The modified files are being analyzed. */ + GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED, + + /** The untracked and ignored files are being analyzed. */ + GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED, + + /** The untracked files are being written to disk. */ + GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED, + + /** The modified files are being written to disk. */ + GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED, + + /** The stash was applied successfully. */ + GIT_STASH_APPLY_PROGRESS_DONE, +} git_stash_apply_progress_t; + +/** + * Stash application progress notification function. + * Return 0 to continue processing, or a negative value to + * abort the stash application. + */ +typedef int (*git_stash_apply_progress_cb)( + git_stash_apply_progress_t progress, + void *payload); + /** Stash application options structure. * * Initialize with the `GIT_STASH_APPLY_OPTIONS_INIT` macro to set @@ -95,6 +129,10 @@ typedef struct git_stash_apply_options { /** Options to use when writing files to the working directory. */ git_checkout_options checkout_options; + + /** Optional callback to notify the consumer of application progress. */ + git_stash_apply_progress_cb progress_cb; + void *progress_payload; } git_stash_apply_options; #define GIT_STASH_APPLY_OPTIONS_VERSION 1 diff --git a/src/stash.c b/src/stash.c index dade06f94..71ab7b945 100644 --- a/src/stash.c +++ b/src/stash.c @@ -701,6 +701,11 @@ int git_stash_apply_init_options(git_stash_apply_options *opts, unsigned int ver return 0; } +#define NOTIFY_PROGRESS(opts, progress_type) \ + if ((opts).progress_cb && \ + (error = (opts).progress_cb((progress_type), (opts).progress_payload))) \ + return (error < 0) ? error : -1; + int git_stash_apply( git_repository *repo, size_t index, @@ -725,6 +730,8 @@ int git_stash_apply( normalize_apply_options(&opts, given_opts); checkout_strategy = opts.checkout_options.checkout_strategy; + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_LOADING_STASH); + /* Retrieve commit corresponding to the given stash */ if ((error = retrieve_stash_commit(&stash_commit, repo, index)) < 0) goto cleanup; @@ -739,6 +746,8 @@ int git_stash_apply( if ((error = git_repository_index(&repo_index, repo)) < 0) goto cleanup; + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX); + /* Restore index if required */ if ((opts.flags & GIT_STASH_APPLY_REINSTATE_INDEX) && git_oid_cmp(git_tree_id(stash_parent_tree), git_tree_id(index_tree))) { @@ -753,19 +762,26 @@ int git_stash_apply( } } + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED); + /* Restore modified files in workdir */ if ((error = merge_index_and_tree( &modified_index, repo, stash_parent_tree, repo_index, stash_tree)) < 0) goto cleanup; /* If applicable, restore untracked / ignored files in workdir */ - if (untracked_tree && - (error = merge_index_and_tree(&untracked_index, repo, NULL, repo_index, untracked_tree)) < 0) - goto cleanup; + if (untracked_tree) { + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED); + + if ((error = merge_index_and_tree(&untracked_index, repo, NULL, repo_index, untracked_tree)) < 0) + goto cleanup; + } if (untracked_index) { opts.checkout_options.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX; + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED); + if ((error = git_checkout_index(repo, untracked_index, &opts.checkout_options)) < 0) goto cleanup; @@ -787,6 +803,8 @@ int git_stash_apply( */ opts.checkout_options.baseline_index = repo_index; + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED); + if ((error = git_checkout_index(repo, modified_index, &opts.checkout_options)) < 0) goto cleanup; @@ -795,6 +813,8 @@ int git_stash_apply( goto cleanup; } + NOTIFY_PROGRESS(opts, GIT_STASH_APPLY_PROGRESS_DONE); + cleanup: git_index_free(untracked_index); git_index_free(modified_index); diff --git a/tests/stash/apply.c b/tests/stash/apply.c index de330e9d3..213945e9b 100644 --- a/tests/stash/apply.c +++ b/tests/stash/apply.c @@ -286,3 +286,48 @@ void test_stash_apply__executes_notify_cb(void) cl_assert_equal_b(true, seen_paths.who); cl_assert_equal_b(true, seen_paths.when); } + +int progress_cb( + git_stash_apply_progress_t progress, + void *payload) +{ + git_stash_apply_progress_t *p = (git_stash_apply_progress_t *)payload; + + cl_assert_equal_i((*p)+1, progress); + + *p = progress; + + return 0; +} + +void test_stash_apply__calls_progress_cb(void) +{ + git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; + git_stash_apply_progress_t progress = GIT_STASH_APPLY_PROGRESS_NONE; + + opts.progress_cb = progress_cb; + opts.progress_payload = &progress; + + cl_git_pass(git_stash_apply(repo, 0, &opts)); + cl_assert_equal_i(progress, GIT_STASH_APPLY_PROGRESS_DONE); +} + +int aborting_progress_cb( + git_stash_apply_progress_t progress, + void *payload) +{ + if (progress == GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED) + return -44; + + return 0; +} + +void test_stash_apply__progress_cb_can_abort(void) +{ + git_stash_apply_options opts = GIT_STASH_APPLY_OPTIONS_INIT; + git_stash_apply_progress_t progress = GIT_STASH_APPLY_PROGRESS_NONE; + + opts.progress_cb = aborting_progress_cb; + + cl_git_fail_with(-44, git_stash_apply(repo, 0, &opts)); +} From 4497287321d2d2624316b38c00a032a5e890e88f Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 11 May 2015 14:02:53 -0400 Subject: [PATCH 15/17] stash: propagate the error when writing a tree --- src/stash.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stash.c b/src/stash.c index 71ab7b945..c79068edf 100644 --- a/src/stash.c +++ b/src/stash.c @@ -106,7 +106,7 @@ static int build_tree_from_index(git_tree **out, git_index *index) git_oid i_tree_oid; if ((error = git_index_write_tree(&i_tree_oid, index)) < 0) - return -1; + return error; return git_tree_lookup(out, git_index_owner(index), &i_tree_oid); } From 15fdf0548f1c7a2783e1ac84383536065c1a50ae Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 11 May 2015 14:06:47 -0400 Subject: [PATCH 16/17] stash application: document new API in CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4067e48f2..3657906d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,12 @@ support for HTTPS connections insead of OpenSSL. * `git_index_add_frombuffer()` can now create a blob from memory buffer and add it to the index which is attached to a repository. +* `git_stash_apply()` can now apply a stashed state from the stash list, + placing the data into the working directory and index. + +* `git_stash_pop()` will apply a stashed state (like `git_stash_apply()`) + but will remove the stashed state after a successful application. + ### API removals ### Breaking API changes From 1f1f5c639e7e7df3f0c65dbdbcc6b884041a8647 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Mon, 11 May 2015 14:10:24 -0400 Subject: [PATCH 17/17] checkout: better document the `baseline_index` opt --- CHANGELOG.md | 3 +++ include/git2/checkout.h | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3657906d6..d8bea9e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ v0.22 + 1 * On Mac OS X, we now use SecureTransport to provide the cryptographic support for HTTPS connections insead of OpenSSL. +* Checkout can now accept an index for the baseline computations via the + `baseline_index` member. + ### API additions * The `git_merge_options` gained a `file_flags` member. diff --git a/include/git2/checkout.h b/include/git2/checkout.h index 58f190719..6cf9ed8bd 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -272,7 +272,15 @@ typedef struct git_checkout_options { */ git_strarray paths; - git_tree *baseline; /**< expected content of workdir, defaults to HEAD */ + /** The expected content of the working directory; defaults to HEAD. + * If the working directory does not match this baseline information, + * that will produce a checkout conflict. + */ + git_tree *baseline; + + /** Like `baseline` above, though expressed as an index. This + * option overrides `baseline`. + */ git_index *baseline_index; /**< expected content of workdir, expressed as an index. */ const char *target_directory; /**< alternative checkout path to workdir */