summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@edwardthomson.com>2023-02-16 10:23:28 +0000
committerGitHub <noreply@github.com>2023-02-16 10:23:28 +0000
commit05ba3fe4e15ce91e6470ae57d1f4a71fc6147eba (patch)
treec12ebe6dc8cc8f17c8df1194aa7d61cc1d1a936d
parent7108b4318f365968ad542d3a11b6b1a67c41306d (diff)
parent35580d88a834438275adea77785a5f356352ea21 (diff)
downloadlibgit2-05ba3fe4e15ce91e6470ae57d1f4a71fc6147eba.tar.gz
Merge pull request #6330 from gitkraken-jacobw/partial-stashing
stash: partial stash specific files
-rw-r--r--include/git2/stash.h66
-rw-r--r--src/libgit2/stash.c226
-rw-r--r--tests/libgit2/core/structinit.c5
-rw-r--r--tests/libgit2/stash/save.c37
4 files changed, 302 insertions, 32 deletions
diff --git a/include/git2/stash.h b/include/git2/stash.h
index 32e6f9576..dcfc013dc 100644
--- a/include/git2/stash.h
+++ b/include/git2/stash.h
@@ -44,7 +44,12 @@ typedef enum {
* All ignored files are also stashed and then cleaned up from
* the working directory
*/
- GIT_STASH_INCLUDE_IGNORED = (1 << 2)
+ GIT_STASH_INCLUDE_IGNORED = (1 << 2),
+
+ /**
+ * All changes in the index and working directory are left intact
+ */
+ GIT_STASH_KEEP_ALL = (1 << 3)
} git_stash_flags;
/**
@@ -52,15 +57,10 @@ typedef enum {
*
* @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. (see GIT_STASH_* above)
- *
* @return 0 on success, GIT_ENOTFOUND where there's nothing to stash,
* or error code.
*/
@@ -71,6 +71,60 @@ GIT_EXTERN(int) git_stash_save(
const char *message,
uint32_t flags);
+/**
+ * Stash save options structure
+ *
+ * Initialize with `GIT_STASH_SAVE_OPTIONS_INIT`. Alternatively, you can
+ * use `git_stash_save_options_init`.
+ *
+ */
+typedef struct git_stash_save_options {
+ unsigned int version;
+
+ /** Flags to control the stashing process. (see GIT_STASH_* above) */
+ uint32_t flags;
+
+ /** The identity of the person performing the stashing. */
+ const git_signature *stasher;
+
+ /** Optional description along with the stashed state. */
+ const char *message;
+
+ /** Optional paths that control which files are stashed. */
+ git_strarray paths;
+} git_stash_save_options;
+
+#define GIT_STASH_SAVE_OPTIONS_VERSION 1
+#define GIT_STASH_SAVE_OPTIONS_INIT { GIT_STASH_SAVE_OPTIONS_VERSION }
+
+/**
+ * Initialize git_stash_save_options structure
+ *
+ * Initializes a `git_stash_save_options` with default values. Equivalent to
+ * creating an instance with `GIT_STASH_SAVE_OPTIONS_INIT`.
+ *
+ * @param opts The `git_stash_save_options` struct to initialize.
+ * @param version The struct version; pass `GIT_STASH_SAVE_OPTIONS_VERSION`.
+ * @return Zero on success; -1 on failure.
+ */
+GIT_EXTERN(int) git_stash_save_options_init(
+ git_stash_save_options *opts, unsigned int version);
+
+/**
+ * Save the local modifications to a new stash, with options.
+ *
+ * @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 opts The stash options.
+ * @return 0 on success, GIT_ENOTFOUND where there's nothing to stash,
+ * or error code.
+ */
+GIT_EXTERN(int) git_stash_save_with_opts(
+ git_oid *out,
+ git_repository *repo,
+ const git_stash_save_options *opts);
+
/** Stash application flags. */
typedef enum {
GIT_STASH_APPLY_DEFAULT = 0,
diff --git a/src/libgit2/stash.c b/src/libgit2/stash.c
index 5fc01ac36..80eaddecf 100644
--- a/src/libgit2/stash.c
+++ b/src/libgit2/stash.c
@@ -193,6 +193,30 @@ static int stash_to_index(
return git_index_add(index, &entry);
}
+static int stash_update_index_from_paths(
+ git_repository *repo,
+ git_index *index,
+ const git_strarray *paths)
+{
+ unsigned int status_flags;
+ size_t i;
+ int error = 0;
+
+ for (i = 0; i < paths->count; i++) {
+ git_status_file(&status_flags, repo, paths->strings[i]);
+
+ if (status_flags & (GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_DELETED)) {
+ if ((error = git_index_remove(index, paths->strings[i], 0)) < 0)
+ return error;
+ } else {
+ if ((error = stash_to_index(repo, index, paths->strings[i])) < 0)
+ return error;
+ }
+ }
+
+ return error;
+}
+
static int stash_update_index_from_diff(
git_repository *repo,
git_index *index,
@@ -388,24 +412,79 @@ cleanup:
return error;
}
-static int commit_worktree(
+static int build_stash_commit_from_tree(
git_oid *w_commit_oid,
git_repository *repo,
const git_signature *stasher,
const char *message,
git_commit *i_commit,
git_commit *b_commit,
- git_commit *u_commit)
+ git_commit *u_commit,
+ const git_tree *tree)
{
const git_commit *parents[] = { NULL, NULL, NULL };
- git_index *i_index = NULL, *r_index = NULL;
- git_tree *w_tree = NULL;
- int error = 0, ignorecase;
parents[0] = b_commit;
parents[1] = i_commit;
parents[2] = u_commit;
+ return git_commit_create(
+ w_commit_oid,
+ repo,
+ NULL,
+ stasher,
+ stasher,
+ NULL,
+ message,
+ tree,
+ u_commit ? 3 : 2,
+ parents);
+}
+
+static int build_stash_commit_from_index(
+ git_oid *w_commit_oid,
+ git_repository *repo,
+ const git_signature *stasher,
+ const char *message,
+ git_commit *i_commit,
+ git_commit *b_commit,
+ git_commit *u_commit,
+ git_index *index)
+{
+ git_tree *tree;
+ int error;
+
+ if ((error = build_tree_from_index(&tree, repo, index)) < 0)
+ goto cleanup;
+
+ error = build_stash_commit_from_tree(
+ w_commit_oid,
+ repo,
+ stasher,
+ message,
+ i_commit,
+ b_commit,
+ u_commit,
+ tree);
+
+cleanup:
+ git_tree_free(tree);
+ return error;
+}
+
+static int commit_worktree(
+ git_oid *w_commit_oid,
+ git_repository *repo,
+ const git_signature *stasher,
+ const char *message,
+ git_commit *i_commit,
+ git_commit *b_commit,
+ git_commit *u_commit)
+{
+ git_index *i_index = NULL, *r_index = NULL;
+ git_tree *w_tree = NULL;
+ int error = 0, ignorecase;
+
if ((error = git_repository_index(&r_index, repo) < 0) ||
(error = git_index_new(&i_index)) < 0 ||
(error = git_index__fill(i_index, &r_index->entries) < 0) ||
@@ -417,17 +496,16 @@ static int commit_worktree(
if ((error = build_workdir_tree(&w_tree, repo, i_index, b_commit)) < 0)
goto cleanup;
- error = git_commit_create(
+ error = build_stash_commit_from_tree(
w_commit_oid,
repo,
- NULL,
stasher,
- stasher,
- NULL,
message,
- w_tree,
- u_commit ? 3 : 2,
- parents);
+ i_commit,
+ b_commit,
+ u_commit,
+ w_tree
+ );
cleanup:
git_tree_free(w_tree);
@@ -520,6 +598,54 @@ static int ensure_there_are_changes_to_stash(git_repository *repo, uint32_t flag
return error;
}
+static int has_changes_cb(
+ const char *path,
+ unsigned int status,
+ void *payload)
+{
+ GIT_UNUSED(path);
+ GIT_UNUSED(status);
+ GIT_UNUSED(payload);
+
+ if (status == GIT_STATUS_CURRENT)
+ return GIT_ENOTFOUND;
+
+ return 0;
+}
+
+static int ensure_there_are_changes_to_stash_paths(
+ git_repository *repo,
+ uint32_t flags,
+ const git_strarray *paths)
+{
+ int error;
+ git_status_options opts = GIT_STATUS_OPTIONS_INIT;
+
+ opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
+ opts.flags = GIT_STATUS_OPT_EXCLUDE_SUBMODULES |
+ GIT_STATUS_OPT_INCLUDE_UNMODIFIED |
+ GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
+
+ if (flags & GIT_STASH_INCLUDE_UNTRACKED)
+ opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED |
+ GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
+
+ if (flags & GIT_STASH_INCLUDE_IGNORED)
+ opts.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED |
+ GIT_STATUS_OPT_RECURSE_IGNORED_DIRS;
+
+ git_strarray_copy(&opts.pathspec, paths);
+
+ error = git_status_foreach_ext(repo, &opts, has_changes_cb, NULL);
+
+ git_strarray_dispose(&opts.pathspec);
+
+ if (error == GIT_ENOTFOUND)
+ return create_error(GIT_ENOTFOUND, "one of the files does not have any changes to stash.");
+
+ return error;
+}
+
static int reset_index_and_workdir(git_repository *repo, git_commit *commit, uint32_t flags)
{
git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
@@ -540,14 +666,36 @@ int git_stash_save(
const char *message,
uint32_t flags)
{
- git_index *index = NULL;
+ git_stash_save_options opts = GIT_STASH_SAVE_OPTIONS_INIT;
+
+ GIT_ASSERT_ARG(stasher);
+
+ opts.stasher = stasher;
+ opts.message = message;
+ opts.flags = flags;
+
+ return git_stash_save_with_opts(out, repo, &opts);
+}
+
+int git_stash_save_with_opts(
+ git_oid *out,
+ git_repository *repo,
+ const git_stash_save_options *opts)
+{
+ git_index *index = NULL, *paths_index = NULL;
git_commit *b_commit = NULL, *i_commit = NULL, *u_commit = NULL;
git_str msg = GIT_STR_INIT;
+ git_tree *tree = NULL;
+ git_reference *head = NULL;
+ bool has_paths = false;
+
int error;
GIT_ASSERT_ARG(out);
GIT_ASSERT_ARG(repo);
- GIT_ASSERT_ARG(stasher);
+ GIT_ASSERT_ARG(opts && opts->stasher);
+
+ has_paths = opts->paths.count > 0;
if ((error = git_repository__ensure_not_bare(repo, "stash save")) < 0)
return error;
@@ -555,44 +703,63 @@ int git_stash_save(
if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0)
goto cleanup;
- if ((error = ensure_there_are_changes_to_stash(repo, flags)) < 0)
+ if (!has_paths &&
+ (error = ensure_there_are_changes_to_stash(repo, opts->flags)) < 0)
+ goto cleanup;
+ else if (has_paths &&
+ (error = ensure_there_are_changes_to_stash_paths(
+ repo, opts->flags, &opts->paths)) < 0)
goto cleanup;
if ((error = git_repository_index(&index, repo)) < 0)
goto cleanup;
- if ((error = commit_index(&i_commit, repo, index, stasher,
+ if ((error = commit_index(&i_commit, repo, index, opts->stasher,
git_str_cstr(&msg), b_commit)) < 0)
goto cleanup;
- if ((flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) &&
- (error = commit_untracked(&u_commit, repo, stasher,
- git_str_cstr(&msg), i_commit, flags)) < 0)
+ if ((opts->flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) &&
+ (error = commit_untracked(&u_commit, repo, opts->stasher,
+ git_str_cstr(&msg), i_commit, opts->flags)) < 0)
goto cleanup;
- if ((error = prepare_worktree_commit_message(&msg, message)) < 0)
+ if ((error = prepare_worktree_commit_message(&msg, opts->message)) < 0)
goto cleanup;
- if ((error = commit_worktree(out, repo, stasher, git_str_cstr(&msg),
- i_commit, b_commit, u_commit)) < 0)
- goto cleanup;
+ if (!has_paths) {
+ if ((error = commit_worktree(out, repo, opts->stasher, git_str_cstr(&msg),
+ i_commit, b_commit, u_commit)) < 0)
+ goto cleanup;
+ } else {
+ if ((error = git_index_new(&paths_index)) < 0 ||
+ (error = retrieve_head(&head, repo)) < 0 ||
+ (error = git_reference_peel((git_object**)&tree, head, GIT_OBJECT_TREE)) < 0 ||
+ (error = git_index_read_tree(paths_index, tree)) < 0 ||
+ (error = stash_update_index_from_paths(repo, paths_index, &opts->paths)) < 0 ||
+ (error = build_stash_commit_from_index(out, repo, opts->stasher, git_str_cstr(&msg),
+ i_commit, b_commit, u_commit, paths_index)) < 0)
+ goto cleanup;
+ }
git_str_rtrim(&msg);
if ((error = update_reflog(out, repo, git_str_cstr(&msg))) < 0)
goto cleanup;
- if ((error = reset_index_and_workdir(repo, (flags & GIT_STASH_KEEP_INDEX) ? i_commit : b_commit,
- flags)) < 0)
+ if (!(opts->flags & GIT_STASH_KEEP_ALL) &&
+ (error = reset_index_and_workdir(repo,
+ (opts->flags & GIT_STASH_KEEP_INDEX) ? i_commit : b_commit,opts->flags)) < 0)
goto cleanup;
cleanup:
-
git_str_dispose(&msg);
git_commit_free(i_commit);
git_commit_free(b_commit);
git_commit_free(u_commit);
+ git_tree_free(tree);
+ git_reference_free(head);
git_index_free(index);
+ git_index_free(paths_index);
return error;
}
@@ -777,6 +944,13 @@ int git_stash_apply_options_init(git_stash_apply_options *opts, unsigned int ver
return 0;
}
+int git_stash_save_options_init(git_stash_save_options *opts, unsigned int version)
+{
+ GIT_INIT_STRUCTURE_FROM_TEMPLATE(
+ opts, version, git_stash_save_options, GIT_STASH_SAVE_OPTIONS_INIT);
+ return 0;
+}
+
#ifndef GIT_DEPRECATE_HARD
int git_stash_apply_init_options(git_stash_apply_options *opts, unsigned int version)
{
diff --git a/tests/libgit2/core/structinit.c b/tests/libgit2/core/structinit.c
index 160e2f612..8a6e48d2a 100644
--- a/tests/libgit2/core/structinit.c
+++ b/tests/libgit2/core/structinit.c
@@ -160,6 +160,11 @@ void test_core_structinit__compare(void)
git_stash_apply_options, GIT_STASH_APPLY_OPTIONS_VERSION, \
GIT_STASH_APPLY_OPTIONS_INIT, git_stash_apply_options_init);
+ /* stash save */
+ CHECK_MACRO_FUNC_INIT_EQUAL( \
+ git_stash_save_options, GIT_STASH_SAVE_OPTIONS_VERSION, \
+ GIT_STASH_SAVE_OPTIONS_INIT, git_stash_save_options_init);
+
/* status */
CHECK_MACRO_FUNC_INIT_EQUAL( \
git_status_options, GIT_STATUS_OPTIONS_VERSION, \
diff --git a/tests/libgit2/stash/save.c b/tests/libgit2/stash/save.c
index f574211d7..23f3c1cbb 100644
--- a/tests/libgit2/stash/save.c
+++ b/tests/libgit2/stash/save.c
@@ -130,6 +130,19 @@ void test_stash_save__can_keep_index(void)
assert_status(repo, "just.ignore", GIT_STATUS_IGNORED);
}
+void test_stash_save__can_keep_all(void)
+{
+ cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_KEEP_ALL));
+
+ assert_status(repo, "what", GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_MODIFIED);
+ assert_status(repo, "how", GIT_STATUS_INDEX_MODIFIED);
+ assert_status(repo, "who", GIT_STATUS_WT_MODIFIED);
+ assert_status(repo, "when", GIT_STATUS_WT_NEW);
+ assert_status(repo, "why", GIT_STATUS_INDEX_NEW);
+ assert_status(repo, "where", GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_NEW);
+ assert_status(repo, "just.ignore", GIT_STATUS_IGNORED);
+}
+
static void assert_commit_message_contains(const char *revision, const char *fragment)
{
git_commit *commit;
@@ -488,3 +501,27 @@ void test_stash_save__deleted_in_index_modified_in_workdir(void)
git_index_free(index);
}
+
+void test_stash_save__option_paths(void)
+{
+ git_stash_save_options options = GIT_STASH_SAVE_OPTIONS_INIT;
+ char *paths[2] = { "who", "where" };
+
+ options.paths = (git_strarray){
+ paths,
+ 2
+ };
+ options.stasher = signature;
+
+ cl_git_pass(git_stash_save_with_opts(&stash_tip_oid, repo, &options));
+
+ assert_blob_oid("refs/stash:who", "a0400d4954659306a976567af43125a0b1aa8595");
+ assert_blob_oid("refs/stash:where", "e3d6434ec12eb76af8dfa843a64ba6ab91014a0b");
+
+ assert_blob_oid("refs/stash:what", "ce013625030ba8dba906f756967f9e9ca394464a");
+ assert_blob_oid("refs/stash:how", "ac790413e2d7a26c3767e78c57bb28716686eebc");
+ assert_blob_oid("refs/stash:when", NULL);
+ assert_blob_oid("refs/stash:why", NULL);
+ assert_blob_oid("refs/stash:.gitignore", "ac4d88de61733173d9959e4b77c69b9f17a00980");
+ assert_blob_oid("refs/stash:just.ignore", NULL);
+}