builtin/stash: provide a way to import stashes from a ref

Now that we have a way to export stashes to a ref, let's provide a way to import them from such a ref back to the stash. This works much the way the export code does, except that we strip off the first parent chain commit and then store each resulting commit back to the stash. We don't clear the stash first and instead add the specified stashes to the top of the stash. This is because users may want to export just a few stashes, such as to share a small amount of work in progress with a colleague, and it would be undesirable for the receiving user to lose all of their data. For users who do want to replace the stash, it's easy to do to: simply run "git stash clear" first. We specifically rely on the fact that we'll produce identical stash commits on both sides in our tests. This provides a cheap, straightforward check for our tests and also makes it easy for users to see if they already have the same data in both repositories. Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

brian m. carlson committed Jun 12, 2025 at 01:12 UTC bc303718cc288b54233b204ce88223a16fb38487
3 files changed +275
Documentation/git-stash.adoc
+7
@@ -24,6 +24,7 @@ SYNOPSIS
24 'git stash' create [<message>]
25 'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>
26 'git stash' export (--print | --to-ref <ref>) [<stash>...]
27 +'git stash' import <commit>
28
29 DESCRIPTION
30 -----------
@@ -161,6 +162,12 @@ export ( --print | --to-ref <ref> ) [<stash>...]::
162 a chain of commits which can be transferred using the normal fetch and
163 push mechanisms, then imported using the `import` subcommand.
164
165 +import <commit>::
166 +
167 + Import the specified stashes from the specified commit, which must have been
168 + created by `export`, and add them to the list of stashes. To replace the
169 + existing stashes, use `clear` first.
170 +
171 OPTIONS
172 -------
173 -a::
builtin/stash.c
+167
@@ -61,6 +61,8 @@
61 N_("git stash create [<message>]")
62 #define BUILTIN_STASH_EXPORT_USAGE \
63 N_("git stash export (--print | --to-ref <ref>) [<stash>...]")
64 +#define BUILTIN_STASH_IMPORT_USAGE \
65 + N_("git stash import <commit>")
66 #define BUILTIN_STASH_CLEAR_USAGE \
67 "git stash clear"
68
@@ -77,6 +79,7 @@ static const char * const git_stash_usage[] = {
79 BUILTIN_STASH_CREATE_USAGE,
80 BUILTIN_STASH_STORE_USAGE,
81 BUILTIN_STASH_EXPORT_USAGE,
82 + BUILTIN_STASH_IMPORT_USAGE,
83 NULL
84 };
85
@@ -135,6 +138,10 @@ static const char * const git_stash_export_usage[] = {
138 NULL
139 };
140
141 +static const char * const git_stash_import_usage[] = {
142 + BUILTIN_STASH_IMPORT_USAGE,
143 + NULL
144 +};
145
146 static const char ref_stash[] = "refs/stash";
147 static struct strbuf stash_index_path = STRBUF_INIT;
@@ -144,6 +151,7 @@ static struct strbuf stash_index_path = STRBUF_INIT;
151 * b_commit is set to the base commit
152 * i_commit is set to the commit containing the index tree
153 * u_commit is set to the commit containing the untracked files tree
154 + * c_commit is set to the first parent (chain commit) when importing and is otherwise unset
155 * w_tree is set to the working tree
156 * b_tree is set to the base tree
157 * i_tree is set to the index tree
@@ -154,6 +162,7 @@ struct stash_info {
162 struct object_id b_commit;
163 struct object_id i_commit;
164 struct object_id u_commit;
165 + struct object_id c_commit;
166 struct object_id w_tree;
167 struct object_id b_tree;
168 struct object_id i_tree;
@@ -1991,6 +2000,163 @@ out:
2000 return ret;
2001 }
2002
2003 +static int do_import_stash(struct repository *r, const char *rev)
2004 +{
2005 + struct object_id chain;
2006 + int res = 0;
2007 + const char *buffer = NULL;
2008 + unsigned long bufsize;
2009 + struct commit *this = NULL;
2010 + struct commit_list *items = NULL, *cur;
2011 + char *msg = NULL;
2012 +
2013 + if (repo_get_oid(r, rev, &chain))
2014 + return error(_("not a valid revision: %s"), rev);
2015 +
2016 + this = lookup_commit_reference(r, &chain);
2017 + if (!this)
2018 + return error(_("not a commit: %s"), rev);
2019 +
2020 + /*
2021 + * Walk the commit history, finding each stash entry, and load data into
2022 + * the array.
2023 + */
2024 + for (;;) {
2025 + const char *author, *committer;
2026 + size_t author_len, committer_len;
2027 + const char *p;
2028 + const char *expected = "git stash <git@stash> 1000684800 +0000";
2029 + const char *prefix = "git stash: ";
2030 + struct commit *stash;
2031 + struct tree *tree = repo_get_commit_tree(r, this);
2032 +
2033 + if (!tree ||
2034 + !oideq(&tree->object.oid, r->hash_algo->empty_tree) ||
2035 + (this->parents &&
2036 + (!this->parents->next || this->parents->next->next))) {
2037 + res = error(_("%s is not a valid exported stash commit"),
2038 + oid_to_hex(&this->object.oid));
2039 + goto out;
2040 + }
2041 +
2042 + buffer = repo_get_commit_buffer(r, this, &bufsize);
2043 +
2044 + if (!this->parents) {
2045 + /*
2046 + * We don't have any parents. Make sure this is our
2047 + * root commit.
2048 + */
2049 + author = find_commit_header(buffer, "author", &author_len);
2050 + committer = find_commit_header(buffer, "committer", &committer_len);
2051 +
2052 + if (!author || !committer) {
2053 + error(_("cannot parse commit %s"), oid_to_hex(&this->object.oid));
2054 + goto out;
2055 + }
2056 +
2057 + if (author_len != strlen(expected) ||
2058 + committer_len != strlen(expected) ||
2059 + memcmp(author, expected, author_len) ||
2060 + memcmp(committer, expected, committer_len)) {
2061 + res = error(_("found root commit %s with invalid data"), oid_to_hex(&this->object.oid));
2062 + goto out;
2063 + }
2064 + break;
2065 + }
2066 +
2067 + p = strstr(buffer, "\n\n");
2068 + if (!p) {
2069 + res = error(_("cannot parse commit %s"), oid_to_hex(&this->object.oid));
2070 + goto out;
2071 + }
2072 +
2073 + p += 2;
2074 + if (((size_t)(bufsize - (p - buffer)) < strlen(prefix)) ||
2075 + memcmp(prefix, p, strlen(prefix))) {
2076 + res = error(_("found stash commit %s without expected prefix"), oid_to_hex(&this->object.oid));
2077 + goto out;
2078 + }
2079 +
2080 + stash = this->parents->next->item;
2081 +
2082 + if (repo_parse_commit(r, this->parents->item) ||
2083 + repo_parse_commit(r, stash)) {
2084 + res = error(_("cannot parse parents of commit: %s"),
2085 + oid_to_hex(&this->object.oid));
2086 + goto out;
2087 + }
2088 +
2089 + if (check_stash_topology(r, stash)) {
2090 + res = error(_("%s does not look like a stash commit"),
2091 + oid_to_hex(&stash->object.oid));
2092 + goto out;
2093 + }
2094 +
2095 + repo_unuse_commit_buffer(r, this, buffer);
2096 + buffer = NULL;
2097 + items = commit_list_insert(stash, &items);
2098 + this = this->parents->item;
2099 + }
2100 +
2101 + /*
2102 + * Now, walk each entry, adding it to the stash as a normal stash
2103 + * commit.
2104 + */
2105 + for (cur = items; cur; cur = cur->next) {
2106 + const char *p;
2107 + struct object_id *oid;
2108 +
2109 + this = cur->item;
2110 + oid = &this->object.oid;
2111 + buffer = repo_get_commit_buffer(r, this, &bufsize);
2112 + if (!buffer) {
2113 + res = error(_("cannot read commit buffer for %s"), oid_to_hex(oid));
2114 + goto out;
2115 + }
2116 +
2117 + p = strstr(buffer, "\n\n");
2118 + if (!p) {
2119 + res = error(_("cannot parse commit %s"), oid_to_hex(oid));
2120 + goto out;
2121 + }
2122 +
2123 + p += 2;
2124 + msg = xmemdupz(p, bufsize - (p - buffer));
2125 + repo_unuse_commit_buffer(r, this, buffer);
2126 + buffer = NULL;
2127 +
2128 + if (do_store_stash(oid, msg, 1)) {
2129 + res = error(_("cannot save the stash for %s"), oid_to_hex(oid));
2130 + goto out;
2131 + }
2132 + FREE_AND_NULL(msg);
2133 + }
2134 +out:
2135 + if (this && buffer)
2136 + repo_unuse_commit_buffer(r, this, buffer);
2137 + free_commit_list(items);
2138 + free(msg);
2139 +
2140 + return res;
2141 +}
2142 +
2143 +static int import_stash(int argc, const char **argv, const char *prefix,
2144 + struct repository *repo)
2145 +{
2146 + struct option options[] = {
2147 + OPT_END()
2148 + };
2149 +
2150 + argc = parse_options(argc, argv, prefix, options,
2151 + git_stash_import_usage,
2152 + PARSE_OPT_KEEP_DASHDASH);
2153 +
2154 + if (argc != 1)
2155 + usage_msg_opt("a revision is required", git_stash_import_usage, options);
2156 +
2157 + return do_import_stash(repo, argv[0]);
2158 +}
2159 +
2160 struct stash_entry_data {
2161 struct repository *r;
2162 struct commit_list **items;
@@ -2175,6 +2341,7 @@ int cmd_stash(int argc,
2341 OPT_SUBCOMMAND("create", &fn, create_stash),
2342 OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
2343 OPT_SUBCOMMAND("export", &fn, export_stash),
2344 + OPT_SUBCOMMAND("import", &fn, import_stash),
2345 OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
2346 OPT_END()
2347 };
t/t3903-stash.sh
+101
@@ -11,6 +11,13 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
11 . ./test-lib.sh
12 . "$TEST_DIRECTORY"/lib-unique-files.sh
13
14 +test_expect_success 'setup' '
15 + test_oid_cache <<-EOF
16 + export_base sha1:73c9bab443d1f88ac61aa533d2eeaaa15451239c
17 + export_base sha256:f210fa6346e3e2ce047bdb570426b17075980c1ac01fec8fc4b75bd3ab4bcfe4
18 + EOF
19 +'
20 +
21 test_expect_success 'usage on cmd and subcommand invalid option' '
22 test_expect_code 129 git stash --invalid-option 2>usage &&
23 grep "or: git stash" usage &&
@@ -1412,6 +1419,100 @@ test_expect_success 'stash --keep-index --include-untracked with empty tree' '
1419 )
1420 '
1421
1422 +test_expect_success 'stash export and import round-trip stashes' '
1423 + git reset &&
1424 + >untracked &&
1425 + >tracked1 &&
1426 + >tracked2 &&
1427 + git add tracked* &&
1428 + git stash -- &&
1429 + >subdir/untracked &&
1430 + >subdir/tracked1 &&
1431 + >subdir/tracked2 &&
1432 + git add subdir/tracked* &&
1433 + git stash --include-untracked -- subdir/ &&
1434 + git tag t-stash0 stash@{0} &&
1435 + git tag t-stash1 stash@{1} &&
1436 + simple=$(git stash export --print) &&
1437 + git stash clear &&
1438 + git stash import "$simple" &&
1439 + test_cmp_rev stash@{0} t-stash0 &&
1440 + test_cmp_rev stash@{1} t-stash1 &&
1441 + git stash export --to-ref refs/heads/foo &&
1442 + test_cmp_rev "$(test_oid empty_tree)" foo: &&
1443 + test_cmp_rev "$(test_oid empty_tree)" foo^: &&
1444 + test_cmp_rev t-stash0 foo^2 &&
1445 + test_cmp_rev t-stash1 foo^^2 &&
1446 + git log --first-parent --format="%s" refs/heads/foo >log &&
1447 + grep "^git stash: " log >log2 &&
1448 + test_line_count = 13 log2 &&
1449 + git stash clear &&
1450 + git stash import foo &&
1451 + test_cmp_rev stash@{0} t-stash0 &&
1452 + test_cmp_rev stash@{1} t-stash1
1453 +'
1454 +
1455 +test_expect_success 'stash import appends commits' '
1456 + git log --format=oneline -g refs/stash >out &&
1457 + cat out out >out2 &&
1458 + git stash import refs/heads/foo &&
1459 + git log --format=oneline -g refs/stash >actual &&
1460 + test_line_count = $(wc -l <out2) actual
1461 +'
1462 +
1463 +test_expect_success 'stash export can accept specified stashes' '
1464 + git stash clear &&
1465 + git stash import foo &&
1466 + git stash export --to-ref refs/heads/bar stash@{1} stash@{0} &&
1467 + git stash clear &&
1468 + git stash import refs/heads/bar &&
1469 + test_cmp_rev stash@{1} t-stash0 &&
1470 + test_cmp_rev stash@{0} t-stash1 &&
1471 + git log --format=oneline -g refs/stash >actual &&
1472 + test_line_count = 2 actual
1473 +'
1474 +
1475 +test_expect_success 'stash export rejects invalid arguments' '
1476 + test_must_fail git stash export --print --to-ref refs/heads/invalid 2>err &&
1477 + grep "exactly one of --print and --to-ref is required" err &&
1478 + test_must_fail git stash export 2>err2 &&
1479 + grep "exactly one of --print and --to-ref is required" err2
1480 +'
1481 +
1482 +test_expect_success 'stash can import and export zero stashes' '
1483 + git stash clear &&
1484 + git stash export --to-ref refs/heads/baz &&
1485 + test_cmp_rev "$(test_oid empty_tree)" baz: &&
1486 + test_cmp_rev "$(test_oid export_base)" baz &&
1487 + test_must_fail git rev-parse baz^1 &&
1488 + git stash import baz &&
1489 + test_must_fail git rev-parse refs/stash
1490 +'
1491 +
1492 +test_expect_success 'stash rejects invalid attempts to import commits' '
1493 + git stash import foo &&
1494 + test_must_fail git stash import HEAD 2>output &&
1495 + oid=$(git rev-parse HEAD) &&
1496 + grep "$oid is not a valid exported stash commit" output &&
1497 + test_cmp_rev stash@{0} t-stash0 &&
1498 +
1499 + git checkout --orphan orphan &&
1500 + git commit-tree $(test_oid empty_tree) -p "$oid" -p "$oid^" -m "" >fake-commit &&
1501 + git update-ref refs/heads/orphan "$(cat fake-commit)" &&
1502 + oid=$(git rev-parse HEAD) &&
1503 + test_must_fail git stash import orphan 2>output &&
1504 + grep "found stash commit $oid without expected prefix" output &&
1505 + test_cmp_rev stash@{0} t-stash0 &&
1506 +
1507 + git checkout --orphan orphan2 &&
1508 + git commit-tree $(test_oid empty_tree) -m "" >fake-commit &&
1509 + git update-ref refs/heads/orphan2 "$(cat fake-commit)" &&
1510 + oid=$(git rev-parse HEAD) &&
1511 + test_must_fail git stash import orphan2 2>output &&
1512 + grep "found root commit $oid with invalid data" output &&
1513 + test_cmp_rev stash@{0} t-stash0
1514 +'
1515 +
1516 test_expect_success 'stash apply should succeed with unmodified file' '
1517 echo base >file &&
1518 git add file &&