Skip to content

Commit 27c0be9

Browse files
bk2204gitster
authored andcommitted
builtin/stash: provide a way to export stashes to a ref
A common user problem is how to sync in-progress work to another machine. Users currently must use some sort of transfer of the working tree, which poses security risks and also necessarily causes the index to become dirty. The experience is suboptimal and frustrating for users. A reasonable idea is to use the stash for this purpose, but the stash is stored in the reflog, not in a ref, and as such it cannot be pushed or pulled. This also means that it cannot be saved into a bundle or preserved elsewhere, which is a problem when using throwaway development environments. In addition, users often want to replicate stashes across machines, such as when they must use multiple machines or when they use throwaway dev environments, such as those based on the Devcontainer spec, where they might otherwise lose various in-progress work. Let's solve this problem by allowing the user to export the stash to a ref (or, to just write it into the repository and print the hash, à la git commit-tree). Introduce git stash export, which writes a chain of commits where the first parent is always a chain to the previous stash, or to a single, empty commit (for the final item) and the second is the stash commit normally written to the reflog. Iterate over each stash from top to bottom, looking up the data for each one, and then create the chain from the single empty commit back up in reverse order. Generate a predictable empty commit so our behavior is reproducible. Create a useful commit message, preserving the author and committer information, to help users identify stash commits when viewing them as normal commits. If the user has specified specific stashes they'd like to export instead, use those instead of iterating over all of the stashes. As part of this, specifically request quiet behavior when looking up the OID for a revision because we will eventually hit a revision that doesn't exist and we don't want to die when that occurs. When exporting stashes, be sure to verify that they look like valid stashes and don't contain invalid data. This will help avoid failures on import or problems due to attempting to export invalid refs that are not stashes. Signed-off-by: Phillip Wood <[email protected]> Signed-off-by: brian m. carlson <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 7572e59 commit 27c0be9

File tree

2 files changed

+281
-1
lines changed

2 files changed

+281
-1
lines changed

Documentation/git-stash.adoc

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ SYNOPSIS
2323
'git stash' clear
2424
'git stash' create [<message>]
2525
'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>
26+
'git stash' export (--print | --to-ref <ref>) [<stash>...]
2627

2728
DESCRIPTION
2829
-----------
@@ -154,6 +155,12 @@ store::
154155
reflog. This is intended to be useful for scripts. It is
155156
probably not the command you want to use; see "push" above.
156157

158+
export ( --print | --to-ref <ref> ) [<stash>...]::
159+
160+
Export the specified stashes, or all of them if none are specified, to
161+
a chain of commits which can be transferred using the normal fetch and
162+
push mechanisms, then imported using the `import` subcommand.
163+
157164
OPTIONS
158165
-------
159166
-a::
@@ -242,6 +249,19 @@ literally (including newlines and quotes).
242249
+
243250
Quiet, suppress feedback messages.
244251
252+
--print::
253+
This option is only valid for the `export` command.
254+
+
255+
Create the chain of commits representing the exported stashes without
256+
storing it anywhere in the ref namespace and print the object ID to
257+
standard output. This is designed for scripts.
258+
259+
--to-ref::
260+
This option is only valid for the `export` command.
261+
+
262+
Create the chain of commits representing the exported stashes and store
263+
it to the specified ref.
264+
245265
\--::
246266
This option is only valid for `push` command.
247267
+
@@ -259,7 +279,7 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
259279
260280
<stash>::
261281
This option is only valid for `apply`, `branch`, `drop`, `pop`,
262-
`show` commands.
282+
`show`, and `export` commands.
263283
+
264284
A reference of the form `stash@{<revision>}`. When no `<stash>` is
265285
given, the latest stash is assumed (that is, `stash@{0}`).

builtin/stash.c

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
#include "log-tree.h"
2929
#include "diffcore.h"
3030
#include "reflog.h"
31+
#include "reflog-walk.h"
3132
#include "add-interactive.h"
33+
#include "oid-array.h"
34+
#include "commit.h"
3235

