Skip to content

Commit 8280bb0

Browse files
stash: implement partial stashing by path
code has been moved from `git_stash_save` into `git_stash_save_with_opts` in order to handle cases where we want to partially stash using options or stash normally without partially stashed paths specified
1 parent d6554d0 commit 8280bb0

File tree

2 files changed

+194
-14
lines changed

2 files changed

+194
-14
lines changed

include/git2/stash.h

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ typedef enum {
4545
* the working directory
4646
*/
4747
GIT_STASH_INCLUDE_IGNORED = (1 << 2),
48+
49+
/**
50+
* All changes in the index and working directory are left intact
51+
*/
52+
GIT_STASH_KEEP_ALL = (1 << 3),
4853
} git_stash_flags;
4954

5055
/**
@@ -71,6 +76,65 @@ GIT_EXTERN(int) git_stash_save(
7176
const char *message,
7277
uint32_t flags);
7378

79+
/**
80+
* Stash save options structure
81+
*
82+
* Initialize with `GIT_STASH_SAVE_OPTIONS_INIT`. Alternatively, you can
83+
* use `git_stash_save_options_init`.
84+
*
85+
*/
86+
typedef struct git_stash_save_options {
87+
unsigned int version;
88+
89+
/** The identity of the person performing the stashing. */
90+
const git_signature *stasher;
91+
92+
/** Optional description along with the stashed state. */
93+
const char *message;
94+
95+
/** Flags to control the stashing process. (see GIT_STASH_* above) */
96+
uint32_t flags;
97+
98+
/** Optional paths that control which files are stashed. */
99+
git_strarray paths;
100+
} git_stash_save_options;
101+
102+
#define GIT_STASH_SAVE_OPTIONS_VERSION 1
103+
#define GIT_STASH_SAVE_OPTIONS_INIT { \
104+
GIT_STASH_SAVE_OPTIONS_VERSION, \
105+
NULL, \
106+
NULL, \
107+
GIT_STASH_DEFAULT }
108+
109+
/**
110+
* Initialize git_stash_save_options structure
111+
*
112+
* Initializes a `git_stash_save_options` with default values. Equivalent to
113+
* creating an instance with `GIT_STASH_SAVE_OPTIONS_INIT`.
114+
*
115+
* @param opts The `git_stash_save_options` struct to initialize.
116+
* @param version The struct version; pass `GIT_STASH_SAVE_OPTIONS_VERSION`.
117+
* @return Zero on success; -1 on failure.
118+
*/
119+
GIT_EXTERN(int) git_stash_save_options_init(
120+
git_stash_save_options *opts, unsigned int version);
121+
122+
/**
123+
* Save the local modifications to a new stash, with options.
124+
*
125+
* @param out Object id of the commit containing the stashed state.
126+
* This commit is also the target of the direct reference refs/stash.
127+
*
128+
* @param repo The owning repository.
129+
*
130+
* @param opts The stash options.
131+
*
132+
* @return 0 on success, GIT_ENOTFOUND where there's nothing to stash,
133+
* or error code.
134+
*/
135+
GIT_EXTERN(int) git_stash_save_with_opts(
136+
git_oid *out, git_repository *repo, git_stash_save_options *opts);
137+
74138
/** Stash application flags. */
75139
typedef enum {
76140
GIT_STASH_APPLY_DEFAULT = 0,

src/stash.c

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,31 @@ static int stash_to_index(
194194
return git_index_add(index, &entry);
195195
}
196196

197+
static int stash_update_index_from_paths(
198+
git_repository *repo,
199+
git_index *index,
200+
git_strarray *paths)
201+
{
202+
unsigned int status_flags;
203+
size_t i;
204+
int error = 0;
205+
206+
for(i = 0; i < paths->count; i++) {
207+
git_status_file(&status_flags, repo, paths->strings[i]);
208+
209+
if (status_flags & (GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_DELETED)) {
210+
if ((error = git_index_remove(index, paths->strings[i], 0)) < 0)
211+
return error;
212+
}
213+
else {
214+
if ((error = stash_to_index(repo, index, paths->strings[i])) < 0)
215+
return error;
216+
}
217+
}
218+
219+
return error;
220+
}
221+
197222
static int stash_update_index_from_diff(
198223
git_repository *repo,
199224
git_index *index,
@@ -576,6 +601,50 @@ static int ensure_there_are_changes_to_stash(git_repository *repo, uint32_t flag
576601
return error;
577602
}
578603

604+
static int has_changes_cb(const char *path, unsigned int status, void *payload) {
605+
GIT_UNUSED(path);
606+
GIT_UNUSED(status);
607+
GIT_UNUSED(payload);
608+
609+
if (status == GIT_STATUS_CURRENT)
610+
return GIT_ENOTFOUND;
611+
612+
return 0;
613+
}
614+
615+
static int ensure_there_are_changes_to_stash_paths(
616+
git_repository *repo,
617+
uint32_t flags,
618+
git_strarray *paths)
619+
{
620+
int error;
621+
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
622+
623+
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
624+
opts.flags = GIT_STATUS_OPT_EXCLUDE_SUBMODULES
625+
| GIT_STATUS_OPT_INCLUDE_UNMODIFIED
626+
| GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
627+
628+
if (flags & GIT_STASH_INCLUDE_UNTRACKED)
629+
opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED |
630+
GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
631+
632+
if (flags & GIT_STASH_INCLUDE_IGNORED)
633+
opts.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED |
634+
GIT_STATUS_OPT_RECURSE_IGNORED_DIRS;
635+
636+
git_strarray_copy(&opts.pathspec, paths);
637+
638+
error = git_status_foreach_ext(repo, &opts, has_changes_cb, NULL);
639+
640+
git_strarray_dispose(&opts.pathspec);
641+
642+
if (error == GIT_ENOTFOUND)
643+
return create_error(GIT_ENOTFOUND, "one of the files does not have any changes to stash.");
644+
645+
return error;
646+
}
647+
579648
static int reset_index_and_workdir(git_repository *repo, git_commit *commit, uint32_t flags)
580649
{
581650
git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
@@ -596,50 +665,87 @@ int git_stash_save(
596665
const char *message,
597666
uint32_t flags)
598667
{
599-
git_index *index = NULL;
668+
git_stash_save_options opts = GIT_STASH_SAVE_OPTIONS_INIT;
669+
opts.stasher = stasher;
670+
opts.message = message;
671+
opts.flags = flags;
672+
return git_stash_save_with_opts(out, repo, &opts);
673+
}
674+
675+
int git_stash_save_with_opts(
676+
git_oid *out, git_repository *repo, git_stash_save_options *opts)
677+
{
678+
git_index *index = NULL, *paths_index = NULL;
600679
git_commit *b_commit = NULL, *i_commit = NULL, *u_commit = NULL;
601680
git_buf msg = GIT_BUF_INIT;
681+
git_tree *tree = NULL;
682+
git_reference *head = NULL;
602683
int error;
603684

604685
GIT_ASSERT_ARG(out);
605686
GIT_ASSERT_ARG(repo);
606-
GIT_ASSERT_ARG(stasher);
607687

608688
if ((error = git_repository__ensure_not_bare(repo, "stash save")) < 0)
609689
return error;
610690

611691
if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0)
612692
goto cleanup;
613693

614-
if ((error = ensure_there_are_changes_to_stash(repo, flags)) < 0)
694+
if (opts->paths.count == 0 &&
695+
(error = ensure_there_are_changes_to_stash(repo, opts->flags)) < 0)
696+
goto cleanup;
697+
else if (opts->paths.count > 0 &&
698+
(error = ensure_there_are_changes_to_stash_paths(
699+
repo, opts->flags, &opts->paths)) < 0)
615700
goto cleanup;
616701

617702
if ((error = git_repository_index(&index, repo)) < 0)
618703
goto cleanup;
619704

620-
if ((error = commit_index(&i_commit, repo, index, stasher,
621-
git_buf_cstr(&msg), b_commit)) < 0)
705+
if ((error = commit_index(&i_commit, repo, index, opts->stasher,
706+
git_buf_cstr(&msg), b_commit)) < 0)
622707
goto cleanup;
623708

624-
if ((flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) &&
625-
(error = commit_untracked(&u_commit, repo, stasher,
626-
git_buf_cstr(&msg), i_commit, flags)) < 0)
709+
if ((opts->flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) &&
710+
(error = commit_untracked(&u_commit, repo, opts->stasher,
711+
git_buf_cstr(&msg), i_commit, opts->flags)) < 0)
627712
goto cleanup;
628713

629-
if ((error = prepare_worktree_commit_message(&msg, message)) < 0)
714+
if ((error = prepare_worktree_commit_message(&msg, opts->message)) < 0)
630715
goto cleanup;
631716

632-
if ((error = commit_worktree(out, repo, stasher, git_buf_cstr(&msg),
633-
i_commit, b_commit, u_commit)) < 0)
634-
goto cleanup;
717+
if (opts->paths.count == 0) {
718+
if ((error = commit_worktree(out, repo, opts->stasher, git_buf_cstr(&msg),
719+
i_commit, b_commit, u_commit)) < 0)
720+
goto cleanup;
721+
} else {
722+
if ((error = git_index_new(&paths_index)) < 0)
723+
goto cleanup;
724+
725+
if ((error = retrieve_head(&head, repo)) < 0)
726+
goto cleanup;
727+
728+
if ((error = git_reference_peel((git_object**)&tree, head, GIT_OBJECT_TREE)) < 0)
729+
goto cleanup;
730+
731+
if ((error = git_index_read_tree(paths_index, tree)) < 0)
732+
goto cleanup;
733+
734+
if ((error = stash_update_index_from_paths(repo, paths_index, &opts->paths)) < 0)
735+
goto cleanup;
736+
737+
if ((error = build_stash_commit_from_index(out, repo, opts->stasher, git_buf_cstr(&msg),
738+
i_commit, b_commit, u_commit, paths_index)) < 0)
739+
goto cleanup;
740+
}
635741

636742
git_buf_rtrim(&msg);
637743

638744
if ((error = update_reflog(out, repo, git_buf_cstr(&msg))) < 0)
639745
goto cleanup;
640746

641-
if ((error = reset_index_and_workdir(repo, (flags & GIT_STASH_KEEP_INDEX) ? i_commit : b_commit,
642-
flags)) < 0)
747+
if (!(opts->flags & GIT_STASH_KEEP_ALL) && (error = reset_index_and_workdir(repo,
748+
(opts->flags & GIT_STASH_KEEP_INDEX) ? i_commit : b_commit, opts->flags)) < 0)
643749
goto cleanup;
644750

645751
cleanup:
@@ -648,7 +754,10 @@ int git_stash_save(
648754
git_commit_free(i_commit);
649755
git_commit_free(b_commit);
650756
git_commit_free(u_commit);
757+
git_tree_free(tree);
758+
git_reference_free(head);
651759
git_index_free(index);
760+
git_index_free(paths_index);
652761

653762
return error;
654763
}
@@ -833,6 +942,13 @@ int git_stash_apply_options_init(git_stash_apply_options *opts, unsigned int ver
833942
return 0;
834943
}
835944

945+
int git_stash_save_options_init(git_stash_save_options *opts, unsigned int version)
946+
{
947+
GIT_INIT_STRUCTURE_FROM_TEMPLATE(
948+
opts, version, git_stash_save_options, GIT_STASH_SAVE_OPTIONS_INIT);
949+
return 0;
950+
}
951+
836952
#ifndef GIT_DEPRECATE_HARD
837953
int git_stash_apply_init_options(git_stash_apply_options *opts, unsigned int version)
838954
{

0 commit comments

Comments
 (0)