summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Watson <gary.kactara.email@gmail.com>2022-06-21 15:23:33 -0700
committerJacob Watson <gary.kactara.email@gmail.com>2022-07-14 10:20:20 -0700
commit8dc78a0f620c252ec37832bc8a75140346f0ad62 (patch)
treeb38b506a8c4a2f54a65acd96692aa0fa3c5168d9
parentac0f2245510f6c75db1b1e7af7ca01c15dec26bc (diff)
downloadlibgit2-8dc78a0f620c252ec37832bc8a75140346f0ad62.tar.gz
stash: implement partial stashing by path
-rw-r--r--include/git2/stash.h66
-rw-r--r--src/libgit2/stash.c220
2 files changed, 260 insertions, 26 deletions
diff --git a/include/git2/stash.h b/include/git2/stash.h
index 32e6f9576..3b8908195 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;
/**
@@ -71,6 +76,65 @@ 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;
+
+ /** The identity of the person performing the stashing. */
+ const git_signature *stasher;
+
+ /** Optional description along with the stashed state. */
+ const char *message;
+
+ /** Flags to control the stashing process. (see GIT_STASH_* above) */
+ uint32_t flags;
+
+ /** 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, \
+ NULL, \
+ NULL, \
+ GIT_STASH_DEFAULT }
+
+/**
+ * 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, 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..91ff89889 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,
+ git_strarray *paths)
+{
+ unsigned int status_flags;
+ size_t i, 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,80 @@ 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,
+ 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 +497,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 +599,50 @@ 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,
+ 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 +663,25 @@ int git_stash_save(
const char *message,
uint32_t flags)
{
- git_index *index = NULL;
+ git_stash_save_options opts = GIT_STASH_SAVE_OPTIONS_INIT;
+ 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, 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;
int error;
GIT_ASSERT_ARG(out);
GIT_ASSERT_ARG(repo);
- GIT_ASSERT_ARG(stasher);
if ((error = git_repository__ensure_not_bare(repo, "stash save")) < 0)
return error;
@@ -555,35 +689,61 @@ 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 (opts->paths.count == 0 &&
+ (error = ensure_there_are_changes_to_stash(repo, opts->flags)) < 0)
+ goto cleanup;
+ else if (opts->paths.count > 0 &&
+ (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 (opts->paths.count == 0) {
+ 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)
+ goto cleanup;
+
+ if ((error = retrieve_head(&head, repo)) < 0)
+ goto cleanup;
+
+ if ((error = git_reference_peel((git_object**)&tree, head, GIT_OBJECT_TREE)) < 0)
+ goto cleanup;
+
+ if ((error = git_index_read_tree(paths_index, tree)) < 0)
+ goto cleanup;
+
+ if ((error = stash_update_index_from_paths(repo, paths_index, &opts->paths)) < 0)
+ goto cleanup;
+
+ if ((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:
@@ -592,7 +752,10 @@ cleanup:
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 +940,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)
{