mirror of
https://github.com/git/git.git
synced 2025-12-12 20:36:24 +01:00
Merge branch 'bc/stash-export-import'
An interchange format for stash entries is defined, and subcommand of "git stash" to import/export has been added. * bc/stash-export-import: builtin/stash: provide a way to import stashes from a ref builtin/stash: provide a way to export stashes to a ref builtin/stash: factor out revision parsing into a function object-name: make get_oid quietly return an error
This commit is contained in:
@@ -23,6 +23,8 @@ SYNOPSIS
|
||||
'git stash' clear
|
||||
'git stash' create [<message>]
|
||||
'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>
|
||||
'git stash' export (--print | --to-ref <ref>) [<stash>...]
|
||||
'git stash' import <commit>
|
||||
|
||||
DESCRIPTION
|
||||
-----------
|
||||
@@ -154,6 +156,18 @@ store::
|
||||
reflog. This is intended to be useful for scripts. It is
|
||||
probably not the command you want to use; see "push" above.
|
||||
|
||||
export ( --print | --to-ref <ref> ) [<stash>...]::
|
||||
|
||||
Export the specified stashes, or all of them if none are specified, to
|
||||
a chain of commits which can be transferred using the normal fetch and
|
||||
push mechanisms, then imported using the `import` subcommand.
|
||||
|
||||
import <commit>::
|
||||
|
||||
Import the specified stashes from the specified commit, which must have been
|
||||
created by `export`, and add them to the list of stashes. To replace the
|
||||
existing stashes, use `clear` first.
|
||||
|
||||
OPTIONS
|
||||
-------
|
||||
-a::
|
||||
@@ -242,6 +256,19 @@ literally (including newlines and quotes).
|
||||
+
|
||||
Quiet, suppress feedback messages.
|
||||
|
||||
--print::
|
||||
This option is only valid for the `export` command.
|
||||
+
|
||||
Create the chain of commits representing the exported stashes without
|
||||
storing it anywhere in the ref namespace and print the object ID to
|
||||
standard output. This is designed for scripts.
|
||||
|
||||
--to-ref::
|
||||
This option is only valid for the `export` command.
|
||||
+
|
||||
Create the chain of commits representing the exported stashes and store
|
||||
it to the specified ref.
|
||||
|
||||
\--::
|
||||
This option is only valid for `push` command.
|
||||
+
|
||||
@@ -259,7 +286,7 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
|
||||
|
||||
<stash>::
|
||||
This option is only valid for `apply`, `branch`, `drop`, `pop`,
|
||||
`show` commands.
|
||||
`show`, and `export` commands.
|
||||
+
|
||||
A reference of the form `stash@{<revision>}`. When no `<stash>` is
|
||||
given, the latest stash is assumed (that is, `stash@{0}`).
|
||||
|
||||
458
builtin/stash.c
458
builtin/stash.c
@@ -28,7 +28,10 @@
|
||||
#include "log-tree.h"
|
||||
#include "diffcore.h"
|
||||
#include "reflog.h"
|
||||
#include "reflog-walk.h"
|
||||
#include "add-interactive.h"
|
||||
#include "oid-array.h"
|
||||
#include "commit.h"
|
||||
|
||||
#define INCLUDE_ALL_FILES 2
|
||||
|
||||
@@ -56,6 +59,10 @@
|
||||
" [-u | --include-untracked] [-a | --all] [<message>]")
|
||||
#define BUILTIN_STASH_CREATE_USAGE \
|
||||
N_("git stash create [<message>]")
|
||||
#define BUILTIN_STASH_EXPORT_USAGE \
|
||||
N_("git stash export (--print | --to-ref <ref>) [<stash>...]")
|
||||
#define BUILTIN_STASH_IMPORT_USAGE \
|
||||
N_("git stash import <commit>")
|
||||
#define BUILTIN_STASH_CLEAR_USAGE \
|
||||
"git stash clear"
|
||||
|
||||
@@ -71,6 +78,8 @@ static const char * const git_stash_usage[] = {
|
||||
BUILTIN_STASH_CLEAR_USAGE,
|
||||
BUILTIN_STASH_CREATE_USAGE,
|
||||
BUILTIN_STASH_STORE_USAGE,
|
||||
BUILTIN_STASH_EXPORT_USAGE,
|
||||
BUILTIN_STASH_IMPORT_USAGE,
|
||||
NULL
|
||||
};
|
||||
|
||||
@@ -124,6 +133,16 @@ static const char * const git_stash_save_usage[] = {
|
||||
NULL
|
||||
};
|
||||
|
||||
static const char * const git_stash_export_usage[] = {
|
||||
BUILTIN_STASH_EXPORT_USAGE,
|
||||
NULL
|
||||
};
|
||||
|
||||
static const char * const git_stash_import_usage[] = {
|
||||
BUILTIN_STASH_IMPORT_USAGE,
|
||||
NULL
|
||||
};
|
||||
|
||||
static const char ref_stash[] = "refs/stash";
|
||||
static struct strbuf stash_index_path = STRBUF_INIT;
|
||||
|
||||
@@ -132,6 +151,7 @@ static struct strbuf stash_index_path = STRBUF_INIT;
|
||||
* b_commit is set to the base commit
|
||||
* i_commit is set to the commit containing the index tree
|
||||
* u_commit is set to the commit containing the untracked files tree
|
||||
* c_commit is set to the first parent (chain commit) when importing and is otherwise unset
|
||||
* w_tree is set to the working tree
|
||||
* b_tree is set to the base tree
|
||||
* i_tree is set to the index tree
|
||||
@@ -142,6 +162,7 @@ struct stash_info {
|
||||
struct object_id b_commit;
|
||||
struct object_id i_commit;
|
||||
struct object_id u_commit;
|
||||
struct object_id c_commit;
|
||||
struct object_id w_tree;
|
||||
struct object_id b_tree;
|
||||
struct object_id i_tree;
|
||||
@@ -160,6 +181,33 @@ static void free_stash_info(struct stash_info *info)
|
||||
strbuf_release(&info->revision);
|
||||
}
|
||||
|
||||
static int check_stash_topology(struct repository *r, struct commit *stash)
|
||||
{
|
||||
struct commit *p1, *p2, *p3 = NULL;
|
||||
|
||||
/* stash must have two or three parents */
|
||||
if (!stash->parents || !stash->parents->next ||
|
||||
(stash->parents->next->next && stash->parents->next->next->next))
|
||||
return -1;
|
||||
p1 = stash->parents->item;
|
||||
p2 = stash->parents->next->item;
|
||||
if (stash->parents->next->next)
|
||||
p3 = stash->parents->next->next->item;
|
||||
if (repo_parse_commit(r, p1) || repo_parse_commit(r, p2) ||
|
||||
(p3 && repo_parse_commit(r, p3)))
|
||||
return -1;
|
||||
/* p2 must have a single parent, p3 must have no parents */
|
||||
if (!p2->parents || p2->parents->next || (p3 && p3->parents))
|
||||
return -1;
|
||||
if (repo_parse_commit(r, p2->parents->item))
|
||||
return -1;
|
||||
/* p2^1 must equal p1 */
|
||||
if (!oideq(&p1->object.oid, &p2->parents->item->object.oid))
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void assert_stash_like(struct stash_info *info, const char *revision)
|
||||
{
|
||||
if (get_oidf(&info->b_commit, "%s^1", revision) ||
|
||||
@@ -169,6 +217,25 @@ static void assert_stash_like(struct stash_info *info, const char *revision)
|
||||
die(_("'%s' is not a stash-like commit"), revision);
|
||||
}
|
||||
|
||||
static int parse_stash_revision(struct strbuf *revision, const char *commit, int quiet)
|
||||
{
|
||||
strbuf_reset(revision);
|
||||
if (!commit) {
|
||||
if (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {
|
||||
if (!quiet)
|
||||
fprintf_ln(stderr, _("No stash entries found."));
|
||||
return -1;
|
||||
}
|
||||
|
||||
strbuf_addf(revision, "%s@{0}", ref_stash);
|
||||
} else if (strspn(commit, "0123456789") == strlen(commit)) {
|
||||
strbuf_addf(revision, "%s@{%s}", ref_stash, commit);
|
||||
} else {
|
||||
strbuf_addstr(revision, commit);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int get_stash_info(struct stash_info *info, int argc, const char **argv)
|
||||
{
|
||||
int ret;
|
||||
@@ -196,19 +263,11 @@ static int get_stash_info(struct stash_info *info, int argc, const char **argv)
|
||||
if (argc == 1)
|
||||
commit = argv[0];
|
||||
|
||||
if (!commit) {
|
||||
if (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {
|
||||
fprintf_ln(stderr, _("No stash entries found."));
|
||||
strbuf_init(&info->revision, 0);
|
||||
if (parse_stash_revision(&info->revision, commit, 0)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
strbuf_addf(&info->revision, "%s@{0}", ref_stash);
|
||||
} else if (strspn(commit, "0123456789") == strlen(commit)) {
|
||||
strbuf_addf(&info->revision, "%s@{%s}", ref_stash, commit);
|
||||
} else {
|
||||
strbuf_addstr(&info->revision, commit);
|
||||
}
|
||||
|
||||
revision = info->revision.buf;
|
||||
|
||||
if (repo_get_oid(the_repository, revision, &info->w_commit))
|
||||
@@ -1894,6 +1953,383 @@ static int save_stash(int argc, const char **argv, const char *prefix,
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int write_commit_with_parents(struct repository *r,
|
||||
struct object_id *out,
|
||||
const struct object_id *oid,
|
||||
struct commit_list *parents)
|
||||
{
|
||||
size_t author_len, committer_len;
|
||||
struct commit *this;
|
||||
const char *orig_author, *orig_committer;
|
||||
char *author = NULL, *committer = NULL;
|
||||
const char *buffer;
|
||||
unsigned long bufsize;
|
||||
const char *p;
|
||||
struct strbuf msg = STRBUF_INIT;
|
||||
int ret = 0;
|
||||
struct ident_split id;
|
||||
|
||||
this = lookup_commit_reference(r, oid);
|
||||
buffer = repo_get_commit_buffer(r, this, &bufsize);
|
||||
orig_author = find_commit_header(buffer, "author", &author_len);
|
||||
orig_committer = find_commit_header(buffer, "committer", &committer_len);
|
||||
|
||||
if (!orig_author || !orig_committer) {
|
||||
ret = error(_("cannot parse commit %s"), oid_to_hex(oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (split_ident_line(&id, orig_author, author_len) < 0 ||
|
||||
split_ident_line(&id, orig_committer, committer_len) < 0) {
|
||||
ret = error(_("invalid author or committer for %s"), oid_to_hex(oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
p = strstr(buffer, "\n\n");
|
||||
strbuf_addstr(&msg, "git stash: ");
|
||||
|
||||
if (p)
|
||||
strbuf_add(&msg, p + 2, bufsize - (p + 2 - buffer));
|
||||
strbuf_complete_line(&msg);
|
||||
|
||||
author = xmemdupz(orig_author, author_len);
|
||||
committer = xmemdupz(orig_committer, committer_len);
|
||||
|
||||
if (commit_tree_extended(msg.buf, msg.len,
|
||||
r->hash_algo->empty_tree, parents,
|
||||
out, author, committer,
|
||||
NULL, NULL)) {
|
||||
ret = error(_("could not write commit"));
|
||||
goto out;
|
||||
}
|
||||
out:
|
||||
strbuf_release(&msg);
|
||||
repo_unuse_commit_buffer(r, this, buffer);
|
||||
free(author);
|
||||
free(committer);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int do_import_stash(struct repository *r, const char *rev)
|
||||
{
|
||||
struct object_id chain;
|
||||
int res = 0;
|
||||
const char *buffer = NULL;
|
||||
unsigned long bufsize;
|
||||
struct commit *this = NULL;
|
||||
struct commit_list *items = NULL, *cur;
|
||||
char *msg = NULL;
|
||||
|
||||
if (repo_get_oid(r, rev, &chain))
|
||||
return error(_("not a valid revision: %s"), rev);
|
||||
|
||||
this = lookup_commit_reference(r, &chain);
|
||||
if (!this)
|
||||
return error(_("not a commit: %s"), rev);
|
||||
|
||||
/*
|
||||
* Walk the commit history, finding each stash entry, and load data into
|
||||
* the array.
|
||||
*/
|
||||
for (;;) {
|
||||
const char *author, *committer;
|
||||
size_t author_len, committer_len;
|
||||
const char *p;
|
||||
const char *expected = "git stash <git@stash> 1000684800 +0000";
|
||||
const char *prefix = "git stash: ";
|
||||
struct commit *stash;
|
||||
struct tree *tree = repo_get_commit_tree(r, this);
|
||||
|
||||
if (!tree ||
|
||||
!oideq(&tree->object.oid, r->hash_algo->empty_tree) ||
|
||||
(this->parents &&
|
||||
(!this->parents->next || this->parents->next->next))) {
|
||||
res = error(_("%s is not a valid exported stash commit"),
|
||||
oid_to_hex(&this->object.oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
buffer = repo_get_commit_buffer(r, this, &bufsize);
|
||||
|
||||
if (!this->parents) {
|
||||
/*
|
||||
* We don't have any parents. Make sure this is our
|
||||
* root commit.
|
||||
*/
|
||||
author = find_commit_header(buffer, "author", &author_len);
|
||||
committer = find_commit_header(buffer, "committer", &committer_len);
|
||||
|
||||
if (!author || !committer) {
|
||||
error(_("cannot parse commit %s"), oid_to_hex(&this->object.oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (author_len != strlen(expected) ||
|
||||
committer_len != strlen(expected) ||
|
||||
memcmp(author, expected, author_len) ||
|
||||
memcmp(committer, expected, committer_len)) {
|
||||
res = error(_("found root commit %s with invalid data"), oid_to_hex(&this->object.oid));
|
||||
goto out;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
p = strstr(buffer, "\n\n");
|
||||
if (!p) {
|
||||
res = error(_("cannot parse commit %s"), oid_to_hex(&this->object.oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
p += 2;
|
||||
if (((size_t)(bufsize - (p - buffer)) < strlen(prefix)) ||
|
||||
memcmp(prefix, p, strlen(prefix))) {
|
||||
res = error(_("found stash commit %s without expected prefix"), oid_to_hex(&this->object.oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
stash = this->parents->next->item;
|
||||
|
||||
if (repo_parse_commit(r, this->parents->item) ||
|
||||
repo_parse_commit(r, stash)) {
|
||||
res = error(_("cannot parse parents of commit: %s"),
|
||||
oid_to_hex(&this->object.oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (check_stash_topology(r, stash)) {
|
||||
res = error(_("%s does not look like a stash commit"),
|
||||
oid_to_hex(&stash->object.oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
repo_unuse_commit_buffer(r, this, buffer);
|
||||
buffer = NULL;
|
||||
items = commit_list_insert(stash, &items);
|
||||
this = this->parents->item;
|
||||
}
|
||||
|
||||
/*
|
||||
* Now, walk each entry, adding it to the stash as a normal stash
|
||||
* commit.
|
||||
*/
|
||||
for (cur = items; cur; cur = cur->next) {
|
||||
const char *p;
|
||||
struct object_id *oid;
|
||||
|
||||
this = cur->item;
|
||||
oid = &this->object.oid;
|
||||
buffer = repo_get_commit_buffer(r, this, &bufsize);
|
||||
if (!buffer) {
|
||||
res = error(_("cannot read commit buffer for %s"), oid_to_hex(oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
p = strstr(buffer, "\n\n");
|
||||
if (!p) {
|
||||
res = error(_("cannot parse commit %s"), oid_to_hex(oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
p += 2;
|
||||
msg = xmemdupz(p, bufsize - (p - buffer));
|
||||
repo_unuse_commit_buffer(r, this, buffer);
|
||||
buffer = NULL;
|
||||
|
||||
if (do_store_stash(oid, msg, 1)) {
|
||||
res = error(_("cannot save the stash for %s"), oid_to_hex(oid));
|
||||
goto out;
|
||||
}
|
||||
FREE_AND_NULL(msg);
|
||||
}
|
||||
out:
|
||||
if (this && buffer)
|
||||
repo_unuse_commit_buffer(r, this, buffer);
|
||||
free_commit_list(items);
|
||||
free(msg);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static int import_stash(int argc, const char **argv, const char *prefix,
|
||||
struct repository *repo)
|
||||
{
|
||||
struct option options[] = {
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
argc = parse_options(argc, argv, prefix, options,
|
||||
git_stash_import_usage,
|
||||
PARSE_OPT_KEEP_DASHDASH);
|
||||
|
||||
if (argc != 1)
|
||||
usage_msg_opt("a revision is required", git_stash_import_usage, options);
|
||||
|
||||
return do_import_stash(repo, argv[0]);
|
||||
}
|
||||
|
||||
struct stash_entry_data {
|
||||
struct repository *r;
|
||||
struct commit_list **items;
|
||||
size_t count;
|
||||
};
|
||||
|
||||
static int collect_stash_entries(struct object_id *old_oid UNUSED,
|
||||
struct object_id *new_oid,
|
||||
const char *committer UNUSED,
|
||||
timestamp_t timestamp UNUSED,
|
||||
int tz UNUSED, const char *msg UNUSED,
|
||||
void *cb_data)
|
||||
{
|
||||
struct stash_entry_data *data = cb_data;
|
||||
struct commit *stash;
|
||||
|
||||
data->count++;
|
||||
stash = lookup_commit_reference(data->r, new_oid);
|
||||
if (!stash || check_stash_topology(data->r, stash)) {
|
||||
return error(_("%s does not look like a stash commit"),
|
||||
oid_to_hex(new_oid));
|
||||
}
|
||||
data->items = commit_list_append(stash, data->items);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int do_export_stash(struct repository *r,
|
||||
const char *ref,
|
||||
int argc,
|
||||
const char **argv)
|
||||
{
|
||||
struct object_id base;
|
||||
struct object_context unused;
|
||||
struct commit *prev;
|
||||
struct commit_list *items = NULL, **iter = &items, *cur;
|
||||
int res = 0;
|
||||
int i;
|
||||
struct strbuf revision = STRBUF_INIT;
|
||||
const char *author, *committer;
|
||||
|
||||
/*
|
||||
* This is an arbitrary, fixed date, specifically the one used by git
|
||||
* format-patch. The goal is merely to produce reproducible output.
|
||||
*/
|
||||
prepare_fallback_ident("git stash", "git@stash");
|
||||
author = fmt_ident("git stash", "git@stash", WANT_BLANK_IDENT,
|
||||
"2001-09-17T00:00:00Z", 0);
|
||||
committer = fmt_ident("git stash", "git@stash", WANT_BLANK_IDENT,
|
||||
"2001-09-17T00:00:00Z", 0);
|
||||
|
||||
/* First, we create a single empty commit. */
|
||||
if (commit_tree_extended("", 0, r->hash_algo->empty_tree, NULL,
|
||||
&base, author, committer, NULL, NULL))
|
||||
return error(_("unable to write base commit"));
|
||||
|
||||
prev = lookup_commit_reference(r, &base);
|
||||
|
||||
if (argc) {
|
||||
/*
|
||||
* Find each specified stash, and load data into the array.
|
||||
*/
|
||||
for (i = 0; i < argc; i++) {
|
||||
struct object_id oid;
|
||||
struct commit *stash;
|
||||
|
||||
if (parse_stash_revision(&revision, argv[i], 1) ||
|
||||
get_oid_with_context(r, revision.buf,
|
||||
GET_OID_QUIETLY | GET_OID_GENTLY,
|
||||
&oid, &unused)) {
|
||||
res = error(_("unable to find stash entry %s"), argv[i]);
|
||||
goto out;
|
||||
}
|
||||
|
||||
stash = lookup_commit_reference(r, &oid);
|
||||
if (!stash || check_stash_topology(r, stash)) {
|
||||
res = error(_("%s does not look like a stash commit"),
|
||||
revision.buf);
|
||||
goto out;
|
||||
}
|
||||
iter = commit_list_append(stash, iter);
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* Walk the reflog, finding each stash entry, and load data into the
|
||||
* array.
|
||||
*/
|
||||
struct stash_entry_data cb_data = {
|
||||
.r = r, .items = iter,
|
||||
};
|
||||
if (refs_for_each_reflog_ent_reverse(get_main_ref_store(r),
|
||||
"refs/stash",
|
||||
collect_stash_entries,
|
||||
&cb_data) && cb_data.count)
|
||||
goto out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Now, create a set of commits identical to the regular stash commits,
|
||||
* but where their first parents form a chain to our original empty
|
||||
* base commit.
|
||||
*/
|
||||
items = reverse_commit_list(items);
|
||||
for (cur = items; cur; cur = cur->next) {
|
||||
struct commit_list *parents = NULL;
|
||||
struct commit_list **next = &parents;
|
||||
struct object_id out;
|
||||
struct commit *stash = cur->item;
|
||||
|
||||
next = commit_list_append(prev, next);
|
||||
next = commit_list_append(stash, next);
|
||||
res = write_commit_with_parents(r, &out, &stash->object.oid, parents);
|
||||
free_commit_list(parents);
|
||||
if (res)
|
||||
goto out;
|
||||
prev = lookup_commit_reference(r, &out);
|
||||
}
|
||||
if (ref)
|
||||
refs_update_ref(get_main_ref_store(r), NULL, ref,
|
||||
&prev->object.oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR);
|
||||
else
|
||||
puts(oid_to_hex(&prev->object.oid));
|
||||
out:
|
||||
strbuf_release(&revision);
|
||||
free_commit_list(items);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
enum export_action {
|
||||
ACTION_NONE,
|
||||
ACTION_PRINT,
|
||||
ACTION_TO_REF,
|
||||
};
|
||||
|
||||
static int export_stash(int argc,
|
||||
const char **argv,
|
||||
const char *prefix,
|
||||
struct repository *repo)
|
||||
{
|
||||
const char *ref = NULL;
|
||||
enum export_action action = ACTION_NONE;
|
||||
struct option options[] = {
|
||||
OPT_CMDMODE(0, "print", &action,
|
||||
N_("print the object ID instead of writing it to a ref"),
|
||||
ACTION_PRINT),
|
||||
OPT_STRING(0, "to-ref", &ref, "ref",
|
||||
N_("save the data to the given ref")),
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
argc = parse_options(argc, argv, prefix, options,
|
||||
git_stash_export_usage,
|
||||
PARSE_OPT_KEEP_DASHDASH);
|
||||
|
||||
if (ref && action == ACTION_NONE)
|
||||
action = ACTION_TO_REF;
|
||||
|
||||
if (action == ACTION_NONE || (ref && action == ACTION_PRINT))
|
||||
return error(_("exactly one of --print and --to-ref is required"));
|
||||
|
||||
return do_export_stash(repo, ref, argc, argv);
|
||||
}
|
||||
|
||||
int cmd_stash(int argc,
|
||||
const char **argv,
|
||||
const char *prefix,
|
||||
@@ -1914,6 +2350,8 @@ int cmd_stash(int argc,
|
||||
OPT_SUBCOMMAND("store", &fn, store_stash),
|
||||
OPT_SUBCOMMAND("create", &fn, create_stash),
|
||||
OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
|
||||
OPT_SUBCOMMAND("export", &fn, export_stash),
|
||||
OPT_SUBCOMMAND("import", &fn, import_stash),
|
||||
OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
1
hash.h
1
hash.h
@@ -216,6 +216,7 @@ struct object_id {
|
||||
#define GET_OID_REQUIRE_PATH 010000
|
||||
#define GET_OID_HASH_ANY 020000
|
||||
#define GET_OID_SKIP_AMBIGUITY_CHECK 040000
|
||||
#define GET_OID_GENTLY 0100000
|
||||
|
||||
#define GET_OID_DISAMBIGUATORS \
|
||||
(GET_OID_COMMIT | GET_OID_COMMITTISH | \
|
||||
|
||||
@@ -1081,13 +1081,17 @@ static int get_oid_basic(struct repository *r, const char *str, int len,
|
||||
* still fill in the oid with the "old" value,
|
||||
* which we can use.
|
||||
*/
|
||||
} else {
|
||||
} else if (!(flags & GET_OID_GENTLY)) {
|
||||
if (flags & GET_OID_QUIETLY) {
|
||||
exit(128);
|
||||
}
|
||||
die(_("log for '%.*s' only has %d entries"),
|
||||
len, str, co_cnt);
|
||||
}
|
||||
if (flags & GET_OID_GENTLY) {
|
||||
free(real_ref);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
101
t/t3903-stash.sh
101
t/t3903-stash.sh
@@ -11,6 +11,13 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
|
||||
. ./test-lib.sh
|
||||
. "$TEST_DIRECTORY"/lib-unique-files.sh
|
||||
|
||||
test_expect_success 'setup' '
|
||||
test_oid_cache <<-EOF
|
||||
export_base sha1:73c9bab443d1f88ac61aa533d2eeaaa15451239c
|
||||
export_base sha256:f210fa6346e3e2ce047bdb570426b17075980c1ac01fec8fc4b75bd3ab4bcfe4
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'usage on cmd and subcommand invalid option' '
|
||||
test_expect_code 129 git stash --invalid-option 2>usage &&
|
||||
grep "or: git stash" usage &&
|
||||
@@ -1434,6 +1441,100 @@ test_expect_success 'stash --keep-index --include-untracked with empty tree' '
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'stash export and import round-trip stashes' '
|
||||
git reset &&
|
||||
>untracked &&
|
||||
>tracked1 &&
|
||||
>tracked2 &&
|
||||
git add tracked* &&
|
||||
git stash -- &&
|
||||
>subdir/untracked &&
|
||||
>subdir/tracked1 &&
|
||||
>subdir/tracked2 &&
|
||||
git add subdir/tracked* &&
|
||||
git stash --include-untracked -- subdir/ &&
|
||||
git tag t-stash0 stash@{0} &&
|
||||
git tag t-stash1 stash@{1} &&
|
||||
simple=$(git stash export --print) &&
|
||||
git stash clear &&
|
||||
git stash import "$simple" &&
|
||||
test_cmp_rev stash@{0} t-stash0 &&
|
||||
test_cmp_rev stash@{1} t-stash1 &&
|
||||
git stash export --to-ref refs/heads/foo &&
|
||||
test_cmp_rev "$(test_oid empty_tree)" foo: &&
|
||||
test_cmp_rev "$(test_oid empty_tree)" foo^: &&
|
||||
test_cmp_rev t-stash0 foo^2 &&
|
||||
test_cmp_rev t-stash1 foo^^2 &&
|
||||
git log --first-parent --format="%s" refs/heads/foo >log &&
|
||||
grep "^git stash: " log >log2 &&
|
||||
test_line_count = 13 log2 &&
|
||||
git stash clear &&
|
||||
git stash import foo &&
|
||||
test_cmp_rev stash@{0} t-stash0 &&
|
||||
test_cmp_rev stash@{1} t-stash1
|
||||
'
|
||||
|
||||
test_expect_success 'stash import appends commits' '
|
||||
git log --format=oneline -g refs/stash >out &&
|
||||
cat out out >out2 &&
|
||||
git stash import refs/heads/foo &&
|
||||
git log --format=oneline -g refs/stash >actual &&
|
||||
test_line_count = $(wc -l <out2) actual
|
||||
'
|
||||
|
||||
test_expect_success 'stash export can accept specified stashes' '
|
||||
git stash clear &&
|
||||
git stash import foo &&
|
||||
git stash export --to-ref refs/heads/bar stash@{1} stash@{0} &&
|
||||
git stash clear &&
|
||||
git stash import refs/heads/bar &&
|
||||
test_cmp_rev stash@{1} t-stash0 &&
|
||||
test_cmp_rev stash@{0} t-stash1 &&
|
||||
git log --format=oneline -g refs/stash >actual &&
|
||||
test_line_count = 2 actual
|
||||
'
|
||||
|
||||
test_expect_success 'stash export rejects invalid arguments' '
|
||||
test_must_fail git stash export --print --to-ref refs/heads/invalid 2>err &&
|
||||
grep "exactly one of --print and --to-ref is required" err &&
|
||||
test_must_fail git stash export 2>err2 &&
|
||||
grep "exactly one of --print and --to-ref is required" err2
|
||||
'
|
||||
|
||||
test_expect_success 'stash can import and export zero stashes' '
|
||||
git stash clear &&
|
||||
git stash export --to-ref refs/heads/baz &&
|
||||
test_cmp_rev "$(test_oid empty_tree)" baz: &&
|
||||
test_cmp_rev "$(test_oid export_base)" baz &&
|
||||
test_must_fail git rev-parse baz^1 &&
|
||||
git stash import baz &&
|
||||
test_must_fail git rev-parse refs/stash
|
||||
'
|
||||
|
||||
test_expect_success 'stash rejects invalid attempts to import commits' '
|
||||
git stash import foo &&
|
||||
test_must_fail git stash import HEAD 2>output &&
|
||||
oid=$(git rev-parse HEAD) &&
|
||||
grep "$oid is not a valid exported stash commit" output &&
|
||||
test_cmp_rev stash@{0} t-stash0 &&
|
||||
|
||||
git checkout --orphan orphan &&
|
||||
git commit-tree $(test_oid empty_tree) -p "$oid" -p "$oid^" -m "" >fake-commit &&
|
||||
git update-ref refs/heads/orphan "$(cat fake-commit)" &&
|
||||
oid=$(git rev-parse HEAD) &&
|
||||
test_must_fail git stash import orphan 2>output &&
|
||||
grep "found stash commit $oid without expected prefix" output &&
|
||||
test_cmp_rev stash@{0} t-stash0 &&
|
||||
|
||||
git checkout --orphan orphan2 &&
|
||||
git commit-tree $(test_oid empty_tree) -m "" >fake-commit &&
|
||||
git update-ref refs/heads/orphan2 "$(cat fake-commit)" &&
|
||||
oid=$(git rev-parse HEAD) &&
|
||||
test_must_fail git stash import orphan2 2>output &&
|
||||
grep "found root commit $oid with invalid data" output &&
|
||||
test_cmp_rev stash@{0} t-stash0
|
||||
'
|
||||
|
||||
test_expect_success 'stash apply should succeed with unmodified file' '
|
||||
echo base >file &&
|
||||
git add file &&
|
||||
|
||||
Reference in New Issue
Block a user