Unverified Commit 05ba3fe4 by Edward Thomson Committed by GitHub

Merge pull request #6330 from gitkraken-jacobw/partial-stashing

stash: partial stash specific files
parents 7108b431 35580d88
......@@ -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,
......
......@@ -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)
{
......
......@@ -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, \
......
......@@ -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);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment