]> git.proxmox.com Git - libgit2.git/commitdiff
Added git_stash_apply() and git_stash_pop() APIs
authorPierre-Olivier Latour <pol@mac.com>
Fri, 14 Nov 2014 03:32:47 +0000 (12:32 +0900)
committerEdward Thomson <ethomson@microsoft.com>
Mon, 11 May 2015 18:11:53 +0000 (14:11 -0400)
AUTHORS
include/git2/stash.h
src/stash.c
tests/stash/apply.c [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
index 2eaec0ff64e523d8faa2c2c16f9b829b9b24ede4..61e2113ec925790c75009e6425781a42796e24e7 100644 (file)
--- 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
index 280c647e8989d253a5b25771965520f99fe14d47..f69ba307a15ba5d5d28f5d2dc8d6a9a2934d55a4 100644 (file)
@@ -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
index 8aa48cafe692db820c1acbf5275905327235b17f..dab32552b73e04b76149bbabf3c8ae637ef71c53 100644 (file)
@@ -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 (file)
index 0000000..5f3cc9a
--- /dev/null
@@ -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);
+}