From 590fb68be087ed8a60323026dc2501c456ede945 Mon Sep 17 00:00:00 2001 From: nulltoken Date: Thu, 4 Oct 2012 13:47:45 +0200 Subject: [PATCH] stash: add git_stash_save() --- include/git2.h | 1 + include/git2/stash.h | 67 ++++ src/stash.c | 577 +++++++++++++++++++++++++++++++ tests-clar/stash/save.c | 370 ++++++++++++++++++++ tests-clar/stash/stash_helpers.c | 66 ++++ tests-clar/stash/stash_helpers.h | 8 + 6 files changed, 1089 insertions(+) create mode 100644 include/git2/stash.h create mode 100644 src/stash.c create mode 100644 tests-clar/stash/save.c create mode 100644 tests-clar/stash/stash_helpers.c create mode 100644 tests-clar/stash/stash_helpers.h diff --git a/include/git2.h b/include/git2.h index d55543986..3bb2fce11 100644 --- a/include/git2.h +++ b/include/git2.h @@ -52,5 +52,6 @@ #include "git2/reset.h" #include "git2/message.h" #include "git2/pack.h" +#include "git2/stash.h" #endif diff --git a/include/git2/stash.h b/include/git2/stash.h new file mode 100644 index 000000000..cadc65673 --- /dev/null +++ b/include/git2/stash.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2009-2012 the libgit2 contributors + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_git_stash_h__ +#define INCLUDE_git_stash_h__ + +#include "common.h" +#include "types.h" + +/** + * @file git2/stash.h + * @brief Git stash management routines + * @ingroup Git + * @{ + */ +GIT_BEGIN_DECL + +enum { + GIT_STASH_DEFAULT = 0, + + /* All changes already added to the index + * are left intact in the working directory + */ + GIT_STASH_KEEP_INDEX = (1 << 0), + + /* All untracked files are also stashed and then + * cleaned up from the working directory + */ + GIT_STASH_INCLUDE_UNTRACKED = (1 << 1), + + /* All ignored files are also stashed and then + * cleaned up from the working directory + */ + GIT_STASH_INCLUDE_IGNORED = (1 << 2), +}; + +/** + * Save the local modifications to a new stash. + * + * @param out Object id of the commit containing the stashed state. + * This commit is also the target of the direct reference refs/stash. + * + * @param repo The owning repository. + * + * @param stasher The identity of the person performing the stashing. + * + * @param message Optional description along with the stashed state. + * + * @param flags Flags to control the stashing process. + * + * @return 0 on success, GIT_ENOTFOUND where there's nothing to stash, + * or error code. + */ + +GIT_EXTERN(int) git_stash_save( + git_oid *out, + git_repository *repo, + git_signature *stasher, + const char *message, + uint32_t flags); + +/** @} */ +GIT_END_DECL +#endif diff --git a/src/stash.c b/src/stash.c new file mode 100644 index 000000000..e63ee99e3 --- /dev/null +++ b/src/stash.c @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2009-2012 the libgit2 contributors + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "common.h" +#include "repository.h" +#include "commit.h" +#include "tree.h" +#include "reflog.h" +#include "git2/diff.h" +#include "git2/stash.h" +#include "git2/status.h" +#include "git2/checkout.h" + +static int create_error(int error, const char *msg) +{ + giterr_set(GITERR_STASH, "Cannot stash changes - %s", msg); + return error; +} + +static int ensure_non_bare_repository(git_repository *repo) +{ + if (!git_repository_is_bare(repo)) + return 0; + + return create_error(GIT_EBAREREPO, + "Stash related operations require a working directory."); +} + +static int retrieve_head(git_reference **out, git_repository *repo) +{ + int error = git_repository_head(out, repo); + + if (error == GIT_EORPHANEDHEAD) + return create_error(error, "You do not have the initial commit yet."); + + return error; +} + +static int append_abbreviated_oid(git_buf *out, const git_oid *b_commit) +{ + char *formatted_oid; + + formatted_oid = git_oid_allocfmt(b_commit); + GITERR_CHECK_ALLOC(formatted_oid); + + git_buf_put(out, formatted_oid, 7); + git__free(formatted_oid); + + return git_buf_oom(out) ? -1 : 0; +} + +static int append_commit_description(git_buf *out, git_commit* commit) +{ + const char *message; + int pos = 0, len; + + 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_putc(out, '\n'); + + return git_buf_oom(out) ? -1 : 0; +} + +static int retrieve_base_commit_and_message( + git_commit **b_commit, + git_buf *stash_message, + git_repository *repo) +{ + git_reference *head = NULL; + int error; + + if ((error = retrieve_head(&head, repo)) < 0) + return error; + + error = -1; + + if (strcmp("HEAD", git_reference_name(head)) == 0) + git_buf_puts(stash_message, "(no branch): "); + else + git_buf_printf( + stash_message, + "%s: ", + git_reference_name(head) + strlen(GIT_REFS_HEADS_DIR)); + + if (git_commit_lookup(b_commit, repo, git_reference_oid(head)) < 0) + goto cleanup; + + if (append_commit_description(stash_message, *b_commit) < 0) + goto cleanup; + + error = 0; + +cleanup: + git_reference_free(head); + return error; +} + +static int build_tree_from_index(git_tree **out, git_index *index) +{ + git_oid i_tree_oid; + + if (git_tree_create_fromindex(&i_tree_oid, index) < 0) + return -1; + + return git_tree_lookup(out, git_index_owner(index), &i_tree_oid); +} + +static int commit_index( + git_commit **i_commit, + git_index *index, + git_signature *stasher, + const char *message, + const git_commit *parent) +{ + git_tree *i_tree = NULL; + git_oid i_commit_oid; + git_buf msg = GIT_BUF_INIT; + int error = -1; + + if (build_tree_from_index(&i_tree, index) < 0) + goto cleanup; + + if (git_buf_printf(&msg, "index on %s\n", message) < 0) + goto cleanup; + + if (git_commit_create( + &i_commit_oid, + git_index_owner(index), + NULL, + stasher, + stasher, + NULL, + git_buf_cstr(&msg), + i_tree, + 1, + &parent) < 0) + goto cleanup; + + error = git_commit_lookup(i_commit, git_index_owner(index), &i_commit_oid); + +cleanup: + git_tree_free(i_tree); + git_buf_free(&msg); + return error; +} + +struct cb_data { + git_index *index; + + bool include_changed; + bool include_untracked; + bool include_ignored; +}; + +static int update_index_cb( + void *cb_data, + const git_diff_delta *delta, + float progress) +{ + int pos; + struct cb_data *data = (struct cb_data *)cb_data; + + GIT_UNUSED(progress); + + switch (delta->status) { + case GIT_DELTA_IGNORED: + if (!data->include_ignored) + break; + + return git_index_add(data->index, delta->new_file.path, 0); + + case GIT_DELTA_UNTRACKED: + if (!data->include_untracked) + break; + + return git_index_add(data->index, delta->new_file.path, 0); + + case GIT_DELTA_ADDED: + /* Fall through */ + case GIT_DELTA_MODIFIED: + if (!data->include_changed) + break; + + return git_index_add(data->index, delta->new_file.path, 0); + + case GIT_DELTA_DELETED: + if (!data->include_changed) + break; + + if ((pos = git_index_find(data->index, delta->new_file.path)) < 0) + return -1; + + if (git_index_remove(data->index, pos) < 0) + return -1; + + default: + /* Unimplemented */ + giterr_set( + GITERR_INVALID, + "Cannot update index. Unimplemented status kind (%d)", + delta->status); + return -1; + } + + return 0; +} + +static int build_untracked_tree( + git_tree **tree_out, + git_index *index, + git_commit *i_commit, + uint32_t flags) +{ + git_tree *i_tree = NULL; + git_diff_list *diff = NULL; + git_diff_options opts = {0}; + struct cb_data data = {0}; + int error = -1; + + git_index_clear(index); + + data.index = index; + + if (flags & GIT_STASH_INCLUDE_UNTRACKED) { + opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED | GIT_DIFF_RECURSE_UNTRACKED_DIRS; + data.include_untracked = true; + } + + if (flags & GIT_STASH_INCLUDE_IGNORED) { + opts.flags |= GIT_DIFF_INCLUDE_IGNORED; + data.include_ignored = true; + } + + if (git_commit_tree(&i_tree, i_commit) < 0) + goto cleanup; + + if (git_diff_workdir_to_tree(git_index_owner(index), &opts, i_tree, &diff) < 0) + goto cleanup; + + if (git_diff_foreach(diff, &data, update_index_cb, NULL, NULL) < 0) + goto cleanup; + + if (build_tree_from_index(tree_out, index) < 0) + goto cleanup; + + error = 0; + +cleanup: + git_diff_list_free(diff); + git_tree_free(i_tree); + return error; +} + +static int commit_untracked( + git_commit **u_commit, + git_index *index, + git_signature *stasher, + const char *message, + git_commit *i_commit, + uint32_t flags) +{ + git_tree *u_tree = NULL; + git_oid u_commit_oid; + git_buf msg = GIT_BUF_INIT; + int error = -1; + + if (build_untracked_tree(&u_tree, index, i_commit, flags) < 0) + goto cleanup; + + if (git_buf_printf(&msg, "untracked files on %s\n", message) < 0) + goto cleanup; + + if (git_commit_create( + &u_commit_oid, + git_index_owner(index), + NULL, + stasher, + stasher, + NULL, + git_buf_cstr(&msg), + u_tree, + 0, + NULL) < 0) + goto cleanup; + + error = git_commit_lookup(u_commit, git_index_owner(index), &u_commit_oid); + +cleanup: + git_tree_free(u_tree); + git_buf_free(&msg); + return error; +} + +static int build_workdir_tree( + git_tree **tree_out, + git_index *index, + git_commit *b_commit) +{ + git_tree *b_tree = NULL; + git_diff_list *diff = NULL, *diff2 = NULL; + git_diff_options opts = {0}; + struct cb_data data = {0}; + int error = -1; + + if (git_commit_tree(&b_tree, b_commit) < 0) + goto cleanup; + + if (git_diff_index_to_tree(git_index_owner(index), &opts, b_tree, &diff) < 0) + goto cleanup; + + if (git_diff_workdir_to_index(git_index_owner(index), &opts, &diff2) < 0) + goto cleanup; + + if (git_diff_merge(diff, diff2) < 0) + goto cleanup; + + data.index = index; + data.include_changed = true; + + if (git_diff_foreach(diff, &data, update_index_cb, NULL, NULL) < 0) + goto cleanup; + + if (build_tree_from_index(tree_out, index) < 0) + goto cleanup; + + error = 0; + +cleanup: + git_diff_list_free(diff); + git_diff_list_free(diff2); + git_tree_free(b_tree); + return error; +} + +static int commit_worktree( + git_oid *w_commit_oid, + git_index *index, + git_signature *stasher, + const char *message, + git_commit *i_commit, + git_commit *b_commit, + git_commit *u_commit) +{ + git_tree *w_tree = NULL, *i_tree = NULL; + int error = -1; + + const git_commit *parents[] = { NULL, NULL, NULL }; + + parents[0] = b_commit; + parents[1] = i_commit; + parents[2] = u_commit; + + if (git_commit_tree(&i_tree, i_commit) < 0) + return -1; + + if (git_index_read_tree(index, i_tree) < 0) + goto cleanup; + + if (build_workdir_tree(&w_tree, index, b_commit) < 0) + goto cleanup; + + if (git_commit_create( + w_commit_oid, + git_index_owner(index), + NULL, + stasher, + stasher, + NULL, + message, + w_tree, + u_commit ? 3 : 2, parents) < 0) + goto cleanup; + + error = 0; + +cleanup: + git_tree_free(i_tree); + git_tree_free(w_tree); + return error; +} + +static int prepare_worktree_commit_message( + git_buf* msg, + const char *user_message) +{ + git_buf buf = GIT_BUF_INIT; + int error = -1; + + git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg)); + git_buf_clear(msg); + + if (!user_message) + git_buf_printf(msg, "WIP on %s", git_buf_cstr(&buf)); + else { + const char *colon; + + if ((colon = strchr(git_buf_cstr(&buf), ':')) == NULL) + goto cleanup; + + git_buf_puts(msg, "On "); + git_buf_put(msg, git_buf_cstr(&buf), colon - buf.ptr); + git_buf_printf(msg, ": %s\n", user_message); + } + + error = git_buf_oom(msg) || git_buf_oom(&buf) ? -1 : 0; + +cleanup: + git_buf_free(&buf); + return error; +} + +static int update_reflog( + git_oid *w_commit_oid, + git_repository *repo, + git_signature *stasher, + const char *message) +{ + git_reference *stash = NULL; + git_reflog *reflog = NULL; + int error; + + if ((error = git_reference_create_oid(&stash, repo, GIT_REFS_STASH_FILE, w_commit_oid, 1)) < 0) + goto cleanup; + + if ((error = git_reflog_read(&reflog, stash)) < 0) + goto cleanup; + + if ((error = git_reflog_append(reflog, w_commit_oid, stasher, message)) < 0) + goto cleanup; + + if ((error = git_reflog_write(reflog)) < 0) + goto cleanup; + + error = 0; + +cleanup: + git_reference_free(stash); + git_reflog_free(reflog); + return error; +} + +static int is_dirty_cb(const char *path, unsigned int status, void *payload) +{ + GIT_UNUSED(path); + GIT_UNUSED(status); + GIT_UNUSED(payload); + + return 1; +} + +static int ensure_there_are_changes_to_stash( + git_repository *repo, + bool include_untracked_files, + bool include_ignored_files) +{ + int error; + git_status_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + if (include_untracked_files) + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | + GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; + + if (include_ignored_files) + opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED; + + error = git_status_foreach_ext(repo, &opts, is_dirty_cb, NULL); + + if (error == GIT_EUSER) + return 0; + + if (!error) + return create_error(GIT_ENOTFOUND, "There is nothing to stash."); + + return error; +} + +static int reset_index_and_workdir( + git_repository *repo, + git_commit *commit, + bool remove_untracked) +{ + git_checkout_opts opts; + + memset(&opts, 0, sizeof(git_checkout_opts)); + + opts.checkout_strategy = + GIT_CHECKOUT_CREATE_MISSING | GIT_CHECKOUT_OVERWRITE_MODIFIED; + + if (remove_untracked) + opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED; + + return git_checkout_tree(repo, (git_object *)commit, &opts); +} + +int git_stash_save( + git_oid *out, + git_repository *repo, + git_signature *stasher, + const char *message, + uint32_t flags) +{ + git_index *index = NULL; + git_commit *b_commit = NULL, *i_commit = NULL, *u_commit = NULL; + git_buf msg = GIT_BUF_INIT; + int error; + + assert(out && repo && stasher); + + if ((error = ensure_non_bare_repository(repo)) < 0) + return error; + + if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0) + goto cleanup; + + if ((error = ensure_there_are_changes_to_stash( + repo, + (flags & GIT_STASH_INCLUDE_UNTRACKED) == GIT_STASH_INCLUDE_UNTRACKED, + (flags & GIT_STASH_INCLUDE_IGNORED) == GIT_STASH_INCLUDE_IGNORED)) < 0) + goto cleanup; + + error = -1; + + if (git_repository_index(&index, repo) < 0) + goto cleanup; + + if (commit_index(&i_commit, index, stasher, git_buf_cstr(&msg), b_commit) < 0) + goto cleanup; + + if ((flags & GIT_STASH_INCLUDE_UNTRACKED || flags & GIT_STASH_INCLUDE_IGNORED) + && commit_untracked(&u_commit, index, stasher, git_buf_cstr(&msg), i_commit, flags) < 0) + goto cleanup; + + if (prepare_worktree_commit_message(&msg, message) < 0) + goto cleanup; + + if (commit_worktree(out, index, stasher, git_buf_cstr(&msg), i_commit, b_commit, u_commit) < 0) + goto cleanup; + + git_buf_rtrim(&msg); + if (update_reflog(out, repo, stasher, git_buf_cstr(&msg)) < 0) + goto cleanup; + + if (reset_index_and_workdir( + repo, + ((flags & GIT_STASH_KEEP_INDEX) == GIT_STASH_KEEP_INDEX) ? + i_commit : b_commit, + (flags & GIT_STASH_INCLUDE_UNTRACKED) == GIT_STASH_INCLUDE_UNTRACKED) < 0) + goto cleanup; + + error = 0; + +cleanup: + git_buf_free(&msg); + git_commit_free(i_commit); + git_commit_free(b_commit); + git_commit_free(u_commit); + git_index_free(index); + return error; +} diff --git a/tests-clar/stash/save.c b/tests-clar/stash/save.c new file mode 100644 index 000000000..71b0b6486 --- /dev/null +++ b/tests-clar/stash/save.c @@ -0,0 +1,370 @@ +#include "clar_libgit2.h" +#include "fileops.h" +#include "stash_helpers.h" + +static git_repository *repo; +static git_signature *signature; +static git_oid stash_tip_oid; + +/* + * Friendly reminder, in order to ease the reading of the following tests: + * + * "stash" points to the worktree commit + * "stash^1" points to the base commit (HEAD when the stash was created) + * "stash^2" points to the index commit + * "stash^3" points to the untracked commit + */ + +void test_stash_save__initialize(void) +{ + cl_git_pass(git_repository_init(&repo, "stash", 0)); + cl_git_pass(git_signature_new(&signature, "nulltoken", "emeric.fermas@gmail.com", 1323847743, 60)); /* Wed Dec 14 08:29:03 2011 +0100 */ + + setup_stash(repo, signature); +} + +void test_stash_save__cleanup(void) +{ + git_signature_free(signature); + git_repository_free(repo); + cl_git_pass(git_futils_rmdir_r("stash", NULL, GIT_DIRREMOVAL_FILES_AND_DIRS)); +} + +static void assert_object_oid(const char* revision, const char* expected_oid, git_otype type) +{ + git_object *object; + int result; + + result = git_revparse_single(&object, repo, revision); + + if (!expected_oid) { + cl_assert_equal_i(GIT_ENOTFOUND, result); + return; + } else + cl_assert_equal_i(0, result); + + cl_assert_equal_i(type, git_object_type(object)); + cl_git_pass(git_oid_streq(git_object_id(object), expected_oid)); + + git_object_free(object); +} + +static void assert_blob_oid(const char* revision, const char* expected_oid) +{ + assert_object_oid(revision, expected_oid, GIT_OBJ_BLOB); +} + +void test_stash_save__does_not_keep_index_by_default(void) +{ +/* +$ git stash + +$ git show refs/stash:what +see you later + +$ git show refs/stash:how +not so small and + +$ git show refs/stash:who +funky world + +$ git show refs/stash:when +fatal: Path 'when' exists on disk, but not in 'stash'. + +$ git show refs/stash^2:what +goodbye + +$ git show refs/stash^2:how +not so small and + +$ git show refs/stash^2:who +world + +$ git show refs/stash^2:when +fatal: Path 'when' exists on disk, but not in 'stash^2'. + +$ git status --short +?? when + +*/ + unsigned int status; + + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_DEFAULT)); + cl_git_pass(git_status_file(&status, repo, "when")); + + assert_blob_oid("refs/stash:what", "bc99dc98b3eba0e9157e94769cd4d49cb49de449"); /* see you later */ + assert_blob_oid("refs/stash:how", "e6d64adb2c7f3eb8feb493b556cc8070dca379a3"); /* not so small and */ + assert_blob_oid("refs/stash:who", "a0400d4954659306a976567af43125a0b1aa8595"); /* funky world */ + assert_blob_oid("refs/stash:when", NULL); + assert_blob_oid("refs/stash:just.ignore", NULL); + + assert_blob_oid("refs/stash^2:what", "dd7e1c6f0fefe118f0b63d9f10908c460aa317a6"); /* goodbye */ + assert_blob_oid("refs/stash^2:how", "e6d64adb2c7f3eb8feb493b556cc8070dca379a3"); /* not so small and */ + assert_blob_oid("refs/stash^2:who", "cc628ccd10742baea8241c5924df992b5c019f71"); /* world */ + assert_blob_oid("refs/stash^2:when", NULL); + assert_blob_oid("refs/stash^2:just.ignore", NULL); + + assert_blob_oid("refs/stash^3", NULL); + + cl_assert_equal_i(GIT_STATUS_WT_NEW, status); +} + +static void assert_status( + const char *path, + int status_flags) +{ + unsigned int status; + int error; + + error = git_status_file(&status, repo, path); + + if (status_flags < 0) { + cl_assert_equal_i(status_flags, error); + return; + } + + cl_assert_equal_i(0, error); + cl_assert_equal_i((unsigned int)status_flags, status); +} + +void test_stash_save__can_keep_index(void) +{ + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_KEEP_INDEX)); + + assert_status("what", GIT_STATUS_INDEX_MODIFIED); + assert_status("how", GIT_STATUS_INDEX_MODIFIED); + assert_status("who", GIT_STATUS_CURRENT); + assert_status("when", GIT_STATUS_WT_NEW); + assert_status("just.ignore", GIT_STATUS_IGNORED); +} + +static void assert_commit_message_contains(const char *revision, const char *fragment) +{ + git_commit *commit; + + cl_git_pass(git_revparse_single(((git_object **)&commit), repo, revision)); + + cl_assert(strstr(git_commit_message(commit), fragment) != NULL); + + git_commit_free(commit); +} + +void test_stash_save__can_include_untracked_files(void) +{ + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_INCLUDE_UNTRACKED)); + + assert_commit_message_contains("refs/stash^3", "untracked files on master: "); + + assert_blob_oid("refs/stash^3:what", NULL); + assert_blob_oid("refs/stash^3:how", NULL); + assert_blob_oid("refs/stash^3:who", NULL); + assert_blob_oid("refs/stash^3:when", "b6ed15e81e2593d7bb6265eb4a991d29dc3e628b"); + assert_blob_oid("refs/stash^3:just.ignore", NULL); +} + +void test_stash_save__can_include_untracked_and_ignored_files(void) +{ + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)); + + assert_commit_message_contains("refs/stash^3", "untracked files on master: "); + + assert_blob_oid("refs/stash^3:what", NULL); + assert_blob_oid("refs/stash^3:how", NULL); + assert_blob_oid("refs/stash^3:who", NULL); + assert_blob_oid("refs/stash^3:when", "b6ed15e81e2593d7bb6265eb4a991d29dc3e628b"); + assert_blob_oid("refs/stash^3:just.ignore", "78925fb1236b98b37a35e9723033e627f97aa88b"); +} + +#define MESSAGE "Look Ma! I'm on TV!" +void test_stash_save__can_accept_a_message(void) +{ + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, MESSAGE, GIT_STASH_DEFAULT)); + + assert_commit_message_contains("refs/stash^2", "index on master: "); + assert_commit_message_contains("refs/stash", "On master: " MESSAGE); +} + +void test_stash_save__cannot_stash_against_an_unborn_branch(void) +{ + git_reference *head; + + cl_git_pass(git_reference_lookup(&head, repo, "HEAD")); + cl_git_pass(git_reference_set_target(head, "refs/heads/unborn")); + + cl_assert_equal_i(GIT_EORPHANEDHEAD, + git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_DEFAULT)); + + git_reference_free(head); +} + +void test_stash_save__cannot_stash_against_a_bare_repository(void) +{ + git_repository *local; + + cl_git_pass(git_repository_init(&local, "sorry-it-is-a-non-bare-only-party", 1)); + + cl_assert_equal_i(GIT_EBAREREPO, + git_stash_save(&stash_tip_oid, local, signature, NULL, GIT_STASH_DEFAULT)); + + git_repository_free(local); +} + +void test_stash_save__can_stash_against_a_detached_head(void) +{ + git_repository_detach_head(repo); + + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_DEFAULT)); + + assert_commit_message_contains("refs/stash^2", "index on (no branch): "); + assert_commit_message_contains("refs/stash", "WIP on (no branch): "); +} + +void test_stash_save__stashing_updates_the_reflog(void) +{ + char *sha; + + assert_object_oid("refs/stash@{0}", NULL, GIT_OBJ_COMMIT); + + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_DEFAULT)); + + sha = git_oid_allocfmt(&stash_tip_oid); + + assert_object_oid("refs/stash@{0}", sha, GIT_OBJ_COMMIT); + assert_object_oid("refs/stash@{1}", NULL, GIT_OBJ_COMMIT); + + git__free(sha); +} + +void test_stash_save__cannot_stash_when_there_are_no_local_change(void) +{ + git_index *index; + git_oid commit_oid, stash_tip_oid; + + cl_git_pass(git_repository_index(&index, repo)); + + /* + * 'what' and 'who' are being committed. + * 'when' remain untracked. + */ + git_index_add(index, "what", 0); + git_index_add(index, "who", 0); + cl_git_pass(git_index_write(index)); + commit_staged_files(&commit_oid, index, signature); + git_index_free(index); + + cl_assert_equal_i(GIT_ENOTFOUND, + git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_DEFAULT)); + + p_unlink("stash/when"); + cl_assert_equal_i(GIT_ENOTFOUND, + git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_INCLUDE_UNTRACKED)); +} + +void test_stash_save__can_stage_normal_then_stage_untracked(void) +{ + /* + * $ git ls-tree stash@{1}^0 + * 100644 blob ac4d88de61733173d9959e4b77c69b9f17a00980 .gitignore + * 100644 blob e6d64adb2c7f3eb8feb493b556cc8070dca379a3 how + * 100644 blob bc99dc98b3eba0e9157e94769cd4d49cb49de449 what + * 100644 blob a0400d4954659306a976567af43125a0b1aa8595 who + * + * $ git ls-tree stash@{1}^1 + * 100644 blob ac4d88de61733173d9959e4b77c69b9f17a00980 .gitignore + * 100644 blob ac790413e2d7a26c3767e78c57bb28716686eebc how + * 100644 blob ce013625030ba8dba906f756967f9e9ca394464a what + * 100644 blob cc628ccd10742baea8241c5924df992b5c019f71 who + * + * $ git ls-tree stash@{1}^2 + * 100644 blob ac4d88de61733173d9959e4b77c69b9f17a00980 .gitignore + * 100644 blob e6d64adb2c7f3eb8feb493b556cc8070dca379a3 how + * 100644 blob dd7e1c6f0fefe118f0b63d9f10908c460aa317a6 what + * 100644 blob cc628ccd10742baea8241c5924df992b5c019f71 who + * + * $ git ls-tree stash@{1}^3 + * fatal: Not a valid object name stash@{1}^3 + * + * $ git ls-tree stash@{0}^0 + * 100644 blob ac4d88de61733173d9959e4b77c69b9f17a00980 .gitignore + * 100644 blob ac790413e2d7a26c3767e78c57bb28716686eebc how + * 100644 blob ce013625030ba8dba906f756967f9e9ca394464a what + * 100644 blob cc628ccd10742baea8241c5924df992b5c019f71 who + * + * $ git ls-tree stash@{0}^1 + * 100644 blob ac4d88de61733173d9959e4b77c69b9f17a00980 .gitignore + * 100644 blob ac790413e2d7a26c3767e78c57bb28716686eebc how + * 100644 blob ce013625030ba8dba906f756967f9e9ca394464a what + * 100644 blob cc628ccd10742baea8241c5924df992b5c019f71 who + * + * $ git ls-tree stash@{0}^2 + * 100644 blob ac4d88de61733173d9959e4b77c69b9f17a00980 .gitignore + * 100644 blob ac790413e2d7a26c3767e78c57bb28716686eebc how + * 100644 blob ce013625030ba8dba906f756967f9e9ca394464a what + * 100644 blob cc628ccd10742baea8241c5924df992b5c019f71 who + * + * $ git ls-tree stash@{0}^3 + * 100644 blob b6ed15e81e2593d7bb6265eb4a991d29dc3e628b when + */ + + assert_status("what", GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_MODIFIED); + assert_status("how", GIT_STATUS_INDEX_MODIFIED); + assert_status("who", GIT_STATUS_WT_MODIFIED); + assert_status("when", GIT_STATUS_WT_NEW); + assert_status("just.ignore", GIT_STATUS_IGNORED); + + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_DEFAULT)); + assert_status("what", GIT_STATUS_CURRENT); + assert_status("how", GIT_STATUS_CURRENT); + assert_status("who", GIT_STATUS_CURRENT); + assert_status("when", GIT_STATUS_WT_NEW); + assert_status("just.ignore", GIT_STATUS_IGNORED); + + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_INCLUDE_UNTRACKED)); + assert_status("what", GIT_STATUS_CURRENT); + assert_status("how", GIT_STATUS_CURRENT); + assert_status("who", GIT_STATUS_CURRENT); + assert_status("when", GIT_ENOTFOUND); + assert_status("just.ignore", GIT_STATUS_IGNORED); + + + assert_blob_oid("stash@{1}^0:what", "bc99dc98b3eba0e9157e94769cd4d49cb49de449"); /* see you later */ + assert_blob_oid("stash@{1}^0:how", "e6d64adb2c7f3eb8feb493b556cc8070dca379a3"); /* not so small and */ + assert_blob_oid("stash@{1}^0:who", "a0400d4954659306a976567af43125a0b1aa8595"); /* funky world */ + assert_blob_oid("stash@{1}^0:when", NULL); + + assert_blob_oid("stash@{1}^2:what", "dd7e1c6f0fefe118f0b63d9f10908c460aa317a6"); /* goodbye */ + assert_blob_oid("stash@{1}^2:how", "e6d64adb2c7f3eb8feb493b556cc8070dca379a3"); /* not so small and */ + assert_blob_oid("stash@{1}^2:who", "cc628ccd10742baea8241c5924df992b5c019f71"); /* world */ + assert_blob_oid("stash@{1}^2:when", NULL); + + assert_object_oid("stash@{1}^3", NULL, GIT_OBJ_COMMIT); + + assert_blob_oid("stash@{0}^0:what", "ce013625030ba8dba906f756967f9e9ca394464a"); /* hello */ + assert_blob_oid("stash@{0}^0:how", "ac790413e2d7a26c3767e78c57bb28716686eebc"); /* small */ + assert_blob_oid("stash@{0}^0:who", "cc628ccd10742baea8241c5924df992b5c019f71"); /* world */ + assert_blob_oid("stash@{0}^0:when", NULL); + + assert_blob_oid("stash@{0}^2:what", "ce013625030ba8dba906f756967f9e9ca394464a"); /* hello */ + assert_blob_oid("stash@{0}^2:how", "ac790413e2d7a26c3767e78c57bb28716686eebc"); /* small */ + assert_blob_oid("stash@{0}^2:who", "cc628ccd10742baea8241c5924df992b5c019f71"); /* world */ + assert_blob_oid("stash@{0}^2:when", NULL); + + assert_blob_oid("stash@{0}^3:when", "b6ed15e81e2593d7bb6265eb4a991d29dc3e628b"); /* now */ +} + +#define EMPTY_TREE "4b825dc642cb6eb9a060e54bf8d69288fbee4904" + +void test_stash_save__including_untracked_without_any_untracked_file_creates_an_empty_tree(void) +{ + p_unlink("stash/when"); + + assert_status("what", GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_MODIFIED); + assert_status("how", GIT_STATUS_INDEX_MODIFIED); + assert_status("who", GIT_STATUS_WT_MODIFIED); + assert_status("when", GIT_ENOTFOUND); + assert_status("just.ignore", GIT_STATUS_IGNORED); + + cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_INCLUDE_UNTRACKED)); + + assert_object_oid("stash^3^{tree}", EMPTY_TREE, GIT_OBJ_TREE); +} diff --git a/tests-clar/stash/stash_helpers.c b/tests-clar/stash/stash_helpers.c new file mode 100644 index 000000000..f646ef28b --- /dev/null +++ b/tests-clar/stash/stash_helpers.c @@ -0,0 +1,66 @@ +#include "clar_libgit2.h" +#include "fileops.h" + +void commit_staged_files( + git_oid *commit_oid, + git_index *index, + git_signature *signature) +{ + git_tree *tree; + git_oid tree_oid; + git_repository *repo; + + repo = git_index_owner(index); + + cl_git_pass(git_tree_create_fromindex(&tree_oid, index)); + + cl_git_pass(git_tree_lookup(&tree, repo, &tree_oid)); + cl_git_pass(git_commit_create_v( + commit_oid, + repo, + "HEAD", + signature, + signature, + NULL, + "Initial commit", + tree, + 0)); + + git_tree_free(tree); +} + +void setup_stash(git_repository *repo, git_signature *signature) +{ + git_oid commit_oid; + git_index *index; + + cl_git_pass(git_repository_index(&index, repo)); + + cl_git_mkfile("stash/what", "hello\n"); /* ce013625030ba8dba906f756967f9e9ca394464a */ + cl_git_mkfile("stash/how", "small\n"); /* ac790413e2d7a26c3767e78c57bb28716686eebc */ + cl_git_mkfile("stash/who", "world\n"); /* cc628ccd10742baea8241c5924df992b5c019f71 */ + cl_git_mkfile("stash/when", "now\n"); /* b6ed15e81e2593d7bb6265eb4a991d29dc3e628b */ + cl_git_mkfile("stash/just.ignore", "me\n"); /* 78925fb1236b98b37a35e9723033e627f97aa88b */ + + cl_git_mkfile("stash/.gitignore", "*.ignore\n"); + + cl_git_pass(git_index_add(index, "what", 0)); + cl_git_pass(git_index_add(index, "how", 0)); + cl_git_pass(git_index_add(index, "who", 0)); + cl_git_pass(git_index_add(index, ".gitignore", 0)); + cl_git_pass(git_index_write(index)); + + commit_staged_files(&commit_oid, index, signature); + + cl_git_rewritefile("stash/what", "goodbye\n"); /* dd7e1c6f0fefe118f0b63d9f10908c460aa317a6 */ + cl_git_rewritefile("stash/how", "not so small and\n"); /* e6d64adb2c7f3eb8feb493b556cc8070dca379a3 */ + cl_git_rewritefile("stash/who", "funky world\n"); /* a0400d4954659306a976567af43125a0b1aa8595 */ + + cl_git_pass(git_index_add(index, "what", 0)); + cl_git_pass(git_index_add(index, "how", 0)); + cl_git_pass(git_index_write(index)); + + cl_git_rewritefile("stash/what", "see you later\n"); /* bc99dc98b3eba0e9157e94769cd4d49cb49de449 */ + + git_index_free(index); +} diff --git a/tests-clar/stash/stash_helpers.h b/tests-clar/stash/stash_helpers.h new file mode 100644 index 000000000..bb7fec4f5 --- /dev/null +++ b/tests-clar/stash/stash_helpers.h @@ -0,0 +1,8 @@ +void setup_stash( + git_repository *repo, + git_signature *signature); + +void commit_staged_files( + git_oid *commit_oid, + git_index *index, + git_signature *signature); \ No newline at end of file