aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/git-stash.adoc29
-rw-r--r--builtin/stash.c460
-rw-r--r--hash.h1
-rw-r--r--object-name.c6
-rwxr-xr-xt/t3903-stash.sh101
5 files changed, 584 insertions, 13 deletions
diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc
index 1a5177f498..e5e6c9d37f 100644
--- a/Documentation/git-stash.adoc
+++ b/Documentation/git-stash.adoc
@@ -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}`).
diff --git a/builtin/stash.c b/builtin/stash.c
index 7cd3ad8aa4..e2f95cc2eb 100644
--- a/builtin/stash.c
+++ b/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,17 +263,9 @@ 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."));
- 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);
+ strbuf_init(&info->revision, 0);
+ if (parse_stash_revision(&info->revision, commit, 0)) {
+ return -1;
}
revision = info->revision.buf;
@@ -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()
};
diff --git a/hash.h b/hash.h
index d6422ddf45..ec594c63a6 100644
--- a/hash.h
+++ b/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 | \
diff --git a/object-name.c b/object-name.c
index 9288b2dd24..851858975f 100644
--- a/object-name.c
+++ b/object-name.c
@@ -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;
+ }
}
}
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 35b85c7909..c58ccb136c 100755
--- a/t/t3903-stash.sh
+++ b/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 &&