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 [] 'git stash' store [(-m | --message) ] [-q | --quiet] +'git stash' export (--print | --to-ref ) [...] +'git stash' import 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 ) [...]:: + + 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 :: + + 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]. :: This option is only valid for `apply`, `branch`, `drop`, `pop`, - `show` commands. + `show`, and `export` commands. + A reference of the form `stash@{}`. When no `` 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] []") #define BUILTIN_STASH_CREATE_USAGE \ 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" @@ -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 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 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 &&