diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc index 0aef0a5b86..e5e6c9d37f 100644 --- a/Documentation/git-stash.adoc +++ b/Documentation/git-stash.adoc @@ -24,6 +24,7 @@ SYNOPSIS 'git stash' create [] 'git stash' store [(-m | --message) ] [-q | --quiet] 'git stash' export (--print | --to-ref ) [...] +'git stash' import DESCRIPTION ----------- @@ -161,6 +162,12 @@ export ( --print | --to-ref ) [...]:: a chain of commits which can be transferred using the normal fetch and push mechanisms, then imported using the `import` subcommand. +import :: + + 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:: diff --git a/builtin/stash.c b/builtin/stash.c index 192b0d9969..0e77fa94ee 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -61,6 +61,8 @@ N_("git stash create []") #define BUILTIN_STASH_EXPORT_USAGE \ N_("git stash export (--print | --to-ref ) [...]") +#define BUILTIN_STASH_IMPORT_USAGE \ + N_("git stash import ") #define BUILTIN_STASH_CLEAR_USAGE \ "git stash clear" @@ -77,6 +79,7 @@ static const char * const git_stash_usage[] = { BUILTIN_STASH_CREATE_USAGE, BUILTIN_STASH_STORE_USAGE, BUILTIN_STASH_EXPORT_USAGE, + BUILTIN_STASH_IMPORT_USAGE, NULL }; @@ -135,6 +138,10 @@ static const char * const git_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; @@ -144,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 @@ -154,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; @@ -1991,6 +2000,163 @@ out: 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 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; @@ -2175,6 +2341,7 @@ int cmd_stash(int argc, 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/t/t3903-stash.sh b/t/t3903-stash.sh index 74666ff3e4..12d30a9865 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 && @@ -1412,6 +1419,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 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 &&