3336
#define INCLUDE_ALL_FILES 2
3437

@@ -56,6 +59,8 @@
5659
" [-u | --include-untracked] [-a | --all] [<message>]")
5760
#define BUILTIN_STASH_CREATE_USAGE \
5861
N_("git stash create [<message>]")
62+
#define BUILTIN_STASH_EXPORT_USAGE \
63+
N_("git stash export (--print | --to-ref <ref>) [<stash>...]")
5964
#define BUILTIN_STASH_CLEAR_USAGE \
6065
"git stash clear"
6166

@@ -71,6 +76,7 @@ static const char * const git_stash_usage[] = {
7176
BUILTIN_STASH_CLEAR_USAGE,
7277
BUILTIN_STASH_CREATE_USAGE,
7378
BUILTIN_STASH_STORE_USAGE,
79+
BUILTIN_STASH_EXPORT_USAGE,
7480
NULL
7581
};
7682

@@ -124,6 +130,12 @@ static const char * const git_stash_save_usage[] = {
124130
NULL
125131
};
126132

133+
static const char * const git_stash_export_usage[] = {
134+
BUILTIN_STASH_EXPORT_USAGE,
135+
NULL
136+
};
137+
138+
127139
static const char ref_stash[] = "refs/stash";
128140
static struct strbuf stash_index_path = STRBUF_INIT;
129141

@@ -160,6 +172,33 @@ static void free_stash_info(struct stash_info *info)
160172
strbuf_release(&info->revision);
161173
}
162174

175+
static int check_stash_topology(struct repository *r, struct commit *stash)
176+
{
177+
struct commit *p1, *p2, *p3 = NULL;
178+
179+
/* stash must have two or three parents */
180+
if (!stash->parents || !stash->parents->next ||
181+
(stash->parents->next->next && stash->parents->next->next->next))
182+
return -1;
183+
p1 = stash->parents->item;
184+
p2 = stash->parents->next->item;
185+
if (stash->parents->next->next)
186+
p3 = stash->parents->next->next->item;
187+
if (repo_parse_commit(r, p1) || repo_parse_commit(r, p2) ||
188+
(p3 && repo_parse_commit(r, p3)))
189+
return -1;
190+
/* p2 must have a single parent, p3 must have no parents */
191+
if (!p2->parents || p2->parents->next || (p3 && p3->parents))
192+
return -1;
193+
if (repo_parse_commit(r, p2->parents->item))
194+
return -1;
195+
/* p2^1 must equal p1 */
196+
if (!oideq(&p1->object.oid, &p2->parents->item->object.oid))
197+
return -1;
198+
199+
return 0;
200+
}
201+
163202
static void assert_stash_like(struct stash_info *info, const char *revision)
164203
{
165204
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,
18951934
return ret;
18961935
}
18971936

1937+
static int write_commit_with_parents(struct repository *r,
1938+
struct object_id *out,
1939+
const struct object_id *oid,
1940+
struct commit_list *parents)
1941+
{
1942+
size_t author_len, committer_len;
1943+
struct commit *this;
1944+
const char *orig_author, *orig_committer;
1945+
char *author = NULL, *committer = NULL;
1946+
const char *buffer;
1947+
unsigned long bufsize;
1948+
const char *p;
1949+
struct strbuf msg = STRBUF_INIT;
1950+
int ret = 0;
1951+
struct ident_split id;
1952+
1953+
this = lookup_commit_reference(r, oid);
1954+
buffer = repo_get_commit_buffer(r, this, &bufsize);
1955+
orig_author = find_commit_header(buffer, "author", &author_len);
1956+
orig_committer = find_commit_header(buffer, "committer", &committer_len);
1957+
1958+
if (!orig_author || !orig_committer) {
1959+
ret = error(_("cannot parse commit %s"), oid_to_hex(oid));
1960+
goto out;
1961+
}
1962+
1963+
if (split_ident_line(&id, orig_author, author_len) < 0 ||
1964+
split_ident_line(&id, orig_committer, committer_len) < 0) {
1965+
ret = error(_("invalid author or committer for %s"), oid_to_hex(oid));
1966+
goto out;
1967+
}
1968+
1969+
p = strstr(buffer, "\n\n");
1970+
strbuf_addstr(&msg, "git stash: ");
1971+
1972+
if (p)
1973+
strbuf_add(&msg, p + 2, bufsize - (p + 2 - buffer));
1974+
strbuf_complete_line(&msg);
1975+
1976+
author = xmemdupz(orig_author, author_len);
1977+
committer = xmemdupz(orig_committer, committer_len);
1978+
1979+
if (commit_tree_extended(msg.buf, msg.len,
1980+
r->hash_algo->empty_tree, parents,
1981+
out, author, committer,
1982+
NULL, NULL)) {
1983+
ret = error(_("could not write commit"));
1984+
goto out;
1985+
}
1986+
out:
1987+
strbuf_release(&msg);
1988+
repo_unuse_commit_buffer(r, this, buffer);
1989+
free(author);
1990+
free(committer);
1991+
return ret;
1992+
}
1993+
1994+
struct stash_entry_data {
1995+
struct repository *r;
1996+
struct commit_list **items;
1997+
size_t count;
1998+
};
1999+
2000+
static int collect_stash_entries(struct object_id *old_oid UNUSED,
2001+
struct object_id *new_oid,
2002+
const char *committer UNUSED,
2003+
timestamp_t timestamp UNUSED,
2004+
int tz UNUSED, const char *msg UNUSED,
2005+
void *cb_data)
2006+
{
2007+
struct stash_entry_data *data = cb_data;
2008+
struct commit *stash;
2009+
2010+
data->count++;
2011+
stash = lookup_commit_reference(data->r, new_oid);
2012+
if (!stash || check_stash_topology(data->r, stash)) {
2013+
return error(_("%s does not look like a stash commit"),
2014+
oid_to_hex(new_oid));
2015+
}
2016+
data->items = commit_list_append(stash, data->items);
2017+
return 0;
2018+
}
2019+
2020+
static int do_export_stash(struct repository *r,
2021+
const char *ref,
2022+
int argc,
2023+
const char **argv)
2024+
{
2025+
struct object_id base;
2026+
struct object_context unused;
2027+
struct commit *prev;
2028+
struct commit_list *items = NULL, **iter = &items, *cur;
2029+
int res = 0;
2030+
int i;
2031+
struct strbuf revision = STRBUF_INIT;
2032+
const char *author, *committer;
2033+
2034+
/*
2035+
* This is an arbitrary, fixed date, specifically the one used by git
2036+
* format-patch. The goal is merely to produce reproducible output.
2037+
*/
2038+
prepare_fallback_ident("git stash", "git@stash");
2039+
author = fmt_ident("git stash", "git@stash", WANT_BLANK_IDENT,
2040+
"2001-09-17T00:00:00Z", 0);
2041+
committer = fmt_ident("git stash", "git@stash", WANT_BLANK_IDENT,
2042+
"2001-09-17T00:00:00Z", 0);
2043+
2044+
/* First, we create a single empty commit. */
2045+
if (commit_tree_extended("", 0, r->hash_algo->empty_tree, NULL,
2046+
&base, author, committer, NULL, NULL))
2047+
return error(_("unable to write base commit"));
2048+
2049+
prev = lookup_commit_reference(r, &base);
2050+
2051+
if (argc) {
2052+
/*
2053+
* Find each specified stash, and load data into the array.
2054+
*/
2055+
for (i = 0; i < argc; i++) {
2056+
struct object_id oid;
2057+
struct commit *stash;
2058+
2059+
if (parse_stash_revision(&revision, argv[i], 1) ||
2060+
get_oid_with_context(r, revision.buf,
2061+
GET_OID_QUIETLY | GET_OID_GENTLY,
2062+
&oid, &unused)) {
2063+
res = error(_("unable to find stash entry %s"), argv[i]);
2064+
goto out;
2065+
}
2066+
2067+
stash = lookup_commit_reference(r, &oid);
2068+
if (!stash || check_stash_topology(r, stash)) {
2069+
res = error(_("%s does not look like a stash commit"),
2070+
revision.buf);
2071+
goto out;
2072+
}
2073+
iter = commit_list_append(stash, iter);
2074+
}
2075+
} else {
2076+
/*
2077+
* Walk the reflog, finding each stash entry, and load data into the
2078+
* array.
2079+
*/
2080+
struct stash_entry_data cb_data = {
2081+
.r = r, .items = iter,
2082+
};
2083+
if (refs_for_each_reflog_ent_reverse(get_main_ref_store(r),
2084+
"refs/stash",
2085+
collect_stash_entries,
2086+
&cb_data) && cb_data.count)
2087+
goto out;
2088+
}
2089+
2090+
/*
2091+
* Now, create a set of commits identical to the regular stash commits,
2092+
* but where their first parents form a chain to our original empty
2093+
* base commit.
2094+
*/
2095+
items = reverse_commit_list(items);
2096+
for (cur = items; cur; cur = cur->next) {
2097+
struct commit_list *parents = NULL;
2098+
struct commit_list **next = &parents;
2099+
struct object_id out;
2100+
struct commit *stash = cur->item;
2101+
2102+
next = commit_list_append(prev, next);
2103+
next = commit_list_append(stash, next);
2104+
res = write_commit_with_parents(r, &out, &stash->object.oid, parents);
2105+
free_commit_list(parents);
2106+
if (res)
2107+
goto out;
2108+
prev = lookup_commit_reference(r, &out);
2109+
}
2110+
if (ref)
2111+
refs_update_ref(get_main_ref_store(r), NULL, ref,
2112+
&prev->object.oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR);
2113+
else
2114+
puts(oid_to_hex(&prev->object.oid));
2115+
out:
2116+
strbuf_release(&revision);
2117+
free_commit_list(items);
2118+
2119+
return res;
2120+
}
2121+
2122+
enum export_action {
2123+
ACTION_NONE,
2124+
ACTION_PRINT,
2125+
ACTION_TO_REF,
2126+
};
2127+
2128+
static int export_stash(int argc,
2129+
const char **argv,
2130+
const char *prefix,
2131+
struct repository *repo)
2132+
{
2133+
const char *ref = NULL;
2134+
enum export_action action = ACTION_NONE;
2135+
struct option options[] = {
2136+
OPT_CMDMODE(0, "print", &action,
2137+
N_("print the object ID instead of writing it to a ref"),
2138+
ACTION_PRINT),
2139+
OPT_STRING(0, "to-ref", &ref, "ref",
2140+
N_("save the data to the given ref")),
2141+
OPT_END()
2142+
};
2143+
2144+
argc = parse_options(argc, argv, prefix, options,
2145+
git_stash_export_usage,
2146+
PARSE_OPT_KEEP_DASHDASH);
2147+
2148+
if (ref && action == ACTION_NONE)
2149+
action = ACTION_TO_REF;
2150+
2151+
if (action == ACTION_NONE || (ref && action == ACTION_PRINT))
2152+
return error(_("exactly one of --print and --to-ref is required"));
2153+
2154+
return do_export_stash(repo, ref, argc, argv);
2155+
}
2156+
18982157
int cmd_stash(int argc,
18992158
const char **argv,
19002159
const char *prefix,
@@ -1915,6 +2174,7 @@ int cmd_stash(int argc,
19152174
OPT_SUBCOMMAND("store", &fn, store_stash),
19162175
OPT_SUBCOMMAND("create", &fn, create_stash),
19172176
OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
2177+
OPT_SUBCOMMAND("export", &fn, export_stash),
19182178
OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
19192179
OPT_END()
19202180
};

0 commit comments

Comments
 (0)