Skip to content

Commit 8dc78a0

Browse files
stash: implement partial stashing by path
1 parent ac0f224 commit 8dc78a0

File tree

2 files changed

+260
-26
lines changed

2 files changed

+260
-26
lines changed

include/git2/stash.h

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ typedef enum {
4444
* All ignored files are also stashed and then cleaned up from
4545
* the working directory
4646
*/
47-
GIT_STASH_INCLUDE_IGNORED = (1 << 2)
47+
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/libgit2/stash.c

Lines changed: 195 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,30 @@ static int stash_to_index(
193193
return git_index_add(index, &entry);
194194
}
195195

196+
static int stash_update_index_from_paths(
197+
git_repository *repo,
198+
git_index *index,
199+
git_strarray *paths)
200+
{
201+
unsigned int status_flags;
202+
size_t i, error = 0;
203+
204+
for(i = 0; i < paths->count; i++) {
205+
git_status_file(&status_flags, repo, paths->strings[i]);
206+
207+
if (status_flags & (GIT_STATUS_WT_DELETED | GIT_STATUS_INDEX_DELETED)) {
208+
if ((error = git_index_remove(index, paths->strings[i], 0)) < 0)
209+
return error;
210+
}
211+
else {
212+
if ((error = stash_to_index(repo, index, paths->strings[i])) < 0)
213+
return error;
214+
}
215+
}
216+
217+
return error;
218+
}
219+
196220
static int stash_update_index_from_diff(
197221
git_repository *repo,
198222
git_index *index,
@@ -388,24 +412,80 @@ static int build_workdir_tree(
388412
return error;
389413
}
390414

391-
static int commit_worktree(
415+
static int build_stash_commit_from_tree(
392416
git_oid *w_commit_oid,
393417
git_repository *repo,
394418
const git_signature *stasher,
395419
const char *message,
396420
git_commit *i_commit,
397421
git_commit *b_commit,
398-
git_commit *u_commit)
422+
git_commit *u_commit,
423+
git_tree *tree)
399424
{
400425
const git_commit *parents[] = { NULL, NULL, NULL };
401-
git_index *i_index = NULL, *r_index = NULL;
402-
git_tree *w_tree = NULL;
403-
int error = 0, ignorecase;
404426

405427
parents[0] = b_commit;
406428
parents[1] = i_commit;
407429
parents[2] = u_commit;
408430

431+
return git_commit_create(
432+
w_commit_oid,
433+
repo,
434+
NULL,
435+
stasher,
436+
stasher,
437+
NULL,
438+
message,
439+
tree,
440+
u_commit ? 3 : 2,
441+
parents);
442+
}
443+
444+
static int build_stash_commit_from_index(
445+
git_oid *w_commit_oid,
446+
git_repository *repo,
447+
const git_signature *stasher,
448+
const char *message,
449+
git_commit *i_commit,
450+
git_commit *b_commit,
451+
git_commit *u_commit,
452+
git_index *index)
453+
{
454+
git_tree *tree;
455+
int error;
456+
457+
if ((error = build_tree_from_index(&tree, repo, index)) < 0)
458+
goto cleanup;
459+
460+
error = build_stash_commit_from_tree(
461+
w_commit_oid,
462+
repo,
463+
stasher,
464+
message,
465+
i_commit,
466+
b_commit,
467+
u_commit,
468+
tree
469+
);
470+
471+
cleanup:
472+
git_tree_free(tree);
473+
return error;
474+
}
475+
476+
static int commit_worktree(
477+
git_oid *w_commit_oid,
478+
git_repository *repo,
479+
const git_signature *stasher,
480+
const char *message,
481+
git_commit *i_commit,
482+
git_commit *b_commit,
483+
git_commit *u_commit)
484+
{
485+
git_index *i_index = NULL, *r_index = NULL;
486+
git_tree *w_tree = NULL;
487+
int error = 0, ignorecase;
488+
409489
if ((error = git_repository_index(&r_index, repo) < 0) ||
410490
(error = git_index_new(&i_index)) < 0 ||
411491
(error = git_index__fill(i_index, &r_index->entries) < 0) ||
@@ -417,17 +497,16 @@ static int commit_worktree(
417497
if ((error = build_workdir_tree(&w_tree, repo, i_index, b_commit)) < 0)
418498
goto cleanup;
419499

420-
error = git_commit_create(
500+
error = build_stash_commit_from_tree(
421501
w_commit_oid,
422502
repo,
423-
NULL,
424-
stasher,
425503
stasher,
426-
NULL,
427504
message,
428-
w_tree,
429-
u_commit ? 3 : 2,
430-
parents);
505+
i_commit,
506+
b_commit,
507+
u_commit,
508+
w_tree
509+
);
431510

432511
cleanup:
433512
git_tree_free(w_tree);
@@ -520,6 +599,50 @@ static int ensure_there_are_changes_to_stash(git_repository *repo, uint32_t flag
520599
return error;
521600
}
522601

602+
static int has_changes_cb(const char *path, unsigned int status, void *payload) {
603+
GIT_UNUSED(path);
604+
GIT_UNUSED(status);
605+
GIT_UNUSED(payload);
606+
607+
if (status == GIT_STATUS_CURRENT)
608+
return GIT_ENOTFOUND;
609+
610+
return 0;
611+
}
612+
613+
static int ensure_there_are_changes_to_stash_paths(
614+
git_repository *repo,
615+
uint32_t flags,
616+
git_strarray *paths)
617+
{
618+
int error;
619+
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
620+
621+
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
622+
opts.flags = GIT_STATUS_OPT_EXCLUDE_SUBMODULES
623+
| GIT_STATUS_OPT_INCLUDE_UNMODIFIED
624+
| GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
625+
626+
if (flags & GIT_STASH_INCLUDE_UNTRACKED)
627+
opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED |
628+
GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
629+
630+
if (flags & GIT_STASH_INCLUDE_IGNORED)
631+
opts.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED |
632+
GIT_STATUS_OPT_RECURSE_IGNORED_DIRS;
633+
634+
git_strarray_copy(&opts.pathspec, paths);
635+
636+
error = git_status_foreach_ext(repo, &opts, has_changes_cb, NULL);
637+
638+
git_strarray_dispose(&opts.pathspec);
639+
640+
if (error == GIT_ENOTFOUND)
641+
return create_error(GIT_ENOTFOUND, "one of the files does not have any changes to stash.");
642+
643+
return error;
644+
}
645+
523646
static int reset_index_and_workdir(git_repository *repo, git_commit *commit, uint32_t flags)
524647
{
525648
git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
@@ -540,50 +663,87 @@ int git_stash_save(
540663
const char *message,
541664
uint32_t flags)
542665
{
543-
git_index *index = NULL;
666+
git_stash_save_options opts = GIT_STASH_SAVE_OPTIONS_INIT;
667+
opts.stasher = stasher;
668+
opts.message = message;
669+
opts.flags = flags;
670+
return git_stash_save_with_opts(out, repo, &opts);
671+
}
672+
673+
int git_stash_save_with_opts(
674+
git_oid *out, git_repository *repo, git_stash_save_options *opts)
675+
{
676+
git_index *index = NULL, *paths_index = NULL;
544677
git_commit *b_commit = NULL, *i_commit = NULL, *u_commit = NULL;
545678
git_str msg = GIT_STR_INIT;
679+
git_tree *tree = NULL;
680+
git_reference *head = NULL;
546681
int error;
547682

548683
GIT_ASSERT_ARG(out);
549684
GIT_ASSERT_ARG(repo);
550-
GIT_ASSERT_ARG(stasher);
551685

552686
if ((error = git_repository__ensure_not_bare(repo, "stash save")) < 0)
553687
return error;
554688

555689
if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0)
556690
goto cleanup;
557691

558-
if ((error = ensure_there_are_changes_to_stash(repo, flags)) < 0)
692+
if (opts->paths.count == 0 &&
693+
(error = ensure_there_are_changes_to_stash(repo, opts->flags)) < 0)
694+
goto cleanup;
695+
else if (opts->paths.count > 0 &&
696+
(error = ensure_there_are_changes_to_stash_paths(
697+
repo, opts->flags, &opts->paths)) < 0)
559698
goto cleanup;
560699

561700
if ((error = git_repository_index(&index, repo)) < 0)
562701
goto cleanup;
563702

564-
if ((error = commit_index(&i_commit, repo, index, stasher,
703+
if ((error = commit_index(&i_commit, repo, index, opts->stasher,
565704
git_str_cstr(&msg), b_commit)) < 0)
566705
goto cleanup;
567706

568-
if ((flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) &&
569-
(error = commit_untracked(&u_commit, repo, stasher,
570-
git_str_cstr(&msg), i_commit, flags)) < 0)
707+
if ((opts->flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) &&
708+
(error = commit_untracked(&u_commit, repo, opts->stasher,
709+
git_str_cstr(&msg), i_commit, opts->flags)) < 0)
571710
goto cleanup;
572711

573-
if ((error = prepare_worktree_commit_message(&msg, message)) < 0)
712+
if ((error = prepare_worktree_commit_message(&msg, opts->message)) < 0)
574713
goto cleanup;
575714

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

580740
git_str_rtrim(&msg);
581741

582742
if ((error = update_reflog(out, repo, git_str_cstr(&msg))) < 0)
583743
goto cleanup;
584744

585-
if ((error = reset_index_and_workdir(repo, (flags & GIT_STASH_KEEP_INDEX) ? i_commit : b_commit,
586-
flags)) < 0)
745+
if (!(opts->flags & GIT_STASH_KEEP_ALL) && (error = reset_index_and_workdir(repo,
746+
(opts->flags & GIT_STASH_KEEP_INDEX) ? i_commit : b_commit, opts->flags)) < 0)
587747
goto cleanup;
588748

589749
cleanup:
@@ -592,7 +752,10 @@ int git_stash_save(
592752
git_commit_free(i_commit);
593753
git_commit_free(b_commit);
594754
git_commit_free(u_commit);
755+
git_tree_free(tree);
756+
git_reference_free(head);
595757
git_index_free(index);
758+
git_index_free(paths_index);
596759

597760
return error;
598761
}
@@ -777,6 +940,13 @@ int git_stash_apply_options_init(git_stash_apply_options *opts, unsigned int ver
777940
return 0;
778941
}
779942

943+
int git_stash_save_options_init(git_stash_save_options *opts, unsigned int version)
944+
{
945+
GIT_INIT_STRUCTURE_FROM_TEMPLATE(
946+
opts, version, git_stash_save_options, GIT_STASH_SAVE_OPTIONS_INIT);
947+
return 0;
948+
}
949+
780950
#ifndef GIT_DEPRECATE_HARD
781951
int git_stash_apply_init_options(git_stash_apply_options *opts, unsigned int version)
782952
{

0 commit comments

Comments
 (0)