diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc index 1a5177f498..0aef0a5b86 100644 --- a/Documentation/git-stash.adoc +++ b/Documentation/git-stash.adoc @@ -23,6 +23,7 @@ SYNOPSIS 'git stash' clear 'git stash' create [] 'git stash' store [(-m | --message) ] [-q | --quiet] +'git stash' export (--print | --to-ref ) [...] DESCRIPTION ----------- @@ -154,6 +155,12 @@ 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. + OPTIONS ------- -a:: @@ -242,6 +249,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 +279,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 ab491d5ff6..192b0d9969 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,8 @@ " [-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_CLEAR_USAGE \ "git stash clear" @@ -71,6 +76,7 @@ static const char * const git_stash_usage[] = { BUILTIN_STASH_CLEAR_USAGE, BUILTIN_STASH_CREATE_USAGE, BUILTIN_STASH_STORE_USAGE, + BUILTIN_STASH_EXPORT_USAGE, NULL }; @@ -124,6 +130,12 @@ 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 ref_stash[] = "refs/stash"; static struct strbuf stash_index_path = STRBUF_INIT; @@ -160,6 +172,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) || @@ -1895,6 +1934,226 @@ 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; +} + +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, @@ -1915,6 +2174,7 @@ 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_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE), OPT_END() };