diff options
author | Jacob Watson <gary.kactara.email@gmail.com> | 2022-06-21 15:23:33 -0700 |
---|---|---|
committer | Jacob Watson <gary.kactara.email@gmail.com> | 2022-07-14 10:20:20 -0700 |
commit | 8dc78a0f620c252ec37832bc8a75140346f0ad62 (patch) | |
tree | b38b506a8c4a2f54a65acd96692aa0fa3c5168d9 | |
parent | ac0f2245510f6c75db1b1e7af7ca01c15dec26bc (diff) | |
download | libgit2-8dc78a0f620c252ec37832bc8a75140346f0ad62.tar.gz |
stash: implement partial stashing by path
-rw-r--r-- | include/git2/stash.h | 66 | ||||
-rw-r--r-- | src/libgit2/stash.c | 220 |
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) { |