{"thread":{"id":"63424","subject":"[PATCH v5 2/4] builtin/stash: factor out revision parsing into a function","startedAt":"2025-05-08T23:45:10Z","lastAt":"2025-06-25T16:30:41Z","messageCount":72,"participants":["brian m. carlson","Junio C Hamano","D. Ben Knoble","Jeff King","Phillip Wood","Ramsay Jones","Kristoffer Haugsbakk"],"isPatch":true,"patchVersion":5,"patchTotal":4},"messages":[{"id":"517609","messageId":"20250508234458.3665894-3-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250508234458.3665894-1-sandals@crustytoothpaste.net","subject":"[PATCH v5 2/4] builtin/stash: factor out revision parsing into a function","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-08T23:44:55Z","receivedAt":"2025-05-08T23:45:10Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"We allow several special forms of stash names in this code.  In the\nfuture, we'll want to allow these same forms without parsing a stash\ncommit, so let's refactor this code out into a function for reuse.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n builtin/stash.c | 34 +++++++++++++++++++++++-----------\n 1 file changed, 23 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex cfbd92852a..8ee6752efa 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -169,6 +169,25 @@ static void assert_stash_like(struct stash_info *info, const char *revision)\n \t\tdie(_(\"'%s' is not a stash-like commit\"), revision);\n }\n \n+static int parse_revision(struct strbuf *revision, const char *commit, int quiet)\n+{\n+\tstrbuf_reset(revision);\n+\tif (!commit) {\n+\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n+\t\t\tif (!quiet)\n+\t\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tstrbuf_addf(revision, \"%s@{0}\", ref_stash);\n+\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n+\t\tstrbuf_addf(revision, \"%s@{%s}\", ref_stash, commit);\n+\t} else {\n+\t\tstrbuf_addstr(revision, commit);\n+\t}\n+\treturn 0;\n+}\n+\n static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n {\n \tint ret;\n@@ -196,17 +215,10 @@ static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n \tif (argc == 1)\n \t\tcommit = argv[0];\n \n-\tif (!commit) {\n-\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n-\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tstrbuf_addf(&info->revision, \"%s@{0}\", ref_stash);\n-\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n-\t\tstrbuf_addf(&info->revision, \"%s@{%s}\", ref_stash, commit);\n-\t} else {\n-\t\tstrbuf_addstr(&info->revision, commit);\n+\tstrbuf_init(&info->revision, 0);\n+\tif (parse_revision(&info->revision, commit, 0)) {\n+\t\tfree_stash_info(info);\n+\t\treturn -1;\n \t}\n \n \trevision = info->revision.buf;\n"},{"id":"517610","messageId":"20250508234458.3665894-4-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250508234458.3665894-1-sandals@crustytoothpaste.net","subject":"[PATCH v5 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-08T23:44:56Z","receivedAt":"2025-05-08T23:45:10Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"A common user problem is how to sync in-progress work to another\nmachine.  Users currently must use some sort of transfer of the working\ntree, which poses security risks and also necessarily causes the index\nto become dirty.  The experience is suboptimal and frustrating for\nusers.\n\nA reasonable idea is to use the stash for this purpose, but the stash is\nstored in the reflog, not in a ref, and as such it cannot be pushed or\npulled.  This also means that it cannot be saved into a bundle or\npreserved elsewhere, which is a problem when using throwaway development\nenvironments.\n\nLet's solve this problem by allowing the user to export the stash to a\nref (or, to just write it into the repository and print the hash, à la\ngit commit-tree).  Introduce git stash export, which writes a chain of\ncommits where the first parent is always a chain to the previous stash,\nor to a single, empty commit (for the final item) and the second is the\nstash commit normally written to the reflog.\n\nIterate over each stash from topmost to bottomost, looking up the data\nfor each one, and then create the chain from the single empty commit\nback up in reverse order.  Generate a predictable empty commit so our\nbehavior is reproducible.  Create a useful commit message, preserving\nthe author and committer information, to help users identify stash\ncommits when viewing them as normal commits.\n\nIf the user has specified specific stashes they'd like to export\ninstead, use those instead of iterating over all of the stashes.\n\nAs part of this, specifically request quiet behavior when looking up the\nOID for a revision because we will eventually hit a revision that\ndoesn't exist and we don't want to die when that occurs.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n Documentation/git-stash.adoc |  22 +++-\n builtin/stash.c              | 202 +++++++++++++++++++++++++++++++++++\n 2 files changed, 223 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 1a5177f498..e8efd43ba4 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -23,6 +23,7 @@ SYNOPSIS\n 'git stash' clear\n 'git stash' create [<message>]\n 'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>\n+'git stash' export (--print | --to-ref <ref>) [<stash>...]\n \n DESCRIPTION\n -----------\n@@ -154,6 +155,12 @@ store::\n \treflog.  This is intended to be useful for scripts.  It is\n \tprobably not the command you want to use; see \"push\" above.\n \n+export ( --print | --to-ref <ref> ) [<stash>...]::\n+\n+\tExport the specified stashes, or all of them if none are specified, to\n+\ta chain of commits which can be transferred using the normal fetch and\n+\tpush mechanisms, then imported using the `import` subcommand.\n+\n OPTIONS\n -------\n -a::\n@@ -242,6 +249,19 @@ literally (including newlines and quotes).\n +\n Quiet, suppress feedback messages.\n \n+--print::\n+\tThis option is only valid for `export`.\n++\n+Create the chain of commits representing the exported stashes without\n+storing it anywhere in the ref namespace and print the object ID to\n+standard output.  This is designed for scripts.\n+\n+--to-ref::\n+\tThis option is only valid for `export`.\n++\n+Create the chain of commits representing the exported stashes and store\n+it to the specified ref.\n+\n \\--::\n \tThis option is only valid for `push` command.\n +\n@@ -259,7 +279,7 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].\n \n <stash>::\n \tThis option is only valid for `apply`, `branch`, `drop`, `pop`,\n-\t`show` commands.\n+\t`show`, and `export` commands.\n +\n A reference of the form `stash@{<revision>}`. When no `<stash>` is\n given, the latest stash is assumed (that is, `stash@{0}`).\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 8ee6752efa..4a42126d02 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -29,6 +29,8 @@\n #include \"diffcore.h\"\n #include \"reflog.h\"\n #include \"add-interactive.h\"\n+#include \"oid-array.h\"\n+#include \"commit.h\"\n \n #define INCLUDE_ALL_FILES 2\n \n@@ -56,6 +58,8 @@\n \t   \"          [-u | --include-untracked] [-a | --all] [<message>]\")\n #define BUILTIN_STASH_CREATE_USAGE \\\n \tN_(\"git stash create [<message>]\")\n+#define BUILTIN_STASH_EXPORT_USAGE \\\n+\tN_(\"git stash export (--print | --to-ref <ref>) [<stash>...]\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -71,6 +75,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_CLEAR_USAGE,\n \tBUILTIN_STASH_CREATE_USAGE,\n \tBUILTIN_STASH_STORE_USAGE,\n+\tBUILTIN_STASH_EXPORT_USAGE,\n \tNULL\n };\n \n@@ -124,6 +129,12 @@ static const char * const git_stash_save_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_export_usage[] = {\n+\tBUILTIN_STASH_EXPORT_USAGE,\n+\tNULL\n+};\n+\n+\n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n \n@@ -1896,6 +1907,196 @@ static int save_stash(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int write_commit_with_parents(struct repository *r,\n+\t\t\t\t     struct object_id *out,\n+\t\t\t\t     const struct object_id *oid,\n+\t\t\t\t     struct commit_list *parents)\n+{\n+\tsize_t author_len, committer_len;\n+\tstruct commit *this;\n+\tconst char *orig_author, *orig_committer;\n+\tchar *author = NULL, *committer = NULL;\n+\tconst char *buffer;\n+\tunsigned long bufsize;\n+\tconst char *p;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tint ret = 0;\n+\tstruct ident_split id;\n+\n+\tthis = lookup_commit_reference(r, oid);\n+\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\torig_author = find_commit_header(buffer, \"author\", &author_len);\n+\torig_committer = find_commit_header(buffer, \"committer\", &committer_len);\n+\tif (split_ident_line(&id, orig_author, author_len) < 0 ||\n+\t    split_ident_line(&id, orig_committer, committer_len) < 0) {\n+\t\tret = error(_(\"invalid author or committer for %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\tp = strstr(buffer, \"\\n\\n\");\n+\n+\tif (!orig_author || !orig_committer || !p) {\n+\t\tret = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\t/* Jump to message. */\n+\tp += 2;\n+\tstrbuf_addstr(&msg, \"git stash: \");\n+\tstrbuf_add(&msg, p, bufsize - (p - buffer));\n+\n+\tauthor = xmemdupz(orig_author, author_len);\n+\tcommitter = xmemdupz(orig_committer, committer_len);\n+\n+\tif (commit_tree_extended(msg.buf, msg.len,\n+\t\t\t\t r->hash_algo->empty_tree, parents,\n+\t\t\t\t out, author, committer,\n+\t\t\t\t NULL, NULL)) {\n+\t\tret = error(_(\"could not write commit\"));\n+\t\tgoto out;\n+\t}\n+out:\n+\tstrbuf_release(&msg);\n+\trepo_unuse_commit_buffer(r, this, buffer);\n+\tfree_commit_list(parents);\n+\tfree(author);\n+\tfree(committer);\n+\treturn ret;\n+}\n+\n+static int do_export_stash(struct repository *r,\n+\t\t\t   const char *ref,\n+\t\t\t   int argc,\n+\t\t\t   const char **argv)\n+{\n+\tstruct object_id base;\n+\tstruct object_context unused;\n+\tstruct commit *prev;\n+\tstruct oid_array items = OID_ARRAY_INIT;\n+\tint res = 0;\n+\tint i;\n+\tstruct strbuf revision = STRBUF_INIT;\n+\tconst char *author, *committer;\n+\n+\t/*\n+\t * This is an arbitrary, fixed date, specifically the one used by git\n+\t * format-patch.  The goal is merely to produce reproducible output.\n+\t */\n+\tprepare_fallback_ident(\"git stash\", \"git@stash\");\n+\tauthor = fmt_ident(\"git stash\", \"git@stash\", WANT_BLANK_IDENT,\n+\t\t\t   \"2001-09-17T00:00:00Z\", 0);\n+\tcommitter = fmt_ident(\"git stash\", \"git@stash\", WANT_BLANK_IDENT,\n+\t\t\t      \"2001-09-17T00:00:00Z\", 0);\n+\n+\t/* First, we create a single empty commit. */\n+\tif (commit_tree_extended(\"\", 0, r->hash_algo->empty_tree, NULL,\n+\t\t\t\t &base, author, committer, NULL, NULL))\n+\t\treturn error(_(\"unable to write base commit\"));\n+\n+\tprev = lookup_commit_reference(r, &base);\n+\n+\tif (argc) {\n+\t\t/*\n+\t\t * Find each specified stash, and load data into the array.\n+\t\t */\n+\t\tfor (i = 0; i < argc; i++) {\n+\t\t\tstruct object_id oid;\n+\t\t\tif (parse_revision(&revision, argv[i], 1) ||\n+\t\t\t    get_oid_with_context(r, revision.buf,\n+\t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n+\t\t\t\t\t\t &oid, &unused)) {\n+\t\t\t\tres = error(_(\"unable to find stash entry %s\"), argv[i]);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t\toid_array_append(&items, &oid);\n+\t\t}\n+\t} else {\n+\t\t/*\n+\t\t * Walk the reflog, finding each stash entry, and load data into the\n+\t\t * array.\n+\t\t */\n+\t\tfor (i = 0;; i++) {\n+\t\t\tchar buf[32];\n+\t\t\tstruct object_id oid;\n+\n+\t\t\tsnprintf(buf, sizeof(buf), \"%d\", i);\n+\t\t\tif (parse_revision(&revision, buf, 1) ||\n+\t\t\t    get_oid_with_context(r, revision.buf,\n+\t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n+\t\t\t\t\t\t &oid, &unused))\n+\t\t\t\tbreak;\n+\t\t\toid_array_append(&items, &oid);\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * Now, create a set of commits identical to the regular stash commits,\n+\t * but where their first parents form a chain to our original empty\n+\t * base commit.\n+\t */\n+\tfor (i = items.nr - 1; i >= 0; i--) {\n+\t\tstruct commit_list *parents = NULL;\n+\t\tstruct commit_list **next = &parents;\n+\t\tstruct object_id out;\n+\t\tconst struct object_id *oid = items.oid + i;\n+\n+\t\tnext = commit_list_append(prev, next);\n+\t\tnext = commit_list_append(lookup_commit_reference(r, oid), next);\n+\t\tres = write_commit_with_parents(r, &out, oid, parents);\n+\t\tif (res)\n+\t\t\tgoto out;\n+\t\tprev = lookup_commit_reference(r, &out);\n+\t}\n+\tif (ref)\n+\t\trefs_update_ref(get_main_ref_store(r), NULL, ref,\n+\t\t\t\t&prev->object.oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR);\n+\telse\n+\t\tputs(oid_to_hex(&prev->object.oid));\n+out:\n+\tstrbuf_release(&revision);\n+\toid_array_clear(&items);\n+\n+\treturn res;\n+}\n+\n+enum export_action {\n+\tACTION_NONE,\n+\tACTION_PRINT,\n+\tACTION_TO_REF,\n+};\n+\n+static int export_stash(int argc,\n+\t\t\tconst char **argv,\n+\t\t\tconst char *prefix,\n+\t\t\tstruct repository *repo)\n+{\n+\tconst char *ref = NULL;\n+\tenum export_action action = ACTION_NONE;\n+\tstruct option options[] = {\n+\t\tOPT_CMDMODE(0, \"print\", &action,\n+\t\t\t    N_(\"print the object ID instead of writing it to a ref\"),\n+\t\t\t    ACTION_PRINT),\n+\t\tOPT_CMDMODE(0, \"to-ref\", &action,\n+\t\t\t    N_(\"save the data to the given ref\"),\n+\t\t\t    ACTION_TO_REF),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_export_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (action == ACTION_NONE) {\n+\t\treturn error(_(\"exactly one of --print and --to-ref is required\"));\n+\t} else if (action == ACTION_TO_REF) {\n+\t\tif (!argc)\n+\t\t\treturn error(_(\"--to-ref requires an argument\"));\n+\t\tref = argv[0];\n+\t\targc--;\n+\t\targv++;\n+\t}\n+\n+\treturn do_export_stash(repo, ref, argc, argv);\n+}\n+\n int cmd_stash(int argc,\n \t      const char **argv,\n \t      const char *prefix,\n@@ -1916,6 +2117,7 @@ int cmd_stash(int argc,\n \t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n \t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n+\t\tOPT_SUBCOMMAND(\"export\", &fn, export_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\n"},{"id":"517611","messageId":"20250508234458.3665894-1-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":null,"subject":"[PATCH v5 0/4] Importing and exporting stashes to refs","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-08T23:44:53Z","receivedAt":"2025-05-08T23:45:10Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Stashes are currently stored using the reflog in a given repository.\nThis is an interesting and novel way to handle them, but there is no way\nto easily move a stash across machines.  For example, stashes cannot be\nbundled, pushed, or fetched.\n\nLet's solve this problem by allowing users to import and export stashes\nto a chain of commits.  The commits used in a stash export contain two\nparents: one which is the pointer to the next exported stash (or to an\nempty commit with no parents if there are no more) and the second is the\nstash commit that would normally be stored in the reflog.\n\nOriginal thread at message-ID: <20220310173236.4165310-1-sandals@crustytoothpaste.net>\n\nChanges from v4:\n* Fix another use of oid_array.\n* Fix various memory leaks.\n* Fix a segfault which appeared after a rebase.\n* Use strstr for commits since we don't need to worry about NUL.\n* Added some additional tests.\n* Verify the ident values we're using to avoid using bad values.\n* Various other code cleanups.\n* Rebase on `master`.\n\nChanges from v3:\n* Fix strbuf handling to avoid leaks and generally be more sensible.\n* Make use of the error return code more often.\n* Use oid_array.\n* Tidy various parts of the code and fix long lines.\n* Simplify tests using git tag.\n* Shorten and tidy tests.\n* Add an additional test covering the base commit OID and importing and\n  exporting empty stashes.\n\nChanges from v2:\n* Fix uninitialized strbuf.\n* Avoid C99-style initializations.\n\nChanges from v1:\n* Change storage format as suggested by Junio.\n* Rename to GIT_OID_GENTLY.\n* Remove unnecessary initializations.\n* Use ALLOC_GROW_BY.\n* Ensure completely reproducible exports.\n* Avoid size_t.\n* Various other code cleanups.\n\nbrian m. carlson (4):\n  object-name: make get_oid quietly return an error\n  builtin/stash: factor out revision parsing into a function\n  builtin/stash: provide a way to export stashes to a ref\n  builtin/stash: provide a way to import stashes from a ref\n\n Documentation/git-stash.adoc |  29 ++-\n builtin/stash.c              | 339 +++++++++++++++++++++++++++++++++--\n hash.h                       |   1 +\n object-name.c                |   6 +-\n t/t3903-stash.sh             |  78 ++++++++\n 5 files changed, 440 insertions(+), 13 deletions(-)\n\n"},{"id":"517612","messageId":"20250508234458.3665894-2-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250508234458.3665894-1-sandals@crustytoothpaste.net","subject":"[PATCH v5 1/4] object-name: make get_oid quietly return an error","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-08T23:44:54Z","receivedAt":"2025-05-08T23:45:10Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"A reasonable person looking at the signature and usage of get_oid and\nfriends might conclude that in the event of an error, it always returns\n-1.  However, this is not the case.  Instead, get_oid_basic dies if we\ngo too far back into the history of a reflog (or, when quiet, simply\nexits).\n\nThis is not especially useful, since in many cases, we might want to\nhandle this error differently.  Let's add a flag here to make it just\nreturn -1 like elsewhere in these code paths.\n\nNote that we cannot make this behavior the default, since we have many\nother codepaths that rely on the existing behavior, including in tests.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n hash.h        | 1 +\n object-name.c | 6 +++++-\n 2 files changed, 6 insertions(+), 1 deletion(-)\n\ndiff --git a/hash.h b/hash.h\nindex d6422ddf45..ec594c63a6 100644\n--- a/hash.h\n+++ b/hash.h\n@@ -216,6 +216,7 @@ struct object_id {\n #define GET_OID_REQUIRE_PATH         010000\n #define GET_OID_HASH_ANY             020000\n #define GET_OID_SKIP_AMBIGUITY_CHECK 040000\n+#define GET_OID_GENTLY              0100000\n \n #define GET_OID_DISAMBIGUATORS \\\n \t(GET_OID_COMMIT | GET_OID_COMMITTISH | \\\ndiff --git a/object-name.c b/object-name.c\nindex 2c751a5352..3138103343 100644\n--- a/object-name.c\n+++ b/object-name.c\n@@ -1081,13 +1081,17 @@ static int get_oid_basic(struct repository *r, const char *str, int len,\n \t\t\t\t * still fill in the oid with the \"old\" value,\n \t\t\t\t * which we can use.\n \t\t\t\t */\n-\t\t\t} else {\n+\t\t\t} else if (!(flags & GET_OID_GENTLY)) {\n \t\t\t\tif (flags & GET_OID_QUIETLY) {\n \t\t\t\t\texit(128);\n \t\t\t\t}\n \t\t\t\tdie(_(\"log for '%.*s' only has %d entries\"),\n \t\t\t\t    len, str, co_cnt);\n \t\t\t}\n+\t\t\tif (flags & GET_OID_GENTLY) {\n+\t\t\t\tfree(real_ref);\n+\t\t\t\treturn -1;\n+\t\t\t}\n \t\t}\n \t}\n \n"},{"id":"517613","messageId":"20250508234458.3665894-5-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250508234458.3665894-1-sandals@crustytoothpaste.net","subject":"[PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-08T23:44:57Z","receivedAt":"2025-05-08T23:45:10Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Now that we have a way to export stashes to a ref, let's provide a way\nto import them from such a ref back to the stash.  This works much the\nway the export code does, except that we strip off the first parent\nchain commit and then store each resulting commit back to the stash.\n\nWe don't clear the stash first and instead add the specified stashes to\nthe top of the stash.  This is because users may want to export just a\nfew stashes, such as to share a small amount of work in progress with a\ncolleague, and it would be undesirable for the receiving user to lose\nall of their data.  For users who do want to replace the stash, it's\neasy to do to: simply run \"git stash clear\" first.\n\nWe specifically rely on the fact that we'll produce identical stash\ncommits on both sides in our tests.  This provides a cheap,\nstraightforward check for our tests and also makes it easy for users to\nsee if they already have the same data in both repositories.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n Documentation/git-stash.adoc |   7 +++\n builtin/stash.c              | 103 +++++++++++++++++++++++++++++++++++\n t/t3903-stash.sh             |  78 ++++++++++++++++++++++++++\n 3 files changed, 188 insertions(+)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex e8efd43ba4..f8193b712e 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -24,6 +24,7 @@ SYNOPSIS\n 'git stash' create [<message>]\n 'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>\n 'git stash' export (--print | --to-ref <ref>) [<stash>...]\n+'git stash' import <commit>\n \n DESCRIPTION\n -----------\n@@ -161,6 +162,12 @@ export ( --print | --to-ref <ref> ) [<stash>...]::\n \ta chain of commits which can be transferred using the normal fetch and\n \tpush mechanisms, then imported using the `import` subcommand.\n \n+import <commit>::\n+\n+\tImport the specified stashes from the specified commit, which must have been\n+\tcreated by `export`, and add them to the list of stashes.  To replace the\n+\texisting stashes, use `clear` first.\n+\n OPTIONS\n -------\n -a::\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 4a42126d02..d8332af401 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -60,6 +60,8 @@\n \tN_(\"git stash create [<message>]\")\n #define BUILTIN_STASH_EXPORT_USAGE \\\n \tN_(\"git stash export (--print | --to-ref <ref>) [<stash>...]\")\n+#define BUILTIN_STASH_IMPORT_USAGE \\\n+\tN_(\"git stash import <commit>\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -76,6 +78,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_CREATE_USAGE,\n \tBUILTIN_STASH_STORE_USAGE,\n \tBUILTIN_STASH_EXPORT_USAGE,\n+\tBUILTIN_STASH_IMPORT_USAGE,\n \tNULL\n };\n \n@@ -134,6 +137,10 @@ static const char * const git_stash_export_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_import_usage[] = {\n+\tBUILTIN_STASH_IMPORT_USAGE,\n+\tNULL\n+};\n \n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n@@ -143,6 +150,7 @@ static struct strbuf stash_index_path = STRBUF_INIT;\n  * b_commit is set to the base commit\n  * i_commit is set to the commit containing the index tree\n  * u_commit is set to the commit containing the untracked files tree\n+ * c_commit is set to the first parent (chain commit) when importing and is otherwise unset\n  * w_tree is set to the working tree\n  * b_tree is set to the base tree\n  * i_tree is set to the index tree\n@@ -153,6 +161,7 @@ struct stash_info {\n \tstruct object_id b_commit;\n \tstruct object_id i_commit;\n \tstruct object_id u_commit;\n+\tstruct object_id c_commit;\n \tstruct object_id w_tree;\n \tstruct object_id b_tree;\n \tstruct object_id i_tree;\n@@ -1962,6 +1971,99 @@ static int write_commit_with_parents(struct repository *r,\n \treturn ret;\n }\n \n+static int do_import_stash(struct repository *r, const char *rev)\n+{\n+\tstruct object_id chain;\n+\tstruct oid_array items = OID_ARRAY_INIT;\n+\tint res = 0;\n+\tint i;\n+\tconst char *buffer = NULL;\n+\tstruct commit *this = NULL;\n+\tchar *msg = NULL;\n+\n+\tif (repo_get_oid(r, rev, &chain))\n+\t\treturn error(_(\"not a valid revision: %s\"), rev);\n+\n+\t/*\n+\t * Walk the commit history, finding each stash entry, and load data into\n+\t * the array.\n+\t */\n+\tfor (i = 0;; i++) {\n+\t\tstruct object_id tree, oid;\n+\t\tchar revision[GIT_MAX_HEXSZ + 1];\n+\n+\t\toid_to_hex_r(revision, &chain);\n+\n+\t\tif (get_oidf(&tree, \"%s:\", revision) ||\n+\t\t    !oideq(&tree, r->hash_algo->empty_tree)) {\n+\t\t\tres = error(_(\"%s is not a valid exported stash commit\"), revision);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tif (get_oidf(&chain, \"%s^1\", revision) ||\n+\t\t    get_oidf(&oid, \"%s^2\", revision))\n+\t\t\tbreak;\n+\t\toid_array_append(&items, &oid);\n+\t}\n+\n+\t/*\n+\t * Now, walk each entry, adding it to the stash as a normal stash\n+\t * commit.\n+\t */\n+\tfor (i = items.nr - 1; i >= 0; i--) {\n+\t\tunsigned long bufsize;\n+\t\tconst char *p;\n+\t\tconst struct object_id *oid = items.oid + i;\n+\n+\t\tthis = lookup_commit_reference(r, oid);\n+\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\t\tif (!buffer) {\n+\t\t\tres = error(_(\"cannot read commit buffer for %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp = strstr(buffer, \"\\n\\n\");\n+\t\tif (!p) {\n+\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp += 2;\n+\t\tmsg = xmemdupz(p, bufsize - (p - buffer));\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\t\tbuffer = NULL;\n+\n+\t\tif (do_store_stash(oid, msg, 1)) {\n+\t\t\tres = error(_(\"cannot save the stash for %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\t\tFREE_AND_NULL(msg);\n+\t}\n+out:\n+\tif (this && buffer)\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\toid_array_clear(&items);\n+\tfree(msg);\n+\n+\treturn res;\n+}\n+\n+static int import_stash(int argc, const char **argv, const char *prefix,\n+\t\t\tstruct repository *repo)\n+{\n+\tstruct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_import_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (argc != 1)\n+\t\tusage_msg_opt(\"a revision is required\", git_stash_import_usage, options);\n+\n+\treturn do_import_stash(repo, argv[0]);\n+}\n+\n static int do_export_stash(struct repository *r,\n \t\t\t   const char *ref,\n \t\t\t   int argc,\n@@ -2118,6 +2220,7 @@ int cmd_stash(int argc,\n \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n \t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n \t\tOPT_SUBCOMMAND(\"export\", &fn, export_stash),\n+\t\tOPT_SUBCOMMAND(\"import\", &fn, import_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex 74666ff3e4..2ab9e5d848 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -11,6 +11,13 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-unique-files.sh\n \n+test_expect_success 'setup' '\n+\ttest_oid_cache <<-EOF\n+\texport_base sha1:73c9bab443d1f88ac61aa533d2eeaaa15451239c\n+\texport_base sha256:f210fa6346e3e2ce047bdb570426b17075980c1ac01fec8fc4b75bd3ab4bcfe4\n+\tEOF\n+'\n+\n test_expect_success 'usage on cmd and subcommand invalid option' '\n \ttest_expect_code 129 git stash --invalid-option 2>usage &&\n \tgrep \"or: git stash\" usage &&\n@@ -1412,6 +1419,77 @@ test_expect_success 'stash --keep-index --include-untracked with empty tree' '\n \t)\n '\n \n+test_expect_success 'stash export and import round-trip stashes' '\n+\tgit reset &&\n+\t>untracked &&\n+\t>tracked1 &&\n+\t>tracked2 &&\n+\tgit add tracked* &&\n+\tgit stash -- &&\n+\t>subdir/untracked &&\n+\t>subdir/tracked1 &&\n+\t>subdir/tracked2 &&\n+\tgit add subdir/tracked* &&\n+\tgit stash --include-untracked -- subdir/ &&\n+\tgit tag t-stash0 stash@{0} &&\n+\tgit tag t-stash1 stash@{1} &&\n+\tsimple=$(git stash export --print) &&\n+\tgit stash clear &&\n+\tgit stash import \"$simple\" &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\ttest_cmp_rev stash@{1} t-stash1 &&\n+\tgit stash export --to-ref refs/heads/foo &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" foo: &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" foo^: &&\n+\ttest_cmp_rev t-stash0 foo^2 &&\n+\ttest_cmp_rev t-stash1 foo^^2 &&\n+\tgit log --first-parent --format=\"%s\" refs/heads/foo >log &&\n+\tgrep \"^git stash: \" log >log2 &&\n+\ttest_line_count = 13 log2 &&\n+\tgit stash clear &&\n+\tgit stash import foo &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\ttest_cmp_rev stash@{1} t-stash1\n+'\n+\n+test_expect_success 'stash import appends commits' '\n+\tgit log --format=oneline -g refs/stash >out &&\n+\tcat out out >out2 &&\n+\tgit stash import refs/heads/foo &&\n+\tgit log --format=oneline -g refs/stash >actual &&\n+\ttest_line_count = $(wc -l <out2) actual\n+'\n+\n+test_expect_success 'stash export can accept specified stashes' '\n+\tgit stash clear &&\n+\tgit stash import foo &&\n+\tgit stash export --to-ref bar stash@{1} stash@{0} &&\n+\tgit stash clear &&\n+\tgit stash import bar &&\n+\ttest_cmp_rev stash@{1} t-stash0 &&\n+\ttest_cmp_rev stash@{0} t-stash1 &&\n+\tgit log --format=oneline -g refs/stash >actual &&\n+\ttest_line_count = 2 actual\n+'\n+\n+test_expect_success 'stash can import and export zero stashes' '\n+\tgit stash clear &&\n+\tgit stash export --to-ref baz &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" baz: &&\n+\ttest_cmp_rev \"$(test_oid export_base)\" baz &&\n+\ttest_must_fail git rev-parse baz^1 &&\n+\tgit stash import baz &&\n+\ttest_must_fail git rev-parse refs/stash\n+'\n+\n+test_expect_success 'stash rejects invalid attempts to import commits' '\n+\tgit stash import foo &&\n+\ttest_must_fail git stash import HEAD 2>output &&\n+\toid=$(git rev-parse HEAD) &&\n+\tgrep \"$oid is not a valid exported stash commit\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0\n+'\n+\n test_expect_success 'stash apply should succeed with unmodified file' '\n \techo base >file &&\n \tgit add file &&\n"},{"id":"517616","messageId":"xmqq8qn64mho.fsf@gitster.g","threadId":"63424","inReplyTo":"20250508234458.3665894-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 0/4] Importing and exporting stashes to refs","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-09T01:10:59Z","receivedAt":"2025-05-09T01:11:01Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> Stashes are currently stored using the reflog in a given repository.\n> This is an interesting and novel way to handle them, but there is no way\n> to easily move a stash across machines.  For example, stashes cannot be\n> bundled, pushed, or fetched.\n>\n> Let's solve this problem by allowing users to import and export stashes\n> to a chain of commits.  The commits used in a stash export contain two\n> parents: one which is the pointer to the next exported stash (or to an\n> empty commit with no parents if there are no more) and the second is the\n> stash commit that would normally be stored in the reflog.\n>\n> Original thread at message-ID: <20220310173236.4165310-1-sandals@crustytoothpaste.net>\n\nThanks for resurrecting the thread.  Once this part gets sorted out\nand the outside-reflog representation of stash becomes stable, the\nnext logical step would be to teach the \"git stash create\" and\nfriends to directly work on the new format, perhaps?  But first\nthings first---export+import would certainly be a logical first\nstep.\n\n"},{"id":"517619","messageId":"xmqq4ixu4kfo.fsf@gitster.g","threadId":"63424","inReplyTo":"20250508234458.3665894-2-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 1/4] object-name: make get_oid quietly return an error","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-09T01:55:23Z","receivedAt":"2025-05-09T01:55:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> diff --git a/object-name.c b/object-name.c\n> index 2c751a5352..3138103343 100644\n> --- a/object-name.c\n> +++ b/object-name.c\n> @@ -1081,13 +1081,17 @@ static int get_oid_basic(struct repository *r, const char *str, int len,\n>  \t\t\t\t * still fill in the oid with the \"old\" value,\n>  \t\t\t\t * which we can use.\n>  \t\t\t\t */\n> -\t\t\t} else {\n> +\t\t\t} else if (!(flags & GET_OID_GENTLY)) {\n>  \t\t\t\tif (flags & GET_OID_QUIETLY) {\n>  \t\t\t\t\texit(128);\n>  \t\t\t\t}\n>  \t\t\t\tdie(_(\"log for '%.*s' only has %d entries\"),\n>  \t\t\t\t    len, str, co_cnt);\n>  \t\t\t}\n> +\t\t\tif (flags & GET_OID_GENTLY) {\n> +\t\t\t\tfree(real_ref);\n> +\t\t\t\treturn -1;\n> +\t\t\t}\n>  \t\t}\n>  \t}\n>  \n\nAlmost everybody else in this function hits \"return -1\" after\ndetecting that it cannot yield a valid object name, and this change\nmakes the oddball case do the same.\n\nIdeally in a distant past, we might want to remove this _GENTLY\nflag, together with the code path that is not so gentle, and adjust\nthe callers that depend on the current behaviour (which I somehow\ndoubt--- they need to be prepared to deal with the error return from\nother parts of the same function already).  We might need to make it\npossible for callers to tell which error condition we got (e.g., did\nthe input give it a non-existing ref?  did reflog walk run out?),\nbut these (including to the change to just lose \"die\" and always go\nthe GENTLY code path) are totally outside the scope of this series.\n\nThanks.\n\n\n"},{"id":"517689","messageId":"xmqqldr53iuf.fsf@gitster.g","threadId":"63424","inReplyTo":"20250508234458.3665894-3-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 2/4] builtin/stash: factor out revision parsing into a function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-09T15:27:20Z","receivedAt":"2025-05-09T15:27:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> We allow several special forms of stash names in this code.  In the\n> future, we'll want to allow these same forms without parsing a stash\n> commit, so let's refactor this code out into a function for reuse.\n>\n> Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n> ---\n>  builtin/stash.c | 34 +++++++++++++++++++++++-----------\n>  1 file changed, 23 insertions(+), 11 deletions(-)\n>\n> diff --git a/builtin/stash.c b/builtin/stash.c\n> index cfbd92852a..8ee6752efa 100644\n> --- a/builtin/stash.c\n> +++ b/builtin/stash.c\n> @@ -169,6 +169,25 @@ static void assert_stash_like(struct stash_info *info, const char *revision)\n>  \t\tdie(_(\"'%s' is not a stash-like commit\"), revision);\n>  }\n>  \n> +static int parse_revision(struct strbuf *revision, const char *commit, int quiet)\n> +{\n> +\tstrbuf_reset(revision);\n> +\tif (!commit) {\n> +\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n> +\t\t\tif (!quiet)\n> +\t\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n> +\t\t\treturn -1;\n> +\t\t}\n> +\n> +\t\tstrbuf_addf(revision, \"%s@{0}\", ref_stash);\n> +\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n> +\t\tstrbuf_addf(revision, \"%s@{%s}\", ref_stash, commit);\n> +\t} else {\n> +\t\tstrbuf_addstr(revision, commit);\n> +\t}\n> +\treturn 0;\n> +}\n> +\n>  static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n>  {\n>  \tint ret;\n> @@ -196,17 +215,10 @@ static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n>  \tif (argc == 1)\n>  \t\tcommit = argv[0];\n>  \n> -\tif (!commit) {\n> -\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n> -\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n> -\t\t\treturn -1;\n> -\t\t}\n> -\n> -\t\tstrbuf_addf(&info->revision, \"%s@{0}\", ref_stash);\n> -\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n> -\t\tstrbuf_addf(&info->revision, \"%s@{%s}\", ref_stash, commit);\n> -\t} else {\n> -\t\tstrbuf_addstr(&info->revision, commit);\n> +\tstrbuf_init(&info->revision, 0);\n> +\tif (parse_revision(&info->revision, commit, 0)) {\n> +\t\tfree_stash_info(info);\n> +\t\treturn -1;\n>  \t}\n\nTwo comments:\n\n - It is file-scope static so it is not a huge deal, but it is a bit\n   confusing that parse_revision() sounds like a helper function you\n   would have in revision.c and call from everywhere.\n\n - The call to free_stash_info() from inside get_stash_info() is\n   probably wrong.  The early error return when ref_stash is missing\n   leaves info intact, and the callers of it share this pattern:\n\n        if (get_stash_info(&info, argc - 1, argv + 1))\n                goto cleanup;\n        ...\n    cleanup:\n        free_stash_info(&info);\n        return ret;\n\n   Even if free_stah_info() happen to be idempotent right now, I do\n   not think we want our code to rely on it.\n\n"},{"id":"517695","messageId":"xmqqr00x20zv.fsf@gitster.g","threadId":"63424","inReplyTo":"20250508234458.3665894-4-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-09T16:38:12Z","receivedAt":"2025-05-09T16:38:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> +static int write_commit_with_parents(struct repository *r,\n> +\t\t\t\t     struct object_id *out,\n> +\t\t\t\t     const struct object_id *oid,\n> +\t\t\t\t     struct commit_list *parents)\n> +{\n> +\tsize_t author_len, committer_len;\n> +\tstruct commit *this;\n> +\tconst char *orig_author, *orig_committer;\n> +\tchar *author = NULL, *committer = NULL;\n> +\tconst char *buffer;\n> +\tunsigned long bufsize;\n> +\tconst char *p;\n> +\tstruct strbuf msg = STRBUF_INIT;\n> +\tint ret = 0;\n> +\tstruct ident_split id;\n> +\n> +\tthis = lookup_commit_reference(r, oid);\n> +\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n> +\torig_author = find_commit_header(buffer, \"author\", &author_len);\n> +\torig_committer = find_commit_header(buffer, \"committer\", &committer_len);\n> +\tif (split_ident_line(&id, orig_author, author_len) < 0 ||\n> +\t    split_ident_line(&id, orig_committer, committer_len) < 0) {\n> +\t\tret = error(_(\"invalid author or committer for %s\"), oid_to_hex(oid));\n> +\t\tgoto out;\n> +\t}\n> +\tp = strstr(buffer, \"\\n\\n\");\n> +\n> +\tif (!orig_author || !orig_committer || !p) {\n> +\t\tret = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n> +\t\tgoto out;\n> +\t}\n\nIt is too late to check the NULL-ness of orig_author and\norig_committer here.  They have already been used without checking\nfor their NULL-ness to call split_ident_line() that happily will\ndereference its second \"const char *line\" parameter, so we would\nhave already segfaulted.\n\nAs fsck.c::verify_headers() say, it is not a crime to lack the\n\"\\n\\n\" after the last header item, if the commit truly lacks any\nmessage.  So '!p' is a bit overly strict, but in practice I do not\nthink our tools saved a byte by omitting the empty line after the\nheader even when creating a commit with an empty message for a long\ntime, so this may be OK.  On the other hand, preparing for a stash\nentry a third-party reimplementation prepared would not be too hard\nto do here.\n\n\tif (!orig_author || !orig_committer) {\n\t\tret = error(_(\"cannot parse...\"));\n\t\tgoto out;\n\t}\n\tif (split_ident_line(...) < 0 ||\n\t    split_ident_line(...) < 0) {\n\t\tret = error(_(\"invalid au...\"));\n\t\tgoto out;\n\t}\n\n\tp = strstr(buffer, \"\\n\\n\");\n\tstrbuf_addstr(&msg, \"git stash: \");\n\n\tif (p)\n\t\tstrbuf_add(&msg, p+2, bufsize - (p + 2 - buffer));\n\tstrbuf_complete_line(&mesg);\n\nor something.\n\n> +\t/* Jump to message. */\n> +\tp += 2;\n> +\tstrbuf_addstr(&msg, \"git stash: \");\n> +\tstrbuf_add(&msg, p, bufsize - (p - buffer));\n\n"},{"id":"517699","messageId":"CALnO6CDDrd=XpkkTFYfAoiNcoG5q83Z8c+Ey4jKTBriA=M7TwQ@mail.gmail.com","threadId":"63424","inReplyTo":"20250508234458.3665894-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 0/4] Importing and exporting stashes to refs","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-05-09T16:53:17Z","receivedAt":"2025-05-09T16:53:33Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Thu, May 8, 2025 at 7:47 PM brian m. carlson\n<sandals@crustytoothpaste.net> wrote:\n>\n> Stashes are currently stored using the reflog in a given repository.\n> This is an interesting and novel way to handle them, but there is no way\n> to easily move a stash across machines.  For example, stashes cannot be\n> bundled, pushed, or fetched.\n\nI'm probably missing something here after having skimmed the original\ncover letter at\n<20220310173236.4165310-1-sandals@crustytoothpaste.net>, but:\n\n- Can't we \"git push <remote> stash[@{n}]:<branch>\" to share a stash somewhere?\n- And then, doesn't \"git stash apply [--index] <arbitrary sha>\" work?\n(At which point you could presumably create a new stash, though I'll\nadmit that's cumbersome relative to dedicated export/import.)\n\nI can see how that doesn't help you quickly export a whole _chain_ of\nstashes, so I'm not saying \"this series seems like the wrong tack\"\n(far be it from me, who doesn't understand your use case, to say\nthat!). Rather, I'm confused about the inability to move a (single)\nstash across machines.\n\nUnrelated question: Can we import arbitrary refs into stashes? That\nis, what happens if the commit structure doesn't look right? (Maybe I\nshould go read the tests and see.)\n\n-- \nD. Ben Knoble\n"},{"id":"517705","messageId":"xmqqecwx1szk.fsf@gitster.g","threadId":"63424","inReplyTo":"20250508234458.3665894-4-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-09T19:31:11Z","receivedAt":"2025-05-09T19:31:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> +static int do_export_stash(struct repository *r,\n> +\t\t\t   const char *ref,\n> +\t\t\t   int argc,\n> +\t\t\t   const char **argv)\n> +{\n> +...\n> +\tif (argc) {\n> +\t\t/*\n> +\t\t * Find each specified stash, and load data into the array.\n> +\t\t */\n> +\t\tfor (i = 0; i < argc; i++) {\n> +\t\t\tstruct object_id oid;\n> +\t\t\tif (parse_revision(&revision, argv[i], 1) ||\n> +\t\t\t    get_oid_with_context(r, revision.buf,\n> +\t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n> +\t\t\t\t\t\t &oid, &unused)) {\n> +\t\t\t\tres = error(_(\"unable to find stash entry %s\"), argv[i]);\n> +\t\t\t\tgoto out;\n> +\t\t\t}\n> +\t\t\toid_array_append(&items, &oid);\n> +\t\t}\n\nGrabbing individual reflog entry given on the command line looks\ntrivial, but ...\n\n> +\t} else {\n> +\t\t/*\n> +\t\t * Walk the reflog, finding each stash entry, and load data into the\n> +\t\t * array.\n> +\t\t */\n> +\t\tfor (i = 0;; i++) {\n> +\t\t\tchar buf[32];\n> +\t\t\tstruct object_id oid;\n> +\n> +\t\t\tsnprintf(buf, sizeof(buf), \"%d\", i);\n> +\t\t\tif (parse_revision(&revision, buf, 1) ||\n> +\t\t\t    get_oid_with_context(r, revision.buf,\n> +\t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n> +\t\t\t\t\t\t &oid, &unused))\n> +\t\t\t\tbreak;\n> +\t\t\toid_array_append(&items, &oid);\n> +\t\t}\n\n... have you considered reusing reflog-walk.c:read_complete_reflog()\nas a helper function?  \n\nDoing so of would be more efficient than going from int to string\nback to int to call read_ref_at() and iterate over the same reflog\nentries with refs_for_each_reflog_ent().\n\n> +\t}\n> +\n> +\t/*\n> +\t * Now, create a set of commits identical to the regular stash commits,\n> +\t * but where their first parents form a chain to our original empty\n> +\t * base commit.\n> +\t */\n> +\tfor (i = items.nr - 1; i >= 0; i--) {\n> +\t\tstruct commit_list *parents = NULL;\n> +\t\tstruct commit_list **next = &parents;\n> +\t\tstruct object_id out;\n> +\t\tconst struct object_id *oid = items.oid + i;\n> +\n> +\t\tnext = commit_list_append(prev, next);\n> +\t\tnext = commit_list_append(lookup_commit_reference(r, oid), next);\n\nThe individual-reflog-entry mode above was fairly strict in that a\nlist of reflog entries with even one unreadable commit caused the\nwhole command to fail, but reflog-walk mode assumed that a failure\nto read an entry must always be due to reflog entries running out\ndue to the index incremented to a large enough number.  I suspect\nget_oid_with_context() can give you oid obtained out of a reflog\nentry without actually parsing the object or checking if it exists.\n\nShould we be a bit more defensinve here in lookup_commit_reference()\ncall, which would silently throw a NULL back at us if the commit\ncannot be found without complaining?\n\n"},{"id":"517706","messageId":"aB5cbjlSKXt-uOw-@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"xmqq4ixu4kfo.fsf@gitster.g","subject":"Re: [PATCH v5 1/4] object-name: make get_oid quietly return an error","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-09T19:50:06Z","receivedAt":"2025-05-09T19:50:09Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-09 at 01:55:23, Junio C Hamano wrote:\n> Almost everybody else in this function hits \"return -1\" after\n> detecting that it cannot yield a valid object name, and this change\n> makes the oddball case do the same.\n> \n> Ideally in a distant past, we might want to remove this _GENTLY\n> flag, together with the code path that is not so gentle, and adjust\n> the callers that depend on the current behaviour (which I somehow\n> doubt--- they need to be prepared to deal with the error return from\n> other parts of the same function already).  We might need to make it\n> possible for callers to tell which error condition we got (e.g., did\n> the input give it a non-existing ref?  did reflog walk run out?),\n> but these (including to the change to just lose \"die\" and always go\n> the GENTLY code path) are totally outside the scope of this series.\n\nYes, I think we may have discussed doing this in the past and I\noriginally considered doing so, only to realize that was an entire\nseries to itself and would be pretty disruptive.  Last I looked, we\nunfortunately relied on this case more than you might imagine.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"517707","messageId":"xmqqzfflzhjv.fsf@gitster.g","threadId":"63424","inReplyTo":"20250508234458.3665894-5-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-09T19:54:12Z","receivedAt":"2025-05-09T19:54:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> +static int do_import_stash(struct repository *r, const char *rev)\n> +{\n> +\tstruct object_id chain;\n> +\tstruct oid_array items = OID_ARRAY_INIT;\n> +\tint res = 0;\n> +\tint i;\n> +\tconst char *buffer = NULL;\n> +\tstruct commit *this = NULL;\n> +\tchar *msg = NULL;\n> +\n> +\tif (repo_get_oid(r, rev, &chain))\n> +\t\treturn error(_(\"not a valid revision: %s\"), rev);\n> +\n> +\t/*\n> +\t * Walk the commit history, finding each stash entry, and load data into\n> +\t * the array.\n> +\t */\n> +\tfor (i = 0;; i++) {\n> +\t\tstruct object_id tree, oid;\n> +\t\tchar revision[GIT_MAX_HEXSZ + 1];\n> +\n> +\t\toid_to_hex_r(revision, &chain);\n> +\n> +\t\tif (get_oidf(&tree, \"%s:\", revision) ||\n> +\t\t    !oideq(&tree, r->hash_algo->empty_tree)) {\n> +\t\t\tres = error(_(\"%s is not a valid exported stash commit\"), revision);\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tif (get_oidf(&chain, \"%s^1\", revision) ||\n> +\t\t    get_oidf(&oid, \"%s^2\", revision))\n> +\t\t\tbreak;\n\nThis is to stop at the sentinel commit at the end.  Don't we want to\nmake sure that it actually has the expected shape of the sentinel?\n\nIOW, how robust do we try to be against being fed a random mergy\ncommit history (e.g., our 'master') and mistakenly adding nonsense\nstash entries as refs/stash@{<n>}?\n\n> +\t/*\n> +\t * Now, walk each entry, adding it to the stash as a normal stash\n> +\t * commit.\n> +\t */\n> +\tfor (i = items.nr - 1; i >= 0; i--) {\n> +\t\tunsigned long bufsize;\n> +\t\tconst char *p;\n> +\t\tconst struct object_id *oid = items.oid + i;\n> +\n> +\t\tthis = lookup_commit_reference(r, oid);\n> +\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n> +\t\tif (!buffer) {\n> +\t\t\tres = error(_(\"cannot read commit buffer for %s\"), oid_to_hex(oid));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tp = strstr(buffer, \"\\n\\n\");\n> +\t\tif (!p) {\n> +\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tp += 2;\n> +\t\tmsg = xmemdupz(p, bufsize - (p - buffer));\n\nHere, we could check \"git stash: \" string to make sure that it is as\nexpected in an exported stash we previously made, and abort, just\nlike the above \"cannot parse\" case.\n\n> +\t\trepo_unuse_commit_buffer(r, this, buffer);\n> +\t\tbuffer = NULL;\n> +\n> +\t\tif (do_store_stash(oid, msg, 1)) {\n\nShould we be making sure that the object named by \"oid\" does look\nlike a stash, like what is done in builtin/stash.c:get_stash_info(),\nor is assert_stash_like() called from do_store_stash() sufficient?\n\n"},{"id":"517709","messageId":"aB5iUmASx7BxgbAj@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"CALnO6CDDrd=XpkkTFYfAoiNcoG5q83Z8c+Ey4jKTBriA=M7TwQ@mail.gmail.com","subject":"Re: [PATCH v5 0/4] Importing and exporting stashes to refs","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-09T20:15:14Z","receivedAt":"2025-05-09T20:15:16Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-09 at 16:53:17, D. Ben Knoble wrote:\n> - Can't we \"git push <remote> stash[@{n}]:<branch>\" to share a stash somewhere?\n> - And then, doesn't \"git stash apply [--index] <arbitrary sha>\" work?\n> (At which point you could presumably create a new stash, though I'll\n> admit that's cumbersome relative to dedicated export/import.)\n\nI haven't tried, but it does certainly seem plausible that you can\nimport and export them in that way.\n\n> I can see how that doesn't help you quickly export a whole _chain_ of\n> stashes, so I'm not saying \"this series seems like the wrong tack\"\n> (far be it from me, who doesn't understand your use case, to say\n> that!). Rather, I'm confused about the inability to move a (single)\n> stash across machines.\n\nLet me explain the intended use case here.  At work, many people use\nGitHub Codespaces, which are throwaway development environments.  Since\none's whole set of stashes cannot be imported or exported, it's hard to\nuse stashes effectively in such a case, since they'd be deleted when you\ndestroyed the environment.  I like stashes a lot (my personal Git\ndevelopment repository has 153), so I want to synchronize all of them\nacross.\n\nThe other case is people who routinely work on multiple machines.\n(Remote or throwaway development environments, like GitHub Codespaces or\nDevcontainers, are just a special case of this.)  Many of these users\nwant to keep their working tree and other state across machines and lots\nof them rely on cloud syncing services, such as Dropbox, to do this,\nwhich often ends up corrupting the repository (as outlined in the FAQ).\nProviding a way to quickly and easily synchronize the working tree\nacross systems, including any stashes, is really important to encourage\nbest practices that don't result in data loss or have unpleasant\nsecurity issues (such as untrusted local config).\n\n> Unrelated question: Can we import arbitrary refs into stashes? That\n> is, what happens if the commit structure doesn't look right? (Maybe I\n> should go read the tests and see.)\n\nThat doesn't work because the commit used here has to have a fixed\nnumber of parents, since we need to keep track of the index and the\nworking tree.  Stash commits, even the regular ones used in the reflog,\nalways have to have a certain structure.\n\nIf you try to do that anyway, you get this message:\n\n    % git stash import HEAD\n    error: 3bf235c35ef51d01663f2ab9665026b05b8af1dd is not a valid exported stash commit\n\nI did try to avoid people accidentally destroying data.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"517710","messageId":"aB5irPERgQx20HJ3@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"xmqq8qn64mho.fsf@gitster.g","subject":"Re: [PATCH v5 0/4] Importing and exporting stashes to refs","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-09T20:16:44Z","receivedAt":"2025-05-09T20:16:46Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-09 at 01:10:59, Junio C Hamano wrote:\n> Thanks for resurrecting the thread.  Once this part gets sorted out\n> and the outside-reflog representation of stash becomes stable, the\n> next logical step would be to teach the \"git stash create\" and\n> friends to directly work on the new format, perhaps?  But first\n> things first---export+import would certainly be a logical first\n> step.\n\nThat could certainly be a follow-up, yes.  I don't currently have any\nplans to do that, but of course someone else could or I could be\nconvinced that it's something I should pick up.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"517742","messageId":"20250510172107.GA601540@coredump.intra.peff.net","threadId":"63424","inReplyTo":"20250508234458.3665894-5-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-05-10T17:21:07Z","receivedAt":"2025-05-10T17:21:15Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, May 08, 2025 at 11:44:57PM +0000, brian m. carlson wrote:\n\n> +test_expect_success 'stash export and import round-trip stashes' '\n> [...]\n> +\tgit stash export --to-ref refs/heads/foo &&\n\nHere we export to a name in the refs/heads/ namespace...\n\n> +test_expect_success 'stash export can accept specified stashes' '\n> [...]\n> +\tgit stash export --to-ref bar stash@{1} stash@{0} &&\n\n...but here we are writing to the top-level .git/bar. We do currently\nallow that, but there's been discussion of locking this down a bit\nfurther (requiring BAR or even BAR_HEAD at the top-level). Should this\nbe refs/heads/bar?\n\n> +test_expect_success 'stash can import and export zero stashes' '\n> [...]\n> +\tgit stash export --to-ref baz &&\n\nDitto here.\n\nI noticed because I have a patch series from last summer tightening\nthese rules (it got derailed by some conflicting work, and I've been\nmeaning to pick it back up). I can certainly adjust these tests as part\nof that series, but if you're re-rolling anyway, it might be nice to do\nit now.\n\n-Peff\n"},{"id":"517755","messageId":"CALnO6CAeVzh0OhjD8CkXKEydP4SNkCODfmirMYvzG8xEYXPA=Q@mail.gmail.com","threadId":"63424","inReplyTo":"aB5iUmASx7BxgbAj@tapette.crustytoothpaste.net","subject":"Re: [PATCH v5 0/4] Importing and exporting stashes to refs","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-05-10T19:13:06Z","receivedAt":"2025-05-10T19:13:19Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Fri, May 9, 2025 at 4:15 PM brian m. carlson\n<sandals@crustytoothpaste.net> wrote:\n>\n> On 2025-05-09 at 16:53:17, D. Ben Knoble wrote:\n> > - Can't we \"git push <remote> stash[@{n}]:<branch>\" to share a stash somewhere?\n> > - And then, doesn't \"git stash apply [--index] <arbitrary sha>\" work?\n> > (At which point you could presumably create a new stash, though I'll\n> > admit that's cumbersome relative to dedicated export/import.)\n>\n> I haven't tried, but it does certainly seem plausible that you can\n> import and export them in that way.\n>\n> > I can see how that doesn't help you quickly export a whole _chain_ of\n> > stashes, so I'm not saying \"this series seems like the wrong tack\"\n> > (far be it from me, who doesn't understand your use case, to say\n> > that!). Rather, I'm confused about the inability to move a (single)\n> > stash across machines.\n>\n> Let me explain the intended use case here.  At work, many people use\n> GitHub Codespaces, which are throwaway development environments.  Since\n> one's whole set of stashes cannot be imported or exported, it's hard to\n> use stashes effectively in such a case, since they'd be deleted when you\n> destroyed the environment.  I like stashes a lot (my personal Git\n> development repository has 153), so I want to synchronize all of them\n> across.\n>\n> The other case is people who routinely work on multiple machines.\n> (Remote or throwaway development environments, like GitHub Codespaces or\n> Devcontainers, are just a special case of this.)  Many of these users\n> want to keep their working tree and other state across machines and lots\n> of them rely on cloud syncing services, such as Dropbox, to do this,\n> which often ends up corrupting the repository (as outlined in the FAQ).\n> Providing a way to quickly and easily synchronize the working tree\n> across systems, including any stashes, is really important to encourage\n> best practices that don't result in data loss or have unpleasant\n> security issues (such as untrusted local config).\n>\n> > Unrelated question: Can we import arbitrary refs into stashes? That\n> > is, what happens if the commit structure doesn't look right? (Maybe I\n> > should go read the tests and see.)\n>\n> That doesn't work because the commit used here has to have a fixed\n> number of parents, since we need to keep track of the index and the\n> working tree.  Stash commits, even the regular ones used in the reflog,\n> always have to have a certain structure.\n>\n> If you try to do that anyway, you get this message:\n>\n>     % git stash import HEAD\n>     error: 3bf235c35ef51d01663f2ab9665026b05b8af1dd is not a valid exported stash commit\n>\n> I did try to avoid people accidentally destroying data.\n> --\n> brian m. carlson (they/them)\n> Toronto, Ontario, CA\n\nYou've answered all my questions, thanks!\n\n-- \nD. Ben Knoble\n"},{"id":"517762","messageId":"20250510212436.GA1139735@coredump.intra.peff.net","threadId":"63424","inReplyTo":"20250508234458.3665894-4-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-05-10T21:24:36Z","receivedAt":"2025-05-10T21:24:38Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, May 08, 2025 at 11:44:56PM +0000, brian m. carlson wrote:\n\n> +static int write_commit_with_parents(struct repository *r,\n> +\t\t\t\t     struct object_id *out,\n> +\t\t\t\t     const struct object_id *oid,\n> +\t\t\t\t     struct commit_list *parents)\n> +{\n> +\tsize_t author_len, committer_len;\n> +\tstruct commit *this;\n> +\tconst char *orig_author, *orig_committer;\n> +\tchar *author = NULL, *committer = NULL;\n> +\tconst char *buffer;\n> +\tunsigned long bufsize;\n> +\tconst char *p;\n> +\tstruct strbuf msg = STRBUF_INIT;\n> +\tint ret = 0;\n> +\tstruct ident_split id;\n> +\n> +\tthis = lookup_commit_reference(r, oid);\n> +\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n> +\torig_author = find_commit_header(buffer, \"author\", &author_len);\n> +\torig_committer = find_commit_header(buffer, \"committer\", &committer_len);\n> +\tif (split_ident_line(&id, orig_author, author_len) < 0 ||\n> +\t    split_ident_line(&id, orig_committer, committer_len) < 0) {\n> +\t\tret = error(_(\"invalid author or committer for %s\"), oid_to_hex(oid));\n> +\t\tgoto out;\n> +\t}\n> +\tp = strstr(buffer, \"\\n\\n\");\n> +\n> +\tif (!orig_author || !orig_committer || !p) {\n> +\t\tret = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n> +\t\tgoto out;\n> +\t}\n\nCoverity flagged this as a potential NULL deref. We check that\norig_author and orig_committer aren't NULL here, but we'll already have\nlooked at them via split_ident_line() above. Probably the error checks\nshould be reordered?\n\n-Peff\n"},{"id":"517763","messageId":"20250510213324.GB1139735@coredump.intra.peff.net","threadId":"63424","inReplyTo":"20250508234458.3665894-5-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-05-10T21:33:24Z","receivedAt":"2025-05-10T21:33:25Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, May 08, 2025 at 11:44:57PM +0000, brian m. carlson wrote:\n\n> +static int do_import_stash(struct repository *r, const char *rev)\n> [...]\n> +\tstruct oid_array items = OID_ARRAY_INIT;\n> +\tint i;\n> [...]\n> +\t/*\n> +\t * Walk the commit history, finding each stash entry, and load data into\n> +\t * the array.\n> +\t */\n> +\tfor (i = 0;; i++) {\n> +\t\tstruct object_id tree, oid;\n> +\t\tchar revision[GIT_MAX_HEXSZ + 1];\n> +\n> +\t\toid_to_hex_r(revision, &chain);\n> +\n> +\t\tif (get_oidf(&tree, \"%s:\", revision) ||\n> +\t\t    !oideq(&tree, r->hash_algo->empty_tree)) {\n> +\t\t\tres = error(_(\"%s is not a valid exported stash commit\"), revision);\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tif (get_oidf(&chain, \"%s^1\", revision) ||\n> +\t\t    get_oidf(&oid, \"%s^2\", revision))\n> +\t\t\tbreak;\n> +\t\toid_array_append(&items, &oid);\n> +\t}\n> +\n> +\t/*\n> +\t * Now, walk each entry, adding it to the stash as a normal stash\n> +\t * commit.\n> +\t */\n> +\tfor (i = items.nr - 1; i >= 0; i--) {\n\nCoverity complains about possible integer overflow here. It's an\ninteresting case. items.nr is a size_t, coming from the oid_array, and\nso it's unsigned. You use a signed int to iterate, which is needed to\ncatch walking past the zero. But in that initial assignment, the\nsubtraction of 1 is done on an unsigned value. If items.nr is zero, then\nit wraps around to a big (usually 64-bit) number, which is then\ntruncated and forced into a signed 32-bit int.\n\nI _think_ that usually works out, because the overflowed size_t is going\nto be all-bits-1, and then the truncation to int is also all-bits-1,\nwhich taken as a signed value is -1.\n\nProbably there's some light violation of the standard there, but I think\nit should be OK. But I thought I'd mention it in case I'm missing\nsomething.\n\n-Peff\n\nPS Sorry for the flurry of emails on a v5; this hit jch, so it got\n   sucked into my usual testing / analysis flow.\n"},{"id":"517792","messageId":"aCE2SjpCu5qlwyoK@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"xmqqzfflzhjv.fsf@gitster.g","subject":"Re: [PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-11T23:44:10Z","receivedAt":"2025-05-11T23:49:26Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-09 at 19:54:12, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> \n> > +static int do_import_stash(struct repository *r, const char *rev)\n> > +{\n> > +\tstruct object_id chain;\n> > +\tstruct oid_array items = OID_ARRAY_INIT;\n> > +\tint res = 0;\n> > +\tint i;\n> > +\tconst char *buffer = NULL;\n> > +\tstruct commit *this = NULL;\n> > +\tchar *msg = NULL;\n> > +\n> > +\tif (repo_get_oid(r, rev, &chain))\n> > +\t\treturn error(_(\"not a valid revision: %s\"), rev);\n> > +\n> > +\t/*\n> > +\t * Walk the commit history, finding each stash entry, and load data into\n> > +\t * the array.\n> > +\t */\n> > +\tfor (i = 0;; i++) {\n> > +\t\tstruct object_id tree, oid;\n> > +\t\tchar revision[GIT_MAX_HEXSZ + 1];\n> > +\n> > +\t\toid_to_hex_r(revision, &chain);\n> > +\n> > +\t\tif (get_oidf(&tree, \"%s:\", revision) ||\n> > +\t\t    !oideq(&tree, r->hash_algo->empty_tree)) {\n> > +\t\t\tres = error(_(\"%s is not a valid exported stash commit\"), revision);\n> > +\t\t\tgoto out;\n> > +\t\t}\n> > +\t\tif (get_oidf(&chain, \"%s^1\", revision) ||\n> > +\t\t    get_oidf(&oid, \"%s^2\", revision))\n> > +\t\t\tbreak;\n> \n> This is to stop at the sentinel commit at the end.  Don't we want to\n> make sure that it actually has the expected shape of the sentinel?\n> \n> IOW, how robust do we try to be against being fed a random mergy\n> commit history (e.g., our 'master') and mistakenly adding nonsense\n> stash entries as refs/stash@{<n>}?\n\nI'll try to make this more robust.  The advantage of the current\napproach is that we don't have to parse the buffer, but once we need to\ndo that, we might as well do the entire thing.\n\n> > +\t/*\n> > +\t * Now, walk each entry, adding it to the stash as a normal stash\n> > +\t * commit.\n> > +\t */\n> > +\tfor (i = items.nr - 1; i >= 0; i--) {\n> > +\t\tunsigned long bufsize;\n> > +\t\tconst char *p;\n> > +\t\tconst struct object_id *oid = items.oid + i;\n> > +\n> > +\t\tthis = lookup_commit_reference(r, oid);\n> > +\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n> > +\t\tif (!buffer) {\n> > +\t\t\tres = error(_(\"cannot read commit buffer for %s\"), oid_to_hex(oid));\n> > +\t\t\tgoto out;\n> > +\t\t}\n> > +\n> > +\t\tp = strstr(buffer, \"\\n\\n\");\n> > +\t\tif (!p) {\n> > +\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n> > +\t\t\tgoto out;\n> > +\t\t}\n> > +\n> > +\t\tp += 2;\n> > +\t\tmsg = xmemdupz(p, bufsize - (p - buffer));\n> \n> Here, we could check \"git stash: \" string to make sure that it is as\n> expected in an exported stash we previously made, and abort, just\n> like the above \"cannot parse\" case.\n\nThis part doesn't have the prefix.  That's the part above, but I'll add\nthe check there.  This part instead is the actual stash commit and we're\nextracting the message to put into the reflog.\n\n> Should we be making sure that the object named by \"oid\" does look\n> like a stash, like what is done in builtin/stash.c:get_stash_info(),\n> or is assert_stash_like() called from do_store_stash() sufficient?\n\nI think `assert_stash_like` should be fine.  I don't have any better\nchecking to do than that, since we don't have any more checks to do.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"517810","messageId":"67ba7cf4-e6b9-4c05-9960-e95fdcd4ed97@gmail.com","threadId":"63424","inReplyTo":"20250508234458.3665894-4-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-05-12T09:10:28Z","receivedAt":"2025-05-12T09:10:31Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi brian\n\nOn 09/05/2025 00:44, brian m. carlson wrote:\n> A common user problem is how to sync in-progress work to another\n> machine.  Users currently must use some sort of transfer of the working\n> tree, which poses security risks and also necessarily causes the index\n> to become dirty.  The experience is suboptimal and frustrating for\n> users.\n> \n> A reasonable idea is to use the stash for this purpose, but the stash is\n> stored in the reflog, not in a ref, and as such it cannot be pushed or\n> pulled. \n\nI think the commit message would be more convincing if it concentrated\non the need to import / export chains of stashes and the convenience of\nhaving a dedicated command to import a stash as one can export a single\nstash with\n\n     git push <remote> refs/stash@{<n>}:refs/exported-stash\n\nand then import it with\n\n     git pull <remote> refs/exported-stash\n     git update-ref refs/stash FETCH_HEAD\n\nHaving said that I do agree that adding these new commands is a good\nidea.\n\n> @@ -154,6 +155,12 @@ store::\n>   \treflog.  This is intended to be useful for scripts.  It is\n>   \tprobably not the command you want to use; see \"push\" above.\n>   \n> +export ( --print | --to-ref <ref> ) [<stash>...]::\n\nI was surprised to see that --to-ref does not take an argument. I think\nthat is confusing and it would be better to use OPT_STRING() rather than\nOPT_CMDMODE() and manually check that only one of \"--print\" or\n\"--to-ref\" is given. In the end it is no more work because now you have\nto manually check that we have a ref when \"--to-ref\" is given.\n\nI was also surprised to find that \"git stash export HEAD\" did not error\nout. I was looking for a function that checked the topology of a stash\ncommit and could not find one so I thought I see how difficult it was to\nadd that. The fixup commit below adds a new function to check that a\ncommit looks like a stash and refuses to export anything else. The patch\nadds a new function rather than using assert_stash_like() because\ndespite its name that function is pretty lax (any merge commit will\nsatisfy it) and is more concerned about filling out a stash_info struct.\n\nBest Wishes\n\nPhillip\n\n---- >8 ----\nFrom: Phillip Wood <phillip.wood@dunelm.org.uk>\nSubject: [PATCH] fixup! builtin/stash: provide a way to export stashes to a\n  ref\n\nReject commits that do not look like stashes.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\n  builtin/stash.c | 42 ++++++++++++++++++++++++++++++++++++++++++\n  1 file changed, 42 insertions(+)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex d8332af4017..02cf344ed96 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -180,6 +180,33 @@ static void free_stash_info(struct stash_info *info)\n  \tstrbuf_release(&info->revision);\n  }\n\n+static int check_stash_topology(struct repository *r, struct commit *stash)\n+{\n+\tstruct commit *p1, *p2, *p3 = NULL;\n+\n+\t/* stash must have two or three parents */\n+\tif (!stash->parents || !stash->parents->next ||\n+\t    (stash->parents->next->next && stash->parents->next->next->next))\n+\t\treturn -1;\n+\tp1 = stash->parents->item;\n+\tp2 = stash->parents->next->item;\n+\tif (stash->parents->next->next)\n+\t\tp3 = stash->parents->next->next->item;\n+\tif (repo_parse_commit(r, p1) || repo_parse_commit(r, p2) ||\n+\t    (p3 && repo_parse_commit(r, p3)))\n+\t\treturn -1;\n+\t/* p2 must have a single parent, p3 must have no parents */\n+\tif (!p2->parents || p2->parents->next || (p3 && p3->parents))\n+\t\treturn -1;\n+\tif (repo_parse_commit(r, p2->parents->item))\n+\t\treturn -1;\n+\t/* p2^1 must equal p1 */\n+\tif (!oideq(&p1->object.oid, &p2->parents->item->object.oid))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n  static void assert_stash_like(struct stash_info *info, const char *revision)\n  {\n  \tif (get_oidf(&info->b_commit, \"%s^1\", revision) ||\n@@ -2101,13 +2128,21 @@ static int do_export_stash(struct repository *r,\n  \t\t */\n  \t\tfor (i = 0; i < argc; i++) {\n  \t\t\tstruct object_id oid;\n+\t\t\tstruct commit *stash;\n+\n  \t\t\tif (parse_revision(&revision, argv[i], 1) ||\n  \t\t\t    get_oid_with_context(r, revision.buf,\n  \t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n  \t\t\t\t\t\t &oid, &unused)) {\n  \t\t\t\tres = error(_(\"unable to find stash entry %s\"), argv[i]);\n  \t\t\t\tgoto out;\n  \t\t\t}\n+\t\t\tstash = lookup_commit_reference(r, &oid);\n+\t\t\tif (!stash || check_stash_topology(r, stash)) {\n+\t\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t\t    argv[i]);\n+\t\t\t\tgoto out;\n+\t\t\t}\n  \t\t\toid_array_append(&items, &oid);\n  \t\t}\n  \t} else {\n@@ -2118,13 +2153,20 @@ static int do_export_stash(struct repository *r,\n  \t\tfor (i = 0;; i++) {\n  \t\t\tchar buf[32];\n  \t\t\tstruct object_id oid;\n+\t\t\tstruct commit *stash;\n\n  \t\t\tsnprintf(buf, sizeof(buf), \"%d\", i);\n  \t\t\tif (parse_revision(&revision, buf, 1) ||\n  \t\t\t    get_oid_with_context(r, revision.buf,\n  \t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n  \t\t\t\t\t\t &oid, &unused))\n  \t\t\t\tbreak;\n+\t\t\tstash = lookup_commit_reference(r, &oid);\n+\t\t\tif (!stash || check_stash_topology(r, stash)) {\n+\t\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t\t    revision.buf);\n+\t\t\t\tgoto out;\n+\t\t\t}\n  \t\t\toid_array_append(&items, &oid);\n  \t\t}\n  \t}\n-- \n2.49.0.300.g813f75aecee\n"},{"id":"517811","messageId":"94b26c62-c8da-49ba-a4f1-66da20956c0b@gmail.com","threadId":"63424","inReplyTo":"20250508234458.3665894-5-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-05-12T09:10:46Z","receivedAt":"2025-05-12T09:10:49Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi brian\n\nOn 09/05/2025 00:44, brian m. carlson wrote:\n> \n> @@ -1962,6 +1971,99 @@ static int write_commit_with_parents(struct repository *r,\n>   \treturn ret;\n>   }\n>   \n> +static int do_import_stash(struct repository *r, const char *rev)\n> +{\n> +\tstruct object_id chain;\n> +\tstruct oid_array items = OID_ARRAY_INIT;\n> +\tint res = 0;\n> +\tint i;\n> +\tconst char *buffer = NULL;\n> +\tstruct commit *this = NULL;\n> +\tchar *msg = NULL;\n> +\n> +\tif (repo_get_oid(r, rev, &chain))\n> +\t\treturn error(_(\"not a valid revision: %s\"), rev);\n> +\n> +\t/*\n> +\t * Walk the commit history, finding each stash entry, and load data into\n> +\t * the array.\n> +\t */\n> +\tfor (i = 0;; i++) {\n> +\t\tstruct object_id tree, oid;\n> +\t\tchar revision[GIT_MAX_HEXSZ + 1];\n> +\n> +\t\toid_to_hex_r(revision, &chain);\n> +\n> +\t\tif (get_oidf(&tree, \"%s:\", revision) ||\n> +\t\t    !oideq(&tree, r->hash_algo->empty_tree)) {\n> +\t\t\tres = error(_(\"%s is not a valid exported stash commit\"), revision);\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tif (get_oidf(&chain, \"%s^1\", revision) ||\n> +\t\t    get_oidf(&oid, \"%s^2\", revision))\n> +\t\t\tbreak;\n> +\t\toid_array_append(&items, &oid);\n> +\t}\n\nThis loop could use some improvement - it does not use the loop\nvariable, it accepts any commit with an empty tree as a valid exported\nstash, it is pretty lax about checking that the commits in the chain\nhave either zero or two parents, it does not check if the second parent\nlooks like a stash and it is forever converting between strings and\nobject_ids. I think it would be better to loop over commits rather than\nobject ids then you can walk the history using the pointers to the\nparent commits. Something like\n\n\tif (repo_get_oid(r, rev, &chain))\n\t\treturn error(_(\"not a valid revision: %s\"), rev);\n\n\tthis = lookup_commit_reference(r, &chain);\n\tif (!this)\n\t\treturn error(_(\"not a commit: %s\"), rev);\n\t/*\n\t * Walk the commit history, finding each stash entry, and load data into\n\t * the array.\n\t */\n\tfor (;;) {\n\t\tstruct commit *stash;\n\t\tstruct tree *tree = repo_get_commit_tree(r, this);\n\n\t\tif (!tree ||\n\t\t    !oideq(&tree->object.oid, r->hash_algo->empty_tree) ||\n\t\t    (this->parents &&\n\t\t     (!this->parents->next || this->parents->next->next))) {\n\t\t\tres = error(_(\"%s is not a valid exported stash commit\"),\n\t\t\t\t    oid_to_hex(&this->object.oid));\n\t\t\tgoto out;\n\t\t}\n\t\tif (!this->parents)\n\t\t\tbreak;\n\t\tstash = this->parents->next->item;\n\t\tif (repo_parse_commit(r, this->parents->item) ||\n\t\t    repo_parse_commit(r, stash)) {\n\t\t\tres = error(_(\"cannot parse parents of commit: %s\"),\n\t\t\t\t     oid_to_hex(&this->object.oid));\n\t\t\tgoto out;\n\t\t}\n\t\tif (check_stash_topology(r, stash)) {\n\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n\t\t\t\t    oid_to_hex(&stash->object.oid));\n\t\t\tgoto out;\n\t\t}\n\t\t/* TODO:\n\t\t *  - store commits, not objects\n\t\t */\n\t\toid_array_append(&items, &this->parents->next->item->object.oid);\n\t\tthis = this->parents->item;\n\t}\n\nI've appended a fixup commit below that applies on top of your\npatch. The fixups for this patch and the previous one can be fetched\nwith\n\n   git fetch https://github.com/phillipwood/git.git bc/stash-import-export-fixups\n\nif you want to use them.\n\nBest Wishes\n\nPhillip\n\n---- >8 ----\nFrom: Phillip Wood <phillip.wood@dunelm.org.uk>\nSubject: [PATCH] fixup! builtin/stash: provide a way to import stashes from a\n  ref\n\nStrengthen the checks on imported commits to ensure that the chain of\nimported stashes consists of commits with exactly two parents where the\nfirst parent is either the root commit or another imported stash commit\nand the second parent looks like a stash commit.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\n  builtin/stash.c | 43 +++++++++++++++++++++++++++++++------------\n  1 file changed, 31 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 02cf344ed9..7d3a8c03a0 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -2011,25 +2011,44 @@ static int do_import_stash(struct repository *r, const char *rev)\n  \tif (repo_get_oid(r, rev, &chain))\n  \t\treturn error(_(\"not a valid revision: %s\"), rev);\n  \n+\tthis = lookup_commit_reference(r, &chain);\n+\tif (!this)\n+\t\treturn error(_(\"not a commit: %s\"), rev);\n  \t/*\n  \t * Walk the commit history, finding each stash entry, and load data into\n  \t * the array.\n  \t */\n-\tfor (i = 0;; i++) {\n-\t\tstruct object_id tree, oid;\n-\t\tchar revision[GIT_MAX_HEXSZ + 1];\n-\n-\t\toid_to_hex_r(revision, &chain);\n-\n-\t\tif (get_oidf(&tree, \"%s:\", revision) ||\n-\t\t    !oideq(&tree, r->hash_algo->empty_tree)) {\n-\t\t\tres = error(_(\"%s is not a valid exported stash commit\"), revision);\n+\tfor (;;) {\n+\t\tstruct commit *stash;\n+\t\tstruct tree *tree = repo_get_commit_tree(r, this);\n+\n+\t\tif (!tree ||\n+\t\t    !oideq(&tree->object.oid, r->hash_algo->empty_tree) ||\n+\t\t    (this->parents &&\n+\t\t     (!this->parents->next || this->parents->next->next))) {\n+\t\t\tres = error(_(\"%s is not a valid exported stash commit\"),\n+\t\t\t\t    oid_to_hex(&this->object.oid));\n  \t\t\tgoto out;\n  \t\t}\n-\t\tif (get_oidf(&chain, \"%s^1\", revision) ||\n-\t\t    get_oidf(&oid, \"%s^2\", revision))\n+\t\tif (!this->parents)\n  \t\t\tbreak;\n-\t\toid_array_append(&items, &oid);\n+\t\tstash = this->parents->next->item;\n+\t\tif (repo_parse_commit(r, this->parents->item) ||\n+\t\t    repo_parse_commit(r, stash)) {\n+\t\t\tres = error(_(\"cannot parse parents of commit: %s\"),\n+\t\t\t\t     oid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\t\tif (check_stash_topology(r, stash)) {\n+\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t    oid_to_hex(&stash->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\t\t/* TODO:\n+\t\t *  - store commits, not objects\n+\t\t */\n+\t\toid_array_append(&items, &this->parents->next->item->object.oid);\n+\t\tthis = this->parents->item;\n  \t}\n  \n  \t/*\n-- \n2.49.0.300.g813f75aecee\n\n"},{"id":"517832","messageId":"xmqq5xi6xaov.fsf@gitster.g","threadId":"63424","inReplyTo":"20250510172107.GA601540@coredump.intra.peff.net","subject":"Re: [PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-12T12:42:08Z","receivedAt":"2025-05-12T12:42:11Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> On Thu, May 08, 2025 at 11:44:57PM +0000, brian m. carlson wrote:\n>\n>> +test_expect_success 'stash export and import round-trip stashes' '\n>> [...]\n>> +\tgit stash export --to-ref refs/heads/foo &&\n>\n> Here we export to a name in the refs/heads/ namespace...\n>\n>> +test_expect_success 'stash export can accept specified stashes' '\n>> [...]\n>> +\tgit stash export --to-ref bar stash@{1} stash@{0} &&\n>\n> ...but here we are writing to the top-level .git/bar. We do currently\n> allow that, but there's been discussion of locking this down a bit\n> further (requiring BAR or even BAR_HEAD at the top-level). Should this\n> be refs/heads/bar?\n>\n>> +test_expect_success 'stash can import and export zero stashes' '\n>> [...]\n>> +\tgit stash export --to-ref baz &&\n>\n> Ditto here.\n>\n> I noticed because I have a patch series from last summer tightening\n> these rules (it got derailed by some conflicting work, and I've been\n> meaning to pick it back up). I can certainly adjust these tests as part\n> of that series, but if you're re-rolling anyway, it might be nice to do\n> it now.\n\nTrue, and exported one is a sort-of-normal-looking isolated history,\nso it does not have any strong reason to be at the top level.  But I\nam curious what your plans are to deal with .git/refs/stash itself?\n\nThanks.\n"},{"id":"517837","messageId":"20250512125825.GA1191360@coredump.intra.peff.net","threadId":"63424","inReplyTo":"xmqq5xi6xaov.fsf@gitster.g","subject":"Re: [PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-05-12T12:58:25Z","receivedAt":"2025-05-12T12:58:35Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, May 12, 2025 at 05:42:08AM -0700, Junio C Hamano wrote:\n\n> > I noticed because I have a patch series from last summer tightening\n> > these rules (it got derailed by some conflicting work, and I've been\n> > meaning to pick it back up). I can certainly adjust these tests as part\n> > of that series, but if you're re-rolling anyway, it might be nice to do\n> > it now.\n> \n> True, and exported one is a sort-of-normal-looking isolated history,\n> so it does not have any strong reason to be at the top level.  But I\n> am curious what your plans are to deal with .git/refs/stash itself?\n\nMy series is only about the absolute top-level, outside of refs/. So\n\"refs/stash\" is OK, but \"stash\" is not.\n\n-Peff\n"},{"id":"517860","messageId":"xmqqplgdvn8v.fsf@gitster.g","threadId":"63424","inReplyTo":"67ba7cf4-e6b9-4c05-9960-e95fdcd4ed97@gmail.com","subject":"Re: [PATCH v5 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-12T15:53:52Z","receivedAt":"2025-05-12T15:53:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> I think the commit message would be more convincing if it concentrated\n> on the need to import / export chains of stashes and the convenience of\n> having a dedicated command to import a stash as one can export a single\n> stash with\n>\n>     git push <remote> refs/stash@{<n>}:refs/exported-stash\n>\n> and then import it with\n>\n>     git pull <remote> refs/exported-stash\n>     git update-ref refs/stash FETCH_HEAD\n\nOooh, then as long as the other side has enabled reflog, you could\neven do\n\n\tfor n in $(seq 0 N)\n\tdo\n\t\tgit push <remote> +refs/stash@{$n}:refs/stash\n\tdone\n\nand we can call it done?  ;-)\n\n> Having said that I do agree that adding these new commands is a good\n> idea.\n\nYeah, I do not think there is no reason to oppose having an export/import\npair.\n"},{"id":"517862","messageId":"xmqqldr1vmop.fsf@gitster.g","threadId":"63424","inReplyTo":"20250512125825.GA1191360@coredump.intra.peff.net","subject":"Re: [PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-12T16:05:58Z","receivedAt":"2025-05-12T16:06:01Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> On Mon, May 12, 2025 at 05:42:08AM -0700, Junio C Hamano wrote:\n>\n>> > I noticed because I have a patch series from last summer tightening\n>> > these rules (it got derailed by some conflicting work, and I've been\n>> > meaning to pick it back up). I can certainly adjust these tests as part\n>> > of that series, but if you're re-rolling anyway, it might be nice to do\n>> > it now.\n>> \n>> True, and exported one is a sort-of-normal-looking isolated history,\n>> so it does not have any strong reason to be at the top level.  But I\n>> am curious what your plans are to deal with .git/refs/stash itself?\n>\n> My series is only about the absolute top-level, outside of refs/. So\n> \"refs/stash\" is OK, but \"stash\" is not.\n\nAh, OK.  It would be good to forbid things outside refs/, and\ntightly control the top level, so $GIT_DIR/config and\n$GIT_DIR/objects/[0-9a-f]{2}/[0-9a-f]{38} won't be clobbered.\n\nrefs/stash is probably fine.  Creating refs/head to nuke all local\nbranches might also be something we want to protect against, though.\n\nThanks.\n"},{"id":"517911","messageId":"aCJl5gr_ebxvRhiD@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"20250510172107.GA601540@coredump.intra.peff.net","subject":"Re: [PATCH v5 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-12T21:19:34Z","receivedAt":"2025-05-12T21:19:36Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-10 at 17:21:07, Jeff King wrote:\n> I noticed because I have a patch series from last summer tightening\n> these rules (it got derailed by some conflicting work, and I've been\n> meaning to pick it back up). I can certainly adjust these tests as part\n> of that series, but if you're re-rolling anyway, it might be nice to do\n> it now.\n\nI've fixed these in my branch and will include them in v6.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"518694","messageId":"20250522185524.18398-1-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250508234458.3665894-1-sandals@crustytoothpaste.net","subject":"[PATCH] Makefile: avoid constant rebuilds with compilation database","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-22T18:55:18Z","receivedAt":"2025-05-22T18:56:14Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Many contributors to software use a Language Server Protocol\nimplementation to allow their editor to learn structural information\nabout the code they write and provide additional features, such as\njumping to the declaration or definition of a function or type.  In C,\nthe usual implementation is clangd, which requires compiling with clang.\n\nBecause C and C++ projects lack a standard file system layout and build\nsystem, unlike languages such as Rust and Go, clangd requires a\ncompilation database to be generated by the clang compiler in order to\npass the proper compilation flags and discover all of the files\nnecessary to make the LSP work.  This is done by setting\nGENERATE_COMPILATION_DATABASE to \"yes\".\n\nHowever, when that's enabled and the user runs \"make\" a second time,\nall of the files are re-compiled, which is inconvenient for contributors\nto Git, since it makes small changes or rebases recompile the entirety\nof the codebase.  This happens because the directory holding the\ncompilation database is updated anytime an object is built, so its\nmodification date will always be newer than the first object built.\n\nTo solve this, use the same trick we do just above for the .depend\ndirectory and filter the compilation database directory out if it\nalready exists, which avoids making it a target to be built.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\nHelped-by: Philippe Blain <levraiphilippeblain@gmail.com>\n---\n Makefile | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex 8a7f1c7654..8f38de8ebf 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2804,7 +2804,7 @@ endif\n compdb_dir = compile_commands\n \n ifeq ($(GENERATE_COMPILATION_DATABASE),yes)\n-missing_compdb_dir = $(compdb_dir)\n+missing_compdb_dir = $(filter-out $(wildcard $(compdb_dir)), $(compdb_dir))\n $(missing_compdb_dir):\n \t@mkdir -p $@\n \n"},{"id":"518693","messageId":"20250522185524.18398-5-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250522185524.18398-1-sandals@crustytoothpaste.net","subject":"[PATCH v6 3/5] builtin/stash: factor out revision parsing into a function","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-22T18:55:22Z","receivedAt":"2025-05-22T18:56:15Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"We allow several special forms of stash names in this code.  In the\nfuture, we'll want to allow these same forms without parsing a stash\ncommit, so let's refactor this code out into a function for reuse.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n builtin/stash.c | 33 ++++++++++++++++++++++-----------\n 1 file changed, 22 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex cfbd92852a..ab491d5ff6 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -169,6 +169,25 @@ static void assert_stash_like(struct stash_info *info, const char *revision)\n \t\tdie(_(\"'%s' is not a stash-like commit\"), revision);\n }\n \n+static int parse_stash_revision(struct strbuf *revision, const char *commit, int quiet)\n+{\n+\tstrbuf_reset(revision);\n+\tif (!commit) {\n+\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n+\t\t\tif (!quiet)\n+\t\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tstrbuf_addf(revision, \"%s@{0}\", ref_stash);\n+\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n+\t\tstrbuf_addf(revision, \"%s@{%s}\", ref_stash, commit);\n+\t} else {\n+\t\tstrbuf_addstr(revision, commit);\n+\t}\n+\treturn 0;\n+}\n+\n static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n {\n \tint ret;\n@@ -196,17 +215,9 @@ static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n \tif (argc == 1)\n \t\tcommit = argv[0];\n \n-\tif (!commit) {\n-\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n-\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tstrbuf_addf(&info->revision, \"%s@{0}\", ref_stash);\n-\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n-\t\tstrbuf_addf(&info->revision, \"%s@{%s}\", ref_stash, commit);\n-\t} else {\n-\t\tstrbuf_addstr(&info->revision, commit);\n+\tstrbuf_init(&info->revision, 0);\n+\tif (parse_stash_revision(&info->revision, commit, 0)) {\n+\t\treturn -1;\n \t}\n \n \trevision = info->revision.buf;\n"},{"id":"518695","messageId":"20250522185524.18398-4-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250522185524.18398-1-sandals@crustytoothpaste.net","subject":"[PATCH v6 2/5] reflog-walk: expose read_complete_reflog","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-22T18:55:21Z","receivedAt":"2025-05-22T18:56:15Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"In a future commit, we'll use this function and the corresponding free\nfunction to read the entire reflog.  Expose it in the header so we can\ndo so.\n\nInclude the appropriate header files so that our header is complete.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n reflog-walk.c | 17 ++---------------\n reflog-walk.h | 18 ++++++++++++++++++\n 2 files changed, 20 insertions(+), 15 deletions(-)\n\ndiff --git a/reflog-walk.c b/reflog-walk.c\nindex c7070b13b0..b7a9d70966 100644\n--- a/reflog-walk.c\n+++ b/reflog-walk.c\n@@ -9,19 +9,6 @@\n #include \"string-list.h\"\n #include \"reflog-walk.h\"\n \n-struct complete_reflogs {\n-\tchar *ref;\n-\tchar *short_ref;\n-\tstruct reflog_info {\n-\t\tstruct object_id ooid, noid;\n-\t\tchar *email;\n-\t\ttimestamp_t timestamp;\n-\t\tint tz;\n-\t\tchar *message;\n-\t} *items;\n-\tint nr, alloc;\n-};\n-\n static int read_one_reflog(struct object_id *ooid, struct object_id *noid,\n \t\tconst char *email, timestamp_t timestamp, int tz,\n \t\tconst char *message, void *cb_data)\n@@ -41,7 +28,7 @@ static int read_one_reflog(struct object_id *ooid, struct object_id *noid,\n \treturn 0;\n }\n \n-static void free_complete_reflog(struct complete_reflogs *array)\n+void free_complete_reflog(struct complete_reflogs *array)\n {\n \tint i;\n \n@@ -64,7 +51,7 @@ static void complete_reflogs_clear(void *util, const char *str UNUSED)\n \tfree_complete_reflog(array);\n }\n \n-static struct complete_reflogs *read_complete_reflog(const char *ref)\n+struct complete_reflogs *read_complete_reflog(const char *ref)\n {\n \tstruct complete_reflogs *reflogs =\n \t\txcalloc(1, sizeof(struct complete_reflogs));\ndiff --git a/reflog-walk.h b/reflog-walk.h\nindex 989583dc55..8f0640f662 100644\n--- a/reflog-walk.h\n+++ b/reflog-walk.h\n@@ -1,9 +1,24 @@\n #ifndef REFLOG_WALK_H\n #define REFLOG_WALK_H\n \n+#include \"git-compat-util.h\"\n+#include \"hash.h\"\n+\n struct commit;\n struct reflog_walk_info;\n struct date_mode;\n+struct complete_reflogs {\n+\tchar *ref;\n+\tchar *short_ref;\n+\tstruct reflog_info {\n+\t\tstruct object_id ooid, noid;\n+\t\tchar *email;\n+\t\ttimestamp_t timestamp;\n+\t\tint tz;\n+\t\tchar *message;\n+\t} *items;\n+\tint nr, alloc;\n+};\n \n void init_reflog_walk(struct reflog_walk_info **info);\n void reflog_walk_info_release(struct reflog_walk_info *info);\n@@ -24,4 +39,7 @@ int reflog_walk_empty(struct reflog_walk_info *walk);\n \n struct commit *next_reflog_entry(struct reflog_walk_info *reflog_info);\n \n+void free_complete_reflog(struct complete_reflogs *array);\n+struct complete_reflogs *read_complete_reflog(const char *ref);\n+\n #endif\n"},{"id":"518696","messageId":"20250522185524.18398-2-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250522185524.18398-1-sandals@crustytoothpaste.net","subject":"[PATCH v6 0/5] Importing and exporting stashes to refs","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-22T18:55:19Z","receivedAt":"2025-05-22T18:56:15Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Stashes are currently stored using the reflog in a given repository.\nThis is an interesting and novel way to handle them, but there is no way\nto easily move a set of stashes across machines.  For example, groups of\nstashes cannot be bundled, pushed, or fetched.\n\nLet's solve this problem by allowing users to import and export stashes\nto a chain of commits.  The commits used in a stash export contain two\nparents: one which is the pointer to the next exported stash (or to an\nempty commit with no parents if there are no more) and the second is the\nstash commit that would normally be stored in the reflog.\n\nI will need Phillip's sign-off for the last patch, since his fixup patch\ndidn't include it.\n\nOriginal thread at message-ID: <20220310173236.4165310-1-sandals@crustytoothpaste.net>\n\nChanges from v5:\n* Rename `parse_revision`.\n* Remove extra call to `free_stash_info`.\n* Fix parsing of existing commit.\n* Add more validation of imported stash commits.\n* Add more tests for improved validation of imported stash commits.\n* Explicitly cast `items.nr` and make the iteration counter an `ssize_t`\n  to avoid casting problems.\n* Don't require a trailing `\\n\\n` in commit messages.\n* Use `read_complete_reflog` to walk reflogs.\n* Be more defensive when using `lookup_commit_reference`.\n* Apply parts of Phillip's patches for improved robustness.\n* Update commit message to explain additional use cases.\n* Use `OPT_STRING` for `--to-ref`.\n\nChanges from v4:\n* Fix another use of oid_array.\n* Fix various memory leaks.\n* Fix a segfault which appeared after a rebase.\n* Use strstr for commits since we don't need to worry about NUL.\n* Added some additional tests.\n* Verify the ident values we're using to avoid using bad values.\n* Various other code cleanups.\n* Rebase on `master`.\n\nChanges from v3:\n* Fix strbuf handling to avoid leaks and generally be more sensible.\n* Make use of the error return code more often.\n* Use oid_array.\n* Tidy various parts of the code and fix long lines.\n* Simplify tests using git tag.\n* Shorten and tidy tests.\n* Add an additional test covering the base commit OID and importing and\n  exporting empty stashes.\n\nChanges from v2:\n* Fix uninitialized strbuf.\n* Avoid C99-style initializations.\n\nChanges from v1:\n* Change storage format as suggested by Junio.\n* Rename to GIT_OID_GENTLY.\n* Remove unnecessary initializations.\n* Use ALLOC_GROW_BY.\n* Ensure completely reproducible exports.\n* Avoid size_t.\n* Various other code cleanups.\n\nbrian m. carlson (5):\n  object-name: make get_oid quietly return an error\n  reflog-walk: expose read_complete_reflog\n  builtin/stash: factor out revision parsing into a function\n  builtin/stash: provide a way to export stashes to a ref\n  builtin/stash: provide a way to import stashes from a ref\n\n Documentation/git-stash.adoc |  29 ++-\n builtin/stash.c              | 450 ++++++++++++++++++++++++++++++++++-\n hash.h                       |   1 +\n object-name.c                |   6 +-\n reflog-walk.c                |  17 +-\n reflog-walk.h                |  18 ++\n t/t3903-stash.sh             |  94 ++++++++\n 7 files changed, 587 insertions(+), 28 deletions(-)\n\n"},{"id":"518697","messageId":"20250522185524.18398-3-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250522185524.18398-1-sandals@crustytoothpaste.net","subject":"[PATCH v6 1/5] object-name: make get_oid quietly return an error","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-22T18:55:20Z","receivedAt":"2025-05-22T18:56:15Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"A reasonable person looking at the signature and usage of get_oid and\nfriends might conclude that in the event of an error, it always returns\n-1.  However, this is not the case.  Instead, get_oid_basic dies if we\ngo too far back into the history of a reflog (or, when quiet, simply\nexits).\n\nThis is not especially useful, since in many cases, we might want to\nhandle this error differently.  Let's add a flag here to make it just\nreturn -1 like elsewhere in these code paths.\n\nNote that we cannot make this behavior the default, since we have many\nother codepaths that rely on the existing behavior, including in tests.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n hash.h        | 1 +\n object-name.c | 6 +++++-\n 2 files changed, 6 insertions(+), 1 deletion(-)\n\ndiff --git a/hash.h b/hash.h\nindex d6422ddf45..ec594c63a6 100644\n--- a/hash.h\n+++ b/hash.h\n@@ -216,6 +216,7 @@ struct object_id {\n #define GET_OID_REQUIRE_PATH         010000\n #define GET_OID_HASH_ANY             020000\n #define GET_OID_SKIP_AMBIGUITY_CHECK 040000\n+#define GET_OID_GENTLY              0100000\n \n #define GET_OID_DISAMBIGUATORS \\\n \t(GET_OID_COMMIT | GET_OID_COMMITTISH | \\\ndiff --git a/object-name.c b/object-name.c\nindex 9288b2dd24..851858975f 100644\n--- a/object-name.c\n+++ b/object-name.c\n@@ -1081,13 +1081,17 @@ static int get_oid_basic(struct repository *r, const char *str, int len,\n \t\t\t\t * still fill in the oid with the \"old\" value,\n \t\t\t\t * which we can use.\n \t\t\t\t */\n-\t\t\t} else {\n+\t\t\t} else if (!(flags & GET_OID_GENTLY)) {\n \t\t\t\tif (flags & GET_OID_QUIETLY) {\n \t\t\t\t\texit(128);\n \t\t\t\t}\n \t\t\t\tdie(_(\"log for '%.*s' only has %d entries\"),\n \t\t\t\t    len, str, co_cnt);\n \t\t\t}\n+\t\t\tif (flags & GET_OID_GENTLY) {\n+\t\t\t\tfree(real_ref);\n+\t\t\t\treturn -1;\n+\t\t\t}\n \t\t}\n \t}\n \n"},{"id":"518698","messageId":"20250522185524.18398-7-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250522185524.18398-1-sandals@crustytoothpaste.net","subject":"[PATCH v6 5/5] builtin/stash: provide a way to import stashes from a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-22T18:55:24Z","receivedAt":"2025-05-22T18:56:18Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Now that we have a way to export stashes to a ref, let's provide a way\nto import them from such a ref back to the stash.  This works much the\nway the export code does, except that we strip off the first parent\nchain commit and then store each resulting commit back to the stash.\n\nWe don't clear the stash first and instead add the specified stashes to\nthe top of the stash.  This is because users may want to export just a\nfew stashes, such as to share a small amount of work in progress with a\ncolleague, and it would be undesirable for the receiving user to lose\nall of their data.  For users who do want to replace the stash, it's\neasy to do to: simply run \"git stash clear\" first.\n\nWe specifically rely on the fact that we'll produce identical stash\ncommits on both sides in our tests.  This provides a cheap,\nstraightforward check for our tests and also makes it easy for users to\nsee if they already have the same data in both repositories.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\nI'll need Phillip's sign-off for this patch.\n\n Documentation/git-stash.adoc |   7 ++\n builtin/stash.c              | 167 +++++++++++++++++++++++++++++++++++\n t/t3903-stash.sh             |  94 ++++++++++++++++++++\n 3 files changed, 268 insertions(+)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex e8efd43ba4..f8193b712e 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -24,6 +24,7 @@ SYNOPSIS\n 'git stash' create [<message>]\n 'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>\n 'git stash' export (--print | --to-ref <ref>) [<stash>...]\n+'git stash' import <commit>\n \n DESCRIPTION\n -----------\n@@ -161,6 +162,12 @@ export ( --print | --to-ref <ref> ) [<stash>...]::\n \ta chain of commits which can be transferred using the normal fetch and\n \tpush mechanisms, then imported using the `import` subcommand.\n \n+import <commit>::\n+\n+\tImport the specified stashes from the specified commit, which must have been\n+\tcreated by `export`, and add them to the list of stashes.  To replace the\n+\texisting stashes, use `clear` first.\n+\n OPTIONS\n -------\n -a::\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex ca00663e36..59fcb461eb 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -61,6 +61,8 @@\n \tN_(\"git stash create [<message>]\")\n #define BUILTIN_STASH_EXPORT_USAGE \\\n \tN_(\"git stash export (--print | --to-ref <ref>) [<stash>...]\")\n+#define BUILTIN_STASH_IMPORT_USAGE \\\n+\tN_(\"git stash import <commit>\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -77,6 +79,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_CREATE_USAGE,\n \tBUILTIN_STASH_STORE_USAGE,\n \tBUILTIN_STASH_EXPORT_USAGE,\n+\tBUILTIN_STASH_IMPORT_USAGE,\n \tNULL\n };\n \n@@ -135,6 +138,10 @@ static const char * const git_stash_export_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_import_usage[] = {\n+\tBUILTIN_STASH_IMPORT_USAGE,\n+\tNULL\n+};\n \n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n@@ -144,6 +151,7 @@ static struct strbuf stash_index_path = STRBUF_INIT;\n  * b_commit is set to the base commit\n  * i_commit is set to the commit containing the index tree\n  * u_commit is set to the commit containing the untracked files tree\n+ * c_commit is set to the first parent (chain commit) when importing and is otherwise unset\n  * w_tree is set to the working tree\n  * b_tree is set to the base tree\n  * i_tree is set to the index tree\n@@ -154,6 +162,7 @@ struct stash_info {\n \tstruct object_id b_commit;\n \tstruct object_id i_commit;\n \tstruct object_id u_commit;\n+\tstruct object_id c_commit;\n \tstruct object_id w_tree;\n \tstruct object_id b_tree;\n \tstruct object_id i_tree;\n@@ -1992,6 +2001,163 @@ static int write_commit_with_parents(struct repository *r,\n \treturn ret;\n }\n \n+static int do_import_stash(struct repository *r, const char *rev)\n+{\n+\tstruct object_id chain;\n+\tstruct oid_array items = OID_ARRAY_INIT;\n+\tint res = 0;\n+\tssize_t i;\n+\tconst char *buffer = NULL;\n+\tunsigned long bufsize;\n+\tstruct commit *this = NULL;\n+\tchar *msg = NULL;\n+\n+\tif (repo_get_oid(r, rev, &chain))\n+\t\treturn error(_(\"not a valid revision: %s\"), rev);\n+\n+\tthis = lookup_commit_reference(r, &chain);\n+\tif (!this)\n+\t\treturn error(_(\"not a commit: %s\"), rev);\n+\n+\t/*\n+\t * Walk the commit history, finding each stash entry, and load data into\n+\t * the array.\n+\t */\n+\tfor (;;) {\n+\t\tconst char *author, *committer;\n+\t\tsize_t author_len, committer_len;\n+\t\tconst char *p;\n+\t\tconst char *expected = \"git stash <git@stash> 1000684800 +0000\";\n+\t\tconst char *prefix = \"git stash: \";\n+\t\tstruct commit *stash;\n+\t\tstruct tree *tree = repo_get_commit_tree(r, this);\n+\n+\t\tif (!tree ||\n+\t\t    !oideq(&tree->object.oid, r->hash_algo->empty_tree) ||\n+\t\t    (this->parents &&\n+\t\t     (!this->parents->next || this->parents->next->next))) {\n+\t\t\tres = error(_(\"%s is not a valid exported stash commit\"),\n+\t\t\t\t\toid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\n+\t\tif (!this->parents) {\n+\t\t\t/*\n+\t\t\t * We don't have any parents.  Make sure this is our\n+\t\t\t * root commit.\n+\t\t\t */\n+\t\t\tauthor = find_commit_header(buffer, \"author\", &author_len);\n+\t\t\tcommitter = find_commit_header(buffer, \"committer\", &committer_len);\n+\n+\t\t\tif (!author || !committer) {\n+\t\t\t\terror(_(\"cannot parse commit %s\"), oid_to_hex(&this->object.oid));\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\n+\t\t\tif (author_len != strlen(expected) ||\n+\t\t\t    committer_len != strlen(expected) ||\n+\t\t\t    memcmp(author, expected, author_len) ||\n+\t\t\t    memcmp(committer, expected, committer_len)) {\n+\t\t\t\tres = error(_(\"found root commit %s with invalid data\"), oid_to_hex(&this->object.oid));\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tp = strstr(buffer, \"\\n\\n\");\n+\t\tif (!p) {\n+\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp += 2;\n+\t\tif (((size_t)(bufsize - (p - buffer)) < strlen(prefix)) ||\n+\t\t    memcmp(prefix, p, strlen(prefix))) {\n+\t\t\tres = error(_(\"found stash commit %s with unexpected prefix\"), oid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstash = this->parents->next->item;\n+\n+\t\tif (repo_parse_commit(r, this->parents->item) ||\n+\t\t    repo_parse_commit(r, stash)) {\n+\t\t\tres = error(_(\"cannot parse parents of commit: %s\"),\n+\t\t\t\t\toid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (check_stash_topology(r, stash)) {\n+\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t\toid_to_hex(&stash->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\t\tbuffer = NULL;\n+\t\toid_array_append(&items, &stash->object.oid);\n+\t\tthis = this->parents->item;\n+\t}\n+\n+\t/*\n+\t * Now, walk each entry, adding it to the stash as a normal stash\n+\t * commit.\n+\t */\n+\tfor (i = (ssize_t)items.nr - 1; i >= 0; i--) {\n+\t\tconst char *p;\n+\t\tconst struct object_id *oid = items.oid + i;\n+\n+\t\tthis = lookup_commit_reference(r, oid);\n+\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\t\tif (!buffer) {\n+\t\t\tres = error(_(\"cannot read commit buffer for %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp = strstr(buffer, \"\\n\\n\");\n+\t\tif (!p) {\n+\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp += 2;\n+\t\tmsg = xmemdupz(p, bufsize - (p - buffer));\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\t\tbuffer = NULL;\n+\n+\t\tif (do_store_stash(oid, msg, 1)) {\n+\t\t\tres = error(_(\"cannot save the stash for %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\t\tFREE_AND_NULL(msg);\n+\t}\n+out:\n+\tif (this && buffer)\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\toid_array_clear(&items);\n+\tfree(msg);\n+\n+\treturn res;\n+}\n+\n+static int import_stash(int argc, const char **argv, const char *prefix,\n+\t\t\tstruct repository *repo)\n+{\n+\tstruct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_import_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (argc != 1)\n+\t\tusage_msg_opt(\"a revision is required\", git_stash_import_usage, options);\n+\n+\treturn do_import_stash(repo, argv[0]);\n+}\n+\n static int do_export_stash(struct repository *r,\n \t\t\t   const char *ref,\n \t\t\t   int argc,\n@@ -2165,6 +2331,7 @@ int cmd_stash(int argc,\n \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n \t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n \t\tOPT_SUBCOMMAND(\"export\", &fn, export_stash),\n+\t\tOPT_SUBCOMMAND(\"import\", &fn, import_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex 74666ff3e4..c827709468 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -11,6 +11,13 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-unique-files.sh\n \n+test_expect_success 'setup' '\n+\ttest_oid_cache <<-EOF\n+\texport_base sha1:73c9bab443d1f88ac61aa533d2eeaaa15451239c\n+\texport_base sha256:f210fa6346e3e2ce047bdb570426b17075980c1ac01fec8fc4b75bd3ab4bcfe4\n+\tEOF\n+'\n+\n test_expect_success 'usage on cmd and subcommand invalid option' '\n \ttest_expect_code 129 git stash --invalid-option 2>usage &&\n \tgrep \"or: git stash\" usage &&\n@@ -1412,6 +1419,93 @@ test_expect_success 'stash --keep-index --include-untracked with empty tree' '\n \t)\n '\n \n+test_expect_success 'stash export and import round-trip stashes' '\n+\tgit reset &&\n+\t>untracked &&\n+\t>tracked1 &&\n+\t>tracked2 &&\n+\tgit add tracked* &&\n+\tgit stash -- &&\n+\t>subdir/untracked &&\n+\t>subdir/tracked1 &&\n+\t>subdir/tracked2 &&\n+\tgit add subdir/tracked* &&\n+\tgit stash --include-untracked -- subdir/ &&\n+\tgit tag t-stash0 stash@{0} &&\n+\tgit tag t-stash1 stash@{1} &&\n+\tsimple=$(git stash export --print) &&\n+\tgit stash clear &&\n+\tgit stash import \"$simple\" &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\ttest_cmp_rev stash@{1} t-stash1 &&\n+\tgit stash export --to-ref refs/heads/foo &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" foo: &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" foo^: &&\n+\ttest_cmp_rev t-stash0 foo^2 &&\n+\ttest_cmp_rev t-stash1 foo^^2 &&\n+\tgit log --first-parent --format=\"%s\" refs/heads/foo >log &&\n+\tgrep \"^git stash: \" log >log2 &&\n+\ttest_line_count = 13 log2 &&\n+\tgit stash clear &&\n+\tgit stash import foo &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\ttest_cmp_rev stash@{1} t-stash1\n+'\n+\n+test_expect_success 'stash import appends commits' '\n+\tgit log --format=oneline -g refs/stash >out &&\n+\tcat out out >out2 &&\n+\tgit stash import refs/heads/foo &&\n+\tgit log --format=oneline -g refs/stash >actual &&\n+\ttest_line_count = $(wc -l <out2) actual\n+'\n+\n+test_expect_success 'stash export can accept specified stashes' '\n+\tgit stash clear &&\n+\tgit stash import foo &&\n+\tgit stash export --to-ref refs/heads/bar stash@{1} stash@{0} &&\n+\tgit stash clear &&\n+\tgit stash import refs/heads/bar &&\n+\ttest_cmp_rev stash@{1} t-stash0 &&\n+\ttest_cmp_rev stash@{0} t-stash1 &&\n+\tgit log --format=oneline -g refs/stash >actual &&\n+\ttest_line_count = 2 actual\n+'\n+\n+test_expect_success 'stash can import and export zero stashes' '\n+\tgit stash clear &&\n+\tgit stash export --to-ref refs/heads/baz &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" baz: &&\n+\ttest_cmp_rev \"$(test_oid export_base)\" baz &&\n+\ttest_must_fail git rev-parse baz^1 &&\n+\tgit stash import baz &&\n+\ttest_must_fail git rev-parse refs/stash\n+'\n+\n+test_expect_success 'stash rejects invalid attempts to import commits' '\n+\tgit stash import foo &&\n+\ttest_must_fail git stash import HEAD 2>output &&\n+\toid=$(git rev-parse HEAD) &&\n+\tgrep \"$oid is not a valid exported stash commit\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\n+\tgit checkout --orphan orphan &&\n+\tgit commit-tree $(test_oid empty_tree) -p \"$oid\" -p \"$oid^\" -m \"\" >fake-commit &&\n+\tgit update-ref refs/heads/orphan \"$(cat fake-commit)\" &&\n+\toid=$(git rev-parse HEAD) &&\n+\ttest_must_fail git stash import orphan 2>output &&\n+\tgrep \"found stash commit $oid with unexpected prefix\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\n+\tgit checkout --orphan orphan2 &&\n+\tgit commit-tree $(test_oid empty_tree) -m \"\" >fake-commit &&\n+\tgit update-ref refs/heads/orphan2 \"$(cat fake-commit)\" &&\n+\toid=$(git rev-parse HEAD) &&\n+\ttest_must_fail git stash import orphan2 2>output &&\n+\tgrep \"found root commit $oid with invalid data\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0\n+'\n+\n test_expect_success 'stash apply should succeed with unmodified file' '\n \techo base >file &&\n \tgit add file &&\n"},{"id":"518699","messageId":"20250522185524.18398-6-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250522185524.18398-1-sandals@crustytoothpaste.net","subject":"[PATCH v6 4/5] builtin/stash: provide a way to export stashes to a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-22T18:55:23Z","receivedAt":"2025-05-22T18:56:18Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"A common user problem is how to sync in-progress work to another\nmachine.  Users currently must use some sort of transfer of the working\ntree, which poses security risks and also necessarily causes the index\nto become dirty.  The experience is suboptimal and frustrating for\nusers.\n\nA reasonable idea is to use the stash for this purpose, but the stash is\nstored in the reflog, not in a ref, and as such it cannot be pushed or\npulled.  This also means that it cannot be saved into a bundle or\npreserved elsewhere, which is a problem when using throwaway development\nenvironments.\n\nIn addition, users often want to replicate stashes across machines, such\nas when they must use multiple machines or when they use throwaway dev\nenvironments, such as those based on the Devcontainer spec, where they\nmight otherwise lose various in-progress work.\n\nLet's solve this problem by allowing the user to export the stash to a\nref (or, to just write it into the repository and print the hash, à la\ngit commit-tree).  Introduce git stash export, which writes a chain of\ncommits where the first parent is always a chain to the previous stash,\nor to a single, empty commit (for the final item) and the second is the\nstash commit normally written to the reflog.\n\nIterate over each stash from topmost to bottomost, looking up the data\nfor each one, and then create the chain from the single empty commit\nback up in reverse order.  Generate a predictable empty commit so our\nbehavior is reproducible.  Create a useful commit message, preserving\nthe author and committer information, to help users identify stash\ncommits when viewing them as normal commits.\n\nIf the user has specified specific stashes they'd like to export\ninstead, use those instead of iterating over all of the stashes.\n\nAs part of this, specifically request quiet behavior when looking up the\nOID for a revision because we will eventually hit a revision that\ndoesn't exist and we don't want to die when that occurs.\n\nWhen exporting stashes, be sure to verify that they look like valid\nstashes and don't contain invalid data.  This will help avoid failures\non import or problems due to attempting to export invalid refs that are\nnot stashes.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n---\n Documentation/git-stash.adoc |  22 ++-\n builtin/stash.c              | 250 +++++++++++++++++++++++++++++++++++\n 2 files changed, 271 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 1a5177f498..e8efd43ba4 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -23,6 +23,7 @@ SYNOPSIS\n 'git stash' clear\n 'git stash' create [<message>]\n 'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>\n+'git stash' export (--print | --to-ref <ref>) [<stash>...]\n \n DESCRIPTION\n -----------\n@@ -154,6 +155,12 @@ store::\n \treflog.  This is intended to be useful for scripts.  It is\n \tprobably not the command you want to use; see \"push\" above.\n \n+export ( --print | --to-ref <ref> ) [<stash>...]::\n+\n+\tExport the specified stashes, or all of them if none are specified, to\n+\ta chain of commits which can be transferred using the normal fetch and\n+\tpush mechanisms, then imported using the `import` subcommand.\n+\n OPTIONS\n -------\n -a::\n@@ -242,6 +249,19 @@ literally (including newlines and quotes).\n +\n Quiet, suppress feedback messages.\n \n+--print::\n+\tThis option is only valid for `export`.\n++\n+Create the chain of commits representing the exported stashes without\n+storing it anywhere in the ref namespace and print the object ID to\n+standard output.  This is designed for scripts.\n+\n+--to-ref::\n+\tThis option is only valid for `export`.\n++\n+Create the chain of commits representing the exported stashes and store\n+it to the specified ref.\n+\n \\--::\n \tThis option is only valid for `push` command.\n +\n@@ -259,7 +279,7 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].\n \n <stash>::\n \tThis option is only valid for `apply`, `branch`, `drop`, `pop`,\n-\t`show` commands.\n+\t`show`, and `export` commands.\n +\n A reference of the form `stash@{<revision>}`. When no `<stash>` is\n given, the latest stash is assumed (that is, `stash@{0}`).\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex ab491d5ff6..ca00663e36 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -28,7 +28,10 @@\n #include \"log-tree.h\"\n #include \"diffcore.h\"\n #include \"reflog.h\"\n+#include \"reflog-walk.h\"\n #include \"add-interactive.h\"\n+#include \"oid-array.h\"\n+#include \"commit.h\"\n \n #define INCLUDE_ALL_FILES 2\n \n@@ -56,6 +59,8 @@\n \t   \"          [-u | --include-untracked] [-a | --all] [<message>]\")\n #define BUILTIN_STASH_CREATE_USAGE \\\n \tN_(\"git stash create [<message>]\")\n+#define BUILTIN_STASH_EXPORT_USAGE \\\n+\tN_(\"git stash export (--print | --to-ref <ref>) [<stash>...]\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -71,6 +76,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_CLEAR_USAGE,\n \tBUILTIN_STASH_CREATE_USAGE,\n \tBUILTIN_STASH_STORE_USAGE,\n+\tBUILTIN_STASH_EXPORT_USAGE,\n \tNULL\n };\n \n@@ -124,6 +130,12 @@ static const char * const git_stash_save_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_export_usage[] = {\n+\tBUILTIN_STASH_EXPORT_USAGE,\n+\tNULL\n+};\n+\n+\n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n \n@@ -160,6 +172,33 @@ static void free_stash_info(struct stash_info *info)\n \tstrbuf_release(&info->revision);\n }\n \n+static int check_stash_topology(struct repository *r, struct commit *stash)\n+{\n+\tstruct commit *p1, *p2, *p3 = NULL;\n+\n+\t/* stash must have two or three parents */\n+\tif (!stash->parents || !stash->parents->next ||\n+\t\t\t(stash->parents->next->next && stash->parents->next->next->next))\n+\t\treturn -1;\n+\tp1 = stash->parents->item;\n+\tp2 = stash->parents->next->item;\n+\tif (stash->parents->next->next)\n+\t\tp3 = stash->parents->next->next->item;\n+\tif (repo_parse_commit(r, p1) || repo_parse_commit(r, p2) ||\n+\t\t\t(p3 && repo_parse_commit(r, p3)))\n+\t\treturn -1;\n+\t/* p2 must have a single parent, p3 must have no parents */\n+\tif (!p2->parents || p2->parents->next || (p3 && p3->parents))\n+\t\treturn -1;\n+\tif (repo_parse_commit(r, p2->parents->item))\n+\t\treturn -1;\n+\t/* p2^1 must equal p1 */\n+\tif (!oideq(&p1->object.oid, &p2->parents->item->object.oid))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n static void assert_stash_like(struct stash_info *info, const char *revision)\n {\n \tif (get_oidf(&info->b_commit, \"%s^1\", revision) ||\n@@ -1895,6 +1934,216 @@ static int save_stash(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int write_commit_with_parents(struct repository *r,\n+\t\t\t\t     struct object_id *out,\n+\t\t\t\t     const struct object_id *oid,\n+\t\t\t\t     struct commit_list *parents)\n+{\n+\tsize_t author_len, committer_len;\n+\tstruct commit *this;\n+\tconst char *orig_author, *orig_committer;\n+\tchar *author = NULL, *committer = NULL;\n+\tconst char *buffer;\n+\tunsigned long bufsize;\n+\tconst char *p;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tint ret = 0;\n+\tstruct ident_split id;\n+\n+\tthis = lookup_commit_reference(r, oid);\n+\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\torig_author = find_commit_header(buffer, \"author\", &author_len);\n+\torig_committer = find_commit_header(buffer, \"committer\", &committer_len);\n+\n+\tif (!orig_author || !orig_committer) {\n+\t\tret = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\n+\tif (split_ident_line(&id, orig_author, author_len) < 0 ||\n+\t    split_ident_line(&id, orig_committer, committer_len) < 0) {\n+\t\tret = error(_(\"invalid author or committer for %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\n+\tp = strstr(buffer, \"\\n\\n\");\n+\tstrbuf_addstr(&msg, \"git stash: \");\n+\n+\tif (p)\n+\t\tstrbuf_add(&msg, p + 2, bufsize - (p + 2 - buffer));\n+\tstrbuf_complete_line(&msg);\n+\n+\tauthor = xmemdupz(orig_author, author_len);\n+\tcommitter = xmemdupz(orig_committer, committer_len);\n+\n+\tif (commit_tree_extended(msg.buf, msg.len,\n+\t\t\t\t r->hash_algo->empty_tree, parents,\n+\t\t\t\t out, author, committer,\n+\t\t\t\t NULL, NULL)) {\n+\t\tret = error(_(\"could not write commit\"));\n+\t\tgoto out;\n+\t}\n+out:\n+\tstrbuf_release(&msg);\n+\trepo_unuse_commit_buffer(r, this, buffer);\n+\tfree_commit_list(parents);\n+\tfree(author);\n+\tfree(committer);\n+\treturn ret;\n+}\n+\n+static int do_export_stash(struct repository *r,\n+\t\t\t   const char *ref,\n+\t\t\t   int argc,\n+\t\t\t   const char **argv)\n+{\n+\tstruct object_id base;\n+\tstruct object_context unused;\n+\tstruct commit *prev;\n+\tstruct oid_array items = OID_ARRAY_INIT;\n+\tint res = 0;\n+\tssize_t i;\n+\tstruct strbuf revision = STRBUF_INIT;\n+\tconst char *author, *committer;\n+\n+\t/*\n+\t * This is an arbitrary, fixed date, specifically the one used by git\n+\t * format-patch.  The goal is merely to produce reproducible output.\n+\t */\n+\tprepare_fallback_ident(\"git stash\", \"git@stash\");\n+\tauthor = fmt_ident(\"git stash\", \"git@stash\", WANT_BLANK_IDENT,\n+\t\t\t   \"2001-09-17T00:00:00Z\", 0);\n+\tcommitter = fmt_ident(\"git stash\", \"git@stash\", WANT_BLANK_IDENT,\n+\t\t\t      \"2001-09-17T00:00:00Z\", 0);\n+\n+\t/* First, we create a single empty commit. */\n+\tif (commit_tree_extended(\"\", 0, r->hash_algo->empty_tree, NULL,\n+\t\t\t\t &base, author, committer, NULL, NULL))\n+\t\treturn error(_(\"unable to write base commit\"));\n+\n+\tprev = lookup_commit_reference(r, &base);\n+\n+\tif (argc) {\n+\t\t/*\n+\t\t * Find each specified stash, and load data into the array.\n+\t\t */\n+\t\tfor (i = 0; i < argc; i++) {\n+\t\t\tstruct object_id oid;\n+\t\t\tstruct commit *stash;\n+\n+\t\t\tif (parse_stash_revision(&revision, argv[i], 1) ||\n+\t\t\t    get_oid_with_context(r, revision.buf,\n+\t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n+\t\t\t\t\t\t &oid, &unused)) {\n+\t\t\t\tres = error(_(\"unable to find stash entry %s\"), argv[i]);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\n+\t\t\tstash = lookup_commit_reference(r, &oid);\n+\t\t\tif (!stash || check_stash_topology(r, stash)) {\n+\t\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t\t\trevision.buf);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t\toid_array_append(&items, &oid);\n+\t\t}\n+\t} else {\n+\t\t/*\n+\t\t * Walk the reflog, finding each stash entry, and load data into the\n+\t\t * array.\n+\t\t */\n+\t\tstruct complete_reflogs *crl = read_complete_reflog(\"refs/stash\");\n+\t\tfor (i = 0; i < crl->nr; i++) {\n+\t\t\t/*\n+\t\t\t * Iterate from the latest item in the reflog to the\n+\t\t\t * earliest.  crl->items starts from the earliest first.\n+\t\t\t */\n+\t\t\tstruct reflog_info *item = crl->items + (crl->nr - i - 1);\n+\t\t\tstruct commit *stash;\n+\n+\t\t\tstash = lookup_commit_reference(r, &item->noid);\n+\t\t\tif (!stash || check_stash_topology(r, stash)) {\n+\t\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t\t\trevision.buf);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t\toid_array_append(&items, &item->noid);\n+\t\t}\n+\t\tfree_complete_reflog(crl);\n+\t}\n+\n+\t/*\n+\t * Now, create a set of commits identical to the regular stash commits,\n+\t * but where their first parents form a chain to our original empty\n+\t * base commit.\n+\t */\n+\tfor (i = (ssize_t)items.nr - 1; i >= 0; i--) {\n+\t\tstruct commit_list *parents = NULL;\n+\t\tstruct commit_list **next = &parents;\n+\t\tstruct object_id out;\n+\t\tconst struct object_id *oid = items.oid + i;\n+\t\tstruct commit *stash;\n+\n+\t\tnext = commit_list_append(prev, next);\n+\t\tstash = lookup_commit_reference(r, oid);\n+\t\tif (!stash) {\n+\t\t\tres = error(_(\"%s is not a valid commit\"),\n+\t\t\t\t\trevision.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tnext = commit_list_append(stash, next);\n+\t\tres = write_commit_with_parents(r, &out, oid, parents);\n+\t\tif (res)\n+\t\t\tgoto out;\n+\t\tprev = lookup_commit_reference(r, &out);\n+\t}\n+\tif (ref)\n+\t\trefs_update_ref(get_main_ref_store(r), NULL, ref,\n+\t\t\t\t&prev->object.oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR);\n+\telse\n+\t\tputs(oid_to_hex(&prev->object.oid));\n+out:\n+\tstrbuf_release(&revision);\n+\toid_array_clear(&items);\n+\n+\treturn res;\n+}\n+\n+enum export_action {\n+\tACTION_NONE,\n+\tACTION_PRINT,\n+\tACTION_TO_REF,\n+};\n+\n+static int export_stash(int argc,\n+\t\t\tconst char **argv,\n+\t\t\tconst char *prefix,\n+\t\t\tstruct repository *repo)\n+{\n+\tconst char *ref = NULL;\n+\tenum export_action action = ACTION_NONE;\n+\tstruct option options[] = {\n+\t\tOPT_CMDMODE(0, \"print\", &action,\n+\t\t\t    N_(\"print the object ID instead of writing it to a ref\"),\n+\t\t\t    ACTION_PRINT),\n+\t\tOPT_STRING(0, \"to-ref\", &ref, \"ref\",\n+\t\t\t    N_(\"save the data to the given ref\")),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_export_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (ref && action == ACTION_NONE)\n+\t\taction = ACTION_TO_REF;\n+\n+\tif (action == ACTION_NONE)\n+\t\treturn error(_(\"exactly one of --print and --to-ref is required\"));\n+\n+\treturn do_export_stash(repo, ref, argc, argv);\n+}\n+\n int cmd_stash(int argc,\n \t      const char **argv,\n \t      const char *prefix,\n@@ -1915,6 +2164,7 @@ int cmd_stash(int argc,\n \t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n \t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n+\t\tOPT_SUBCOMMAND(\"export\", &fn, export_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\n"},{"id":"518700","messageId":"aC90SzSUkhVvahvU@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"20250522185524.18398-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH] Makefile: avoid constant rebuilds with compilation database","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-22T19:00:27Z","receivedAt":"2025-05-22T19:00:43Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-22 at 18:55:18, brian m. carlson wrote:\n> Many contributors to software use a Language Server Protocol\n> implementation to allow their editor to learn structural information\n> about the code they write and provide additional features, such as\n> jumping to the declaration or definition of a function or type.  In C,\n> the usual implementation is clangd, which requires compiling with clang.\n\nOops, my apologies.  My patch directory was unclean.  Junio already\npicked this up, I believe.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"518703","messageId":"xmqqh61c5uqt.fsf@gitster.g","threadId":"63424","inReplyTo":"aC90SzSUkhVvahvU@tapette.crustytoothpaste.net","subject":"Re: [PATCH] Makefile: avoid constant rebuilds with compilation database","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-22T19:08:10Z","receivedAt":"2025-05-22T19:08:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> On 2025-05-22 at 18:55:18, brian m. carlson wrote:\n>> Many contributors to software use a Language Server Protocol\n>> implementation to allow their editor to learn structural information\n>> about the code they write and provide additional features, such as\n>> jumping to the declaration or definition of a function or type.  In C,\n>> the usual implementation is clangd, which requires compiling with clang.\n>\n> Oops, my apologies.  My patch directory was unclean.  Junio already\n> picked this up, I believe.\n\nDon't fret--mistakes happen.  880146ae (Makefile: avoid constant\nrebuilds with compilation database, 2025-05-09) is already in\n'master' as of a few days ago.\n\n\n"},{"id":"518705","messageId":"xmqqa5745ttw.fsf@gitster.g","threadId":"63424","inReplyTo":"20250522185524.18398-3-sandals@crustytoothpaste.net","subject":"Re: [PATCH v6 1/5] object-name: make get_oid quietly return an error","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-22T19:27:55Z","receivedAt":"2025-05-22T19:27:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> @@ -1081,13 +1081,17 @@ static int get_oid_basic(struct repository *r, const char *str, int len,\n>  \t\t\t\t * still fill in the oid with the \"old\" value,\n>  \t\t\t\t * which we can use.\n>  \t\t\t\t */\n\nSo far in this if/elseif cascade, we covered the case where we found\na reflog entry we are looking for before running out.  So ...\n\n> -\t\t\t} else {\n> +\t\t\t} else if (!(flags & GET_OID_GENTLY)) {\n>  \t\t\t\tif (flags & GET_OID_QUIETLY) {\n>  \t\t\t\t\texit(128);\n>  \t\t\t\t}\n>  \t\t\t\tdie(_(\"log for '%.*s' only has %d entries\"),\n>  \t\t\t\t    len, str, co_cnt);\n>  \t\t\t}\n\n... existing code chose between a silent exit or die based on\nGET_OID_QUIETLY bit in the flags word.  In the updated code, this\nblock is entered only when the caller did not ask for\nGET_OID_GENTLY.  But the point is that if we do not say GENTLY,\nwe no longer do this \"choose between exit or die, either way we are\ndead at this point\".\n\nOK.\n\n> +\t\t\tif (flags & GET_OID_GENTLY) {\n> +\t\t\t\tfree(real_ref);\n> +\t\t\t\treturn -1;\n> +\t\t\t}\n\nI am confused.\n\nImagine that one of the if/elseif cascade handled the situation.\ne.g. \"The caller asked Nth, we found exactly N entries, so instead\nof usual new side of the N-1th, we can give the old side of the Nth\"\ncase is ready to return a success.  Why should the caller in such a\ncase instead get a failure only because the caller said \"do not die\non me; I will handle failures myself\"?\n\nShouldn't it be made the final \"} else {\" of the if/elseif cascade\ninstead?\n"},{"id":"518715","messageId":"xmqq1psg5qrb.fsf@gitster.g","threadId":"63424","inReplyTo":"20250522185524.18398-5-sandals@crustytoothpaste.net","subject":"Re: [PATCH v6 3/5] builtin/stash: factor out revision parsing into a function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-22T20:34:16Z","receivedAt":"2025-05-22T20:34:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> +\tstrbuf_init(&info->revision, 0);\n> +\tif (parse_stash_revision(&info->revision, commit, 0)) {\n> +\t\treturn -1;\n>  \t}\n\nIt does not look like this series add more code inside this block in\na later step, so let's lose the unnecessary {braces} around a single\nstatement \"return -1\" here.\n\n\nBy the way, what is \"pwodd\" I saw in the e-mail header?\n"},{"id":"518716","messageId":"xmqqv7ps4bef.fsf@gitster.g","threadId":"63424","inReplyTo":"20250522185524.18398-6-sandals@crustytoothpaste.net","subject":"Re: [PATCH v6 4/5] builtin/stash: provide a way to export stashes to a ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-22T20:51:20Z","receivedAt":"2025-05-22T20:51:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> +static int check_stash_topology(struct repository *r, struct commit *stash)\n> +{\n> +\tstruct commit *p1, *p2, *p3 = NULL;\n> +\n> +\t/* stash must have two or three parents */\n> +\tif (!stash->parents || !stash->parents->next ||\n> +\t\t\t(stash->parents->next->next && stash->parents->next->next->next))\n> +\t\treturn -1;\n> +\tp1 = stash->parents->item;\n> +\tp2 = stash->parents->next->item;\n> +\tif (stash->parents->next->next)\n> +\t\tp3 = stash->parents->next->next->item;\n> +\tif (repo_parse_commit(r, p1) || repo_parse_commit(r, p2) ||\n> +\t\t\t(p3 && repo_parse_commit(r, p3)))\n> +\t\treturn -1;\n> +\t/* p2 must have a single parent, p3 must have no parents */\n> +\tif (!p2->parents || p2->parents->next || (p3 && p3->parents))\n> +\t\treturn -1;\n> +\tif (repo_parse_commit(r, p2->parents->item))\n> +\t\treturn -1;\n> +\t/* p2^1 must equal p1 */\n> +\tif (!oideq(&p1->object.oid, &p2->parents->item->object.oid))\n> +\t\treturn -1;\n> +\n> +\treturn 0;\n> +}\n\nThis is much more elaborate than the existing \"does this commit look\nlike a stash entry\" test done elsewhere, isn't it?  Very nicely done.\n\n> +\tthis = lookup_commit_reference(r, oid);\n> +\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n> +\torig_author = find_commit_header(buffer, \"author\", &author_len);\n> +\torig_committer = find_commit_header(buffer, \"committer\", &committer_len);\n> +\n> +\tif (!orig_author || !orig_committer) {\n> +\t\tret = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tif (split_ident_line(&id, orig_author, author_len) < 0 ||\n> +\t    split_ident_line(&id, orig_committer, committer_len) < 0) {\n> +\t\tret = error(_(\"invalid author or committer for %s\"), oid_to_hex(oid));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tp = strstr(buffer, \"\\n\\n\");\n> +\tstrbuf_addstr(&msg, \"git stash: \");\n> +\n> +\tif (p)\n> +\t\tstrbuf_add(&msg, p + 2, bufsize - (p + 2 - buffer));\n> +\tstrbuf_complete_line(&msg);\n> +\n> +\tauthor = xmemdupz(orig_author, author_len);\n> +\tcommitter = xmemdupz(orig_committer, committer_len);\n> +\n> +\tif (commit_tree_extended(msg.buf, msg.len,\n> +\t\t\t\t r->hash_algo->empty_tree, parents,\n> +\t\t\t\t out, author, committer,\n> +\t\t\t\t NULL, NULL)) {\n> +\t\tret = error(_(\"could not write commit\"));\n> +\t\tgoto out;\n> +\t}\n> +out:\n> +\tstrbuf_release(&msg);\n> +\trepo_unuse_commit_buffer(r, this, buffer);\n> +\tfree_commit_list(parents);\n> +\tfree(author);\n> +\tfree(committer);\n> +\treturn ret;\n> +}\n\nNice.\n\n> +\tif (argc) {\n> +\t\t/*\n> +\t\t * Find each specified stash, and load data into the array.\n> +\t\t */\n> +\t\tfor (i = 0; i < argc; i++) {\n> +\t\t\tstruct object_id oid;\n> +\t\t\tstruct commit *stash;\n> +\n> +\t\t\tif (parse_stash_revision(&revision, argv[i], 1) ||\n> +\t\t\t    get_oid_with_context(r, revision.buf,\n> +\t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n> +\t\t\t\t\t\t &oid, &unused)) {\n> +\t\t\t\tres = error(_(\"unable to find stash entry %s\"), argv[i]);\n> +\t\t\t\tgoto out;\n> +\t\t\t}\n> +\n> +\t\t\tstash = lookup_commit_reference(r, &oid);\n> +\t\t\tif (!stash || check_stash_topology(r, stash)) {\n> +\t\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n> +\t\t\t\t\t\trevision.buf);\n> +\t\t\t\tgoto out;\n> +\t\t\t}\n> +\t\t\toid_array_append(&items, &oid);\n> +\t\t}\n> +\t} else {\n> +\t\t/*\n> +\t\t * Walk the reflog, finding each stash entry, and load data into the\n> +\t\t * array.\n> +\t\t */\n> +\t\tstruct complete_reflogs *crl = read_complete_reflog(\"refs/stash\");\n> +\t\tfor (i = 0; i < crl->nr; i++) {\n> +\t\t\t/*\n> +\t\t\t * Iterate from the latest item in the reflog to the\n> +\t\t\t * earliest.  crl->items starts from the earliest first.\n> +\t\t\t */\n> +\t\t\tstruct reflog_info *item = crl->items + (crl->nr - i - 1);\n> +\t\t\tstruct commit *stash;\n> +\n> +\t\t\tstash = lookup_commit_reference(r, &item->noid);\n> +\t\t\tif (!stash || check_stash_topology(r, stash)) {\n> +\t\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n> +\t\t\t\t\t\trevision.buf);\n> +\t\t\t\tgoto out;\n> +\t\t\t}\n> +\t\t\toid_array_append(&items, &item->noid);\n> +\t\t}\n> +\t\tfree_complete_reflog(crl);\n> +\t}\n\nHmph.  As we work with the commit objects in the above part already,\nwould it still make sense to use an oid array?  That would force us\nto look up the same set of commits using the object names again in\nthe later loop.  I would have thought we might use commit_list or\nsomething to accumulate commits, and then consume in the loop below.\n\nNot a huge deal, though.  But I somehow find it more natural.  \n\nIn addition, we would not have to worry about the ssize_t cast and\nother complications if we used commit_list to hold these incoming\nstash entries.\n\n"},{"id":"518717","messageId":"xmqqcyc04akc.fsf@gitster.g","threadId":"63424","inReplyTo":"20250522185524.18398-7-sandals@crustytoothpaste.net","subject":"Re: [PATCH v6 5/5] builtin/stash: provide a way to import stashes from a ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-22T21:09:23Z","receivedAt":"2025-05-22T21:09:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> We don't clear the stash first and instead add the specified stashes to\n> the top of the stash.  This is because users may want to export just a\n> few stashes, such as to share a small amount of work in progress with a\n> colleague, and it would be undesirable for the receiving user to lose\n> all of their data.  For users who do want to replace the stash, it's\n> easy to do to: simply run \"git stash clear\" first.\n\nVery sensible.\n\n> +static int do_import_stash(struct repository *r, const char *rev)\n> +{\n> +\tstruct object_id chain;\n> +\tstruct oid_array items = OID_ARRAY_INIT;\n> +\tint res = 0;\n> +\tssize_t i;\n> +\tconst char *buffer = NULL;\n> +\tunsigned long bufsize;\n> +\tstruct commit *this = NULL;\n> +\tchar *msg = NULL;\n> +\n> +\tif (repo_get_oid(r, rev, &chain))\n> +\t\treturn error(_(\"not a valid revision: %s\"), rev);\n> +\n> +\tthis = lookup_commit_reference(r, &chain);\n> +\tif (!this)\n> +\t\treturn error(_(\"not a commit: %s\"), rev);\n> +\n> +\t/*\n> +\t * Walk the commit history, finding each stash entry, and load data into\n> +\t * the array.\n> +\t */\n> +\tfor (;;) {\n> +\t\tconst char *author, *committer;\n> +\t\tsize_t author_len, committer_len;\n> +\t\tconst char *p;\n> +\t\tconst char *expected = \"git stash <git@stash> 1000684800 +0000\";\n\nMakes me wonder if we can somehow share this with [4/5] where we\nestablish the author and committer ident (timestamp spelled in a\ndifferent form).  Three strings that has to stay in sync is not a\nhuge deal, though.\n\n> +\t\tconst char *prefix = \"git stash: \";\n> +\t\tstruct commit *stash;\n> +\t\tstruct tree *tree = repo_get_commit_tree(r, this);\n> +\n> +\t\tif (!tree ||\n> +\t\t    !oideq(&tree->object.oid, r->hash_algo->empty_tree) ||\n> +\t\t    (this->parents &&\n> +\t\t     (!this->parents->next || this->parents->next->next))) {\n> +\t\t\tres = error(_(\"%s is not a valid exported stash commit\"),\n> +\t\t\t\t\toid_to_hex(&this->object.oid));\n> +\t\t\tgoto out;\n> +\t\t}\n\nOK.  \"buffer\" is initialized to NULL before entering this loop, and\ncleared to NULL at the end of this loop when \"this\" is moved, so\njumping to \"out:\" would not confuse the call to unuse_buffer().\n\n> +\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n\nAnd \"buffer\" becomes non-NULL and holds data associated with \"this\";\nany \"goto out\" in the error path would do the right thing.  Very\nnicely designed loop.\n\n> +\t\tif (!this->parents) {\n> +\t\t\t/*\n> +\t\t\t * We don't have any parents.  Make sure this is our\n> +\t\t\t * root commit.\n> +\t\t\t */\n> +\t\t\tauthor = find_commit_header(buffer, \"author\", &author_len);\n> +\t\t\tcommitter = find_commit_header(buffer, \"committer\", &committer_len);\n> +\n> +\t\t\tif (!author || !committer) {\n> +\t\t\t\terror(_(\"cannot parse commit %s\"), oid_to_hex(&this->object.oid));\n> +\t\t\t\tgoto out;\n> +\t\t\t}\n> +\n> +\t\t\tif (author_len != strlen(expected) ||\n> +\t\t\t    committer_len != strlen(expected) ||\n> +\t\t\t    memcmp(author, expected, author_len) ||\n> +\t\t\t    memcmp(committer, expected, committer_len)) {\n> +\t\t\t\tres = error(_(\"found root commit %s with invalid data\"), oid_to_hex(&this->object.oid));\n> +\t\t\t\tgoto out;\n> +\t\t\t}\n> +\t\t\tbreak;\n> +\t\t}\n\nOK.\n\n> +\t\tp = strstr(buffer, \"\\n\\n\");\n> +\t\tif (!p) {\n> +\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(&this->object.oid));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tp += 2;\n> +\t\tif (((size_t)(bufsize - (p - buffer)) < strlen(prefix)) ||\n> +\t\t    memcmp(prefix, p, strlen(prefix))) {\n> +\t\t\tres = error(_(\"found stash commit %s with unexpected prefix\"), oid_to_hex(&this->object.oid));\n> +\t\t\tgoto out;\n> +\t\t}\n\nI'd call that \"without expected prefix\" (the difference being \"we\nexpected 'git stash:' prefix but you have 'git stosh:' prefix\" vs\n\"your commit title is 'hello world'\"), but OK.  The important thing\nis that we are being careful not to get confused.\n\nIt is unfortunate historical practice to measure almost everything\nin \"unsigned long\" and bufsize here is one of them, but do we need\nthat cast?  We know strlen(prefix) is a small known constant\ninteger, we know p is not smaller than buffer, we know (p-buffer) is\nnot larger than bufsize.\n\n> +\t\tstash = this->parents->next->item;\n> +\n> +\t\tif (repo_parse_commit(r, this->parents->item) ||\n> +\t\t    repo_parse_commit(r, stash)) {\n> +\t\t\tres = error(_(\"cannot parse parents of commit: %s\"),\n> +\t\t\t\t\toid_to_hex(&this->object.oid));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tif (check_stash_topology(r, stash)) {\n> +\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n> +\t\t\t\t\toid_to_hex(&stash->object.oid));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\trepo_unuse_commit_buffer(r, this, buffer);\n> +\t\tbuffer = NULL;\n> +\t\toid_array_append(&items, &stash->object.oid);\n> +\t\tthis = this->parents->item;\n> +\t}\n> +\n> +\t/*\n> +\t * Now, walk each entry, adding it to the stash as a normal stash\n> +\t * commit.\n> +\t */\n> +\tfor (i = (ssize_t)items.nr - 1; i >= 0; i--) {\n> +\t\tconst char *p;\n> +\t\tconst struct object_id *oid = items.oid + i;\n> +\n> +\t\tthis = lookup_commit_reference(r, oid);\n> +\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n> +\t\tif (!buffer) {\n> +\t\t\tres = error(_(\"cannot read commit buffer for %s\"), oid_to_hex(oid));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tp = strstr(buffer, \"\\n\\n\");\n> +\t\tif (!p) {\n> +\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tp += 2;\n> +\t\tmsg = xmemdupz(p, bufsize - (p - buffer));\n> +\t\trepo_unuse_commit_buffer(r, this, buffer);\n> +\t\tbuffer = NULL;\n> +\n> +\t\tif (do_store_stash(oid, msg, 1)) {\n> +\t\t\tres = error(_(\"cannot save the stash for %s\"), oid_to_hex(oid));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tFREE_AND_NULL(msg);\n> +\t}\n> +out:\n> +\tif (this && buffer)\n> +\t\trepo_unuse_commit_buffer(r, this, buffer);\n> +\toid_array_clear(&items);\n> +\tfree(msg);\n> +\n> +\treturn res;\n> +}\n\nOK.  Again, I suspect that commit_list may be a more natural thing\nto use to accumulate the stash entries, but that is not a huge deal.\n"},{"id":"518718","messageId":"xmqq8qmo4aaf.fsf@gitster.g","threadId":"63424","inReplyTo":"20250522185524.18398-7-sandals@crustytoothpaste.net","subject":"Re: [PATCH v6 5/5] builtin/stash: provide a way to import stashes from a ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-22T21:15:20Z","receivedAt":"2025-05-22T21:15:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> Now that we have a way to export stashes to a ref, let's provide a way\n> to import them from such a ref back to the stash.  This works much the\n> way the export code does, except that we strip off the first parent\n> chain commit and then store each resulting commit back to the stash.\n>\n> We don't clear the stash first and instead add the specified stashes to\n> the top of the stash.  This is because users may want to export just a\n> few stashes, such as to share a small amount of work in progress with a\n> colleague, and it would be undesirable for the receiving user to lose\n> all of their data.  For users who do want to replace the stash, it's\n> easy to do to: simply run \"git stash clear\" first.\n>\n> We specifically rely on the fact that we'll produce identical stash\n> commits on both sides in our tests.  This provides a cheap,\n> straightforward check for our tests and also makes it easy for users to\n> see if they already have the same data in both repositories.\n>\n> Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n> ---\n> I'll need Phillip's sign-off for this patch.\n\nLet's Cc'em, then.\n\nAhh, it occurs to me that your pwodd may be a typo of that address?\n"},{"id":"518722","messageId":"2e8e7eb0-f526-49cb-8f75-379fa7ef6446@ramsayjones.plus.com","threadId":"63424","inReplyTo":"20250522185524.18398-4-sandals@crustytoothpaste.net","subject":"Re: [PATCH v6 2/5] reflog-walk: expose read_complete_reflog","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsayjones.plus.com","sentAt":"2025-05-22T21:53:27Z","receivedAt":"2025-05-22T21:56:37Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"\n\nOn 22/05/2025 19:55, brian m. carlson wrote:\n> In a future commit, we'll use this function and the corresponding free\n> function to read the entire reflog.  Expose it in the header so we can\n> do so.\n> \n> Include the appropriate header files so that our header is complete.\n> \n> Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n> ---\n>  reflog-walk.c | 17 ++---------------\n>  reflog-walk.h | 18 ++++++++++++++++++\n>  2 files changed, 20 insertions(+), 15 deletions(-)\n> \n> diff --git a/reflog-walk.c b/reflog-walk.c\n> index c7070b13b0..b7a9d70966 100644\n> --- a/reflog-walk.c\n> +++ b/reflog-walk.c\n> @@ -9,19 +9,6 @@\n>  #include \"string-list.h\"\n>  #include \"reflog-walk.h\"\n>  \n> -struct complete_reflogs {\n> -\tchar *ref;\n> -\tchar *short_ref;\n> -\tstruct reflog_info {\n> -\t\tstruct object_id ooid, noid;\n> -\t\tchar *email;\n> -\t\ttimestamp_t timestamp;\n> -\t\tint tz;\n> -\t\tchar *message;\n> -\t} *items;\n> -\tint nr, alloc;\n> -};\n> -\n>  static int read_one_reflog(struct object_id *ooid, struct object_id *noid,\n>  \t\tconst char *email, timestamp_t timestamp, int tz,\n>  \t\tconst char *message, void *cb_data)\n> @@ -41,7 +28,7 @@ static int read_one_reflog(struct object_id *ooid, struct object_id *noid,\n>  \treturn 0;\n>  }\n>  \n> -static void free_complete_reflog(struct complete_reflogs *array)\n> +void free_complete_reflog(struct complete_reflogs *array)\n>  {\n>  \tint i;\n>  \n> @@ -64,7 +51,7 @@ static void complete_reflogs_clear(void *util, const char *str UNUSED)\n>  \tfree_complete_reflog(array);\n>  }\n>  \n> -static struct complete_reflogs *read_complete_reflog(const char *ref)\n> +struct complete_reflogs *read_complete_reflog(const char *ref)\n>  {\n>  \tstruct complete_reflogs *reflogs =\n>  \t\txcalloc(1, sizeof(struct complete_reflogs));\n> diff --git a/reflog-walk.h b/reflog-walk.h\n> index 989583dc55..8f0640f662 100644\n> --- a/reflog-walk.h\n> +++ b/reflog-walk.h\n> @@ -1,9 +1,24 @@\n>  #ifndef REFLOG_WALK_H\n>  #define REFLOG_WALK_H\n>  \n> +#include \"git-compat-util.h\"\n\nWhy? 'git-compat-util.h' must be #include-d in any compilation\nunit before 'reflog-walk.h', so ...\n\n(Sorry, I have not studied these patches, they were just floating\nin front of my eyes ... so, please ignore me if the reason is\nobvious! :) ).\n\nATB,\nRamsay Jones\n\n> +#include \"hash.h\"\n> +\n>  struct commit;\n>  struct reflog_walk_info;\n>  struct date_mode;\n> +struct complete_reflogs {\n> +\tchar *ref;\n> +\tchar *short_ref;\n> +\tstruct reflog_info {\n> +\t\tstruct object_id ooid, noid;\n> +\t\tchar *email;\n> +\t\ttimestamp_t timestamp;\n> +\t\tint tz;\n> +\t\tchar *message;\n> +\t} *items;\n> +\tint nr, alloc;\n> +};\n>  \n>  void init_reflog_walk(struct reflog_walk_info **info);\n>  void reflog_walk_info_release(struct reflog_walk_info *info);\n> @@ -24,4 +39,7 @@ int reflog_walk_empty(struct reflog_walk_info *walk);\n>  \n>  struct commit *next_reflog_entry(struct reflog_walk_info *reflog_info);\n>  \n> +void free_complete_reflog(struct complete_reflogs *array);\n> +struct complete_reflogs *read_complete_reflog(const char *ref);\n> +\n>  #endif\n> \n\n"},{"id":"518767","messageId":"85847c7f-334a-4d0a-ac54-6a3c9b26c282@gmail.com","threadId":"63424","inReplyTo":"xmqq8qmo4aaf.fsf@gitster.g","subject":"Re: [PATCH v6 5/5] builtin/stash: provide a way to import stashes from a ref","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-05-23T13:17:42Z","receivedAt":"2025-05-23T13:17:53Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 22/05/2025 22:15, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> \n>> I'll need Phillip's sign-off for this patch.\n> \n> Let's Cc'em, then.\n\nThanks, I'm glad the fixups were useful, sorry I forgot to add my sign \noff to the last one - here it is\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n\nI'll take a proper look at this series next week\n\nThanks\n\nPhillip\n\n> Ahh, it occurs to me that your pwodd may be a typo of that address?\n\n"},{"id":"518812","messageId":"aDEDGfyvkTFyf0I8@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"2e8e7eb0-f526-49cb-8f75-379fa7ef6446@ramsayjones.plus.com","subject":"Re: [PATCH v6 2/5] reflog-walk: expose read_complete_reflog","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-23T23:22:01Z","receivedAt":"2025-05-23T23:22:10Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-22 at 21:53:27, Ramsay Jones wrote:\n> \n> \n> On 22/05/2025 19:55, brian m. carlson wrote:\n> > diff --git a/reflog-walk.h b/reflog-walk.h\n> > index 989583dc55..8f0640f662 100644\n> > --- a/reflog-walk.h\n> > +++ b/reflog-walk.h\n> > @@ -1,9 +1,24 @@\n> >  #ifndef REFLOG_WALK_H\n> >  #define REFLOG_WALK_H\n> >  \n> > +#include \"git-compat-util.h\"\n> \n> Why? 'git-compat-util.h' must be #include-d in any compilation\n> unit before 'reflog-walk.h', so ...\n\nOne of the CI jobs fails if we don't include `hash.h` for `struct\nobject_id`.  I don't remember which one.  The rule is that we always\ninclude that header before any other header, so that's what I did here.\nIt is also needed for `timestamp_t`.\n\nI will also mention that having complete headers makes clangd and other\nLSPs work better because then they don't warn about undefined types and\nthey can actually warn when we have failed to include the relevant type,\nand since we actually both headers here, I decided that was the right\nthing to do.\n\nIf we're dead set against it, I can remove it.\n\n> (Sorry, I have not studied these patches, they were just floating\n> in front of my eyes ... so, please ignore me if the reason is\n> obvious! :) ).\n> \n> ATB,\n> Ramsay Jones\n> \n> > +#include \"hash.h\"\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"518813","messageId":"aDED4MrweIc0N51h@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"xmqq1psg5qrb.fsf@gitster.g","subject":"Re: [PATCH v6 3/5] builtin/stash: factor out revision parsing into a function","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-23T23:25:20Z","receivedAt":"2025-05-23T23:25:23Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-22 at 20:34:16, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> \n> > +\tstrbuf_init(&info->revision, 0);\n> > +\tif (parse_stash_revision(&info->revision, commit, 0)) {\n> > +\t\treturn -1;\n> >  \t}\n> \n> It does not look like this series add more code inside this block in\n> a later step, so let's lose the unnecessary {braces} around a single\n> statement \"return -1\" here.\n\nSounds good.  I'll fix that in a v7.\n\n> By the way, what is \"pwodd\" I saw in the e-mail header?\n\nA typo for \"pwood\", which is my alias for Phillip Wood, who I intended\nto CC.  A typo which I unfortunately didn't catch before sending out\npatches (mostly because I was sitting in an airport lounge when I sent\nthem out instead of using my giant screens at home).\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"518817","messageId":"xmqqzff2uaad.fsf@gitster.g","threadId":"63424","inReplyTo":"aDED4MrweIc0N51h@tapette.crustytoothpaste.net","subject":"Re: [PATCH v6 3/5] builtin/stash: factor out revision parsing into a function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-24T00:23:06Z","receivedAt":"2025-05-24T00:23:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> A typo for \"pwood\", which is my alias for Phillip Wood, who I intended\n> to CC.  A typo which I unfortunately didn't catch before sending out\n> patches (mostly because I was sitting in an airport lounge when I sent\n> them out instead of using my giant screens at home).\n\nShould git-send-email be able to (optionally) catch a typo like\nthis?  E.g., we have an address without @ in it, and if we feed it\nto alias expansion, it comes back without a change.  It could be a\nlocal address but is more likely a typo if you are sending the\nmessage also to an external recipients with @ in their addresses.\n\n"},{"id":"518818","messageId":"b8a94020-0a7c-4d3b-92bc-a8a80b8d7af0@ramsayjones.plus.com","threadId":"63424","inReplyTo":"aDEDGfyvkTFyf0I8@tapette.crustytoothpaste.net","subject":"Re: [PATCH v6 2/5] reflog-walk: expose read_complete_reflog","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsayjones.plus.com","sentAt":"2025-05-24T01:09:49Z","receivedAt":"2025-05-24T01:09:58Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"\n\nOn 24/05/2025 00:22, brian m. carlson wrote:\n> On 2025-05-22 at 21:53:27, Ramsay Jones wrote:\n>>\n>>\n>> On 22/05/2025 19:55, brian m. carlson wrote:\n>>> diff --git a/reflog-walk.h b/reflog-walk.h\n>>> index 989583dc55..8f0640f662 100644\n>>> --- a/reflog-walk.h\n>>> +++ b/reflog-walk.h\n>>> @@ -1,9 +1,24 @@\n>>>  #ifndef REFLOG_WALK_H\n>>>  #define REFLOG_WALK_H\n>>>  \n>>> +#include \"git-compat-util.h\"\n>>\n>> Why? 'git-compat-util.h' must be #include-d in any compilation\n>> unit before 'reflog-walk.h', so ...\n> \n> One of the CI jobs fails if we don't include `hash.h` for `struct\n> object_id`.  I don't remember which one.  The rule is that we always\n> include that header before any other header, so that's what I did here.\n> It is also needed for `timestamp_t`.\n\nThe rule refers to '*.c' files not '*.h' files. ie. we #include\n'git-compat-util.h' at the beginning of all C files, so at the\nstart of all compilation units (well Documentation/CodingGuidelines\nhas some exceptions, but ...).\n\nSo, at this point, 'git-compat-util.h' will already have been seen\nby the compiler (so it won't cause any problem, but equally it is\nnot needed). In order to determine if the header can stand alone\nyou should just need to:\n\n    $ make reflog-walk.hco\n\n[That is what the hdr-check target is for].\n\nSo, yes, include 'hash.h' to make the header file stand alone,\nbut 'git-compat-util.h' should not be needed.\n\nThanks.\n\nATB,\nRamsay Jones\n\n\n"},{"id":"518950","messageId":"aDTC10EF5JWvZsf0@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"xmqqzff2uaad.fsf@gitster.g","subject":"Re: [PATCH v6 3/5] builtin/stash: factor out revision parsing into a function","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-26T19:36:55Z","receivedAt":"2025-05-26T19:37:07Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-24 at 00:23:06, Junio C Hamano wrote:\n> Should git-send-email be able to (optionally) catch a typo like\n> this?  E.g., we have an address without @ in it, and if we feed it\n> to alias expansion, it comes back without a change.  It could be a\n> local address but is more likely a typo if you are sending the\n> message also to an external recipients with @ in their addresses.\n\nThat seems like it might be a good idea.  We could override it with a\ncommand-line flag in the unlikely situation that the user really wanted\nthat.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"518951","messageId":"aDTEQDwLTOONeYp_@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"xmqqv7ps4bef.fsf@gitster.g","subject":"Re: [PATCH v6 4/5] builtin/stash: provide a way to export stashes to a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-26T19:42:56Z","receivedAt":"2025-05-26T19:43:01Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-22 at 20:51:20, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> \n> > +static int check_stash_topology(struct repository *r, struct commit *stash)\n> > +{\n> > +\tstruct commit *p1, *p2, *p3 = NULL;\n> > +\n> > +\t/* stash must have two or three parents */\n> > +\tif (!stash->parents || !stash->parents->next ||\n> > +\t\t\t(stash->parents->next->next && stash->parents->next->next->next))\n> > +\t\treturn -1;\n> > +\tp1 = stash->parents->item;\n> > +\tp2 = stash->parents->next->item;\n> > +\tif (stash->parents->next->next)\n> > +\t\tp3 = stash->parents->next->next->item;\n> > +\tif (repo_parse_commit(r, p1) || repo_parse_commit(r, p2) ||\n> > +\t\t\t(p3 && repo_parse_commit(r, p3)))\n> > +\t\treturn -1;\n> > +\t/* p2 must have a single parent, p3 must have no parents */\n> > +\tif (!p2->parents || p2->parents->next || (p3 && p3->parents))\n> > +\t\treturn -1;\n> > +\tif (repo_parse_commit(r, p2->parents->item))\n> > +\t\treturn -1;\n> > +\t/* p2^1 must equal p1 */\n> > +\tif (!oideq(&p1->object.oid, &p2->parents->item->object.oid))\n> > +\t\treturn -1;\n> > +\n> > +\treturn 0;\n> > +}\n> \n> This is much more elaborate than the existing \"does this commit look\n> like a stash entry\" test done elsewhere, isn't it?  Very nicely done.\n\nThis is Phillip Wood's code, but yes, it is very nice.  I looked at the\npatch he provided and it seemed very sensible, so I adopted it.\n\n> Hmph.  As we work with the commit objects in the above part already,\n> would it still make sense to use an oid array?  That would force us\n> to look up the same set of commits using the object names again in\n> the later loop.  I would have thought we might use commit_list or\n> something to accumulate commits, and then consume in the loop below.\n\nI can do that in v7.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"518953","messageId":"aDTHSOodIuhBgZ_x@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"b8a94020-0a7c-4d3b-92bc-a8a80b8d7af0@ramsayjones.plus.com","subject":"Re: [PATCH v6 2/5] reflog-walk: expose read_complete_reflog","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-26T19:55:52Z","receivedAt":"2025-05-26T19:55:55Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-24 at 01:09:49, Ramsay Jones wrote:\n> So, at this point, 'git-compat-util.h' will already have been seen\n> by the compiler (so it won't cause any problem, but equally it is\n> not needed). In order to determine if the header can stand alone\n> you should just need to:\n> \n>     $ make reflog-walk.hco\n> \n> [That is what the hdr-check target is for].\n> \n> So, yes, include 'hash.h' to make the header file stand alone,\n> but 'git-compat-util.h' should not be needed.\n\nOkay, I'll change that here, but it does make clangd much less useful\nbecause `hash.h` does actually depend on `git-compat-util.h` and so\nclangd flags lots of missing types.  Perhaps we should revisit that in\nthe future.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"518954","messageId":"aDTJHGmCnvdFkswt@tapette.crustytoothpaste.net","threadId":"63424","inReplyTo":"xmqqcyc04akc.fsf@gitster.g","subject":"Re: [PATCH v6 5/5] builtin/stash: provide a way to import stashes from a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-05-26T20:03:40Z","receivedAt":"2025-05-26T20:03:43Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-22 at 21:09:23, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> > +\t\tconst char *author, *committer;\n> > +\t\tsize_t author_len, committer_len;\n> > +\t\tconst char *p;\n> > +\t\tconst char *expected = \"git stash <git@stash> 1000684800 +0000\";\n> \n> Makes me wonder if we can somehow share this with [4/5] where we\n> establish the author and committer ident (timestamp spelled in a\n> different form).  Three strings that has to stay in sync is not a\n> huge deal, though.\n\nI'll take a look and see if I can come up with a nice way to solve this.\n\n> > +\t\tp = strstr(buffer, \"\\n\\n\");\n> > +\t\tif (!p) {\n> > +\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(&this->object.oid));\n> > +\t\t\tgoto out;\n> > +\t\t}\n> > +\n> > +\t\tp += 2;\n> > +\t\tif (((size_t)(bufsize - (p - buffer)) < strlen(prefix)) ||\n> > +\t\t    memcmp(prefix, p, strlen(prefix))) {\n> > +\t\t\tres = error(_(\"found stash commit %s with unexpected prefix\"), oid_to_hex(&this->object.oid));\n> > +\t\t\tgoto out;\n> > +\t\t}\n> \n> I'd call that \"without expected prefix\" (the difference being \"we\n> expected 'git stash:' prefix but you have 'git stosh:' prefix\" vs\n> \"your commit title is 'hello world'\"), but OK.  The important thing\n> is that we are being careful not to get confused.\n\nWill fix.\n\n> It is unfortunate historical practice to measure almost everything\n> in \"unsigned long\" and bufsize here is one of them, but do we need\n> that cast?  We know strlen(prefix) is a small known constant\n> integer, we know p is not smaller than buffer, we know (p-buffer) is\n> not larger than bufsize.\n\nYes, we do, because Windows will complain about a comparison between\nsigned and unsigned integer.  `p - buffer` is `ptrdiff_t`, which is 64\nbits, but `unsigned long` is only 32 bite, so they both get promoted to\nlong long, which is 64 bits and signed, and then the comparison is to\n`size_t`, which is 64 bits and unsigned.  The CI jobs fail without this.\n\nI'll make a note in the commit message to that effect.\n\n> OK.  Again, I suspect that commit_list may be a more natural thing\n> to use to accumulate the stash entries, but that is not a huge deal.\n\nI'll try to fix that in v7.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"519147","messageId":"9429a84c-d5e0-45df-8fd2-6dc92cf39d9e@gmail.com","threadId":"63424","inReplyTo":"20250522185524.18398-6-sandals@crustytoothpaste.net","subject":"Re: [PATCH v6 4/5] builtin/stash: provide a way to export stashes to a ref","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-05-29T16:01:03Z","receivedAt":"2025-05-29T16:01:08Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi brian\n\nThis is looking pretty nice, I don't have much to add to Junio's comments.\n\nOn 22/05/2025 19:55, brian m. carlson wrote:\n> [...]\n> +out:\n> +\tstrbuf_release(&msg);\n> +\trepo_unuse_commit_buffer(r, this, buffer);\n> +\tfree_commit_list(parents);\n\nI found this confusing when I was reading the caller as I couldn't see \nwhere the list of parent commits was being free()d. I think it would be \neasier to follow if this function did not take ownership of the list.\n\n> +\t/*\n> +\t * Now, create a set of commits identical to the regular stash commits,\n> +\t * but where their first parents form a chain to our original empty\n> +\t * base commit.\n> +\t */\n> +\tfor (i = (ssize_t)items.nr - 1; i >= 0; i--) {\n\nThis cast is pretty horrible. ssize_t is not guaranteed to be the same \nwidth as size_t (see [1] for a bug fix on NonStop caused by this). I \nthink we'd normally write this as\n\n\tfor (size_t i = items.nr; i > 0; i--) {\n\t\tconst struct object_id *oid = &items[i - 1];\n\n[1] c14e5a1a501 (transport-helper: use xread instead of read, 2019-01-03)\n\n> +\t\tstruct commit_list *parents = NULL;\n> +\t\tstruct commit_list **next = &parents;\n> +\t\tstruct object_id out;\n> +\t\tconst struct object_id *oid = items.oid + i;\n> [...]\n> +static int export_stash(int argc,\n> +\t\t\tconst char **argv,\n> +\t\t\tconst char *prefix,\n> +\t\t\tstruct repository *repo)\n> +{\n> +\tconst char *ref = NULL;\n> +\tenum export_action action = ACTION_NONE;\n> +\tstruct option options[] = {\n> +\t\tOPT_CMDMODE(0, \"print\", &action,\n> +\t\t\t    N_(\"print the object ID instead of writing it to a ref\"),\n> +\t\t\t    ACTION_PRINT),\n> +\t\tOPT_STRING(0, \"to-ref\", &ref, \"ref\",\n> +\t\t\t    N_(\"save the data to the given ref\")),\n> +\t\tOPT_END()\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, prefix, options,\n> +\t\t\t     git_stash_export_usage,\n> +\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n> +\n> +\tif (ref && action == ACTION_NONE)\n> +\t\taction = ACTION_TO_REF;\n> +\n> +\tif (action == ACTION_NONE)\n> +\t\treturn error(_(\"exactly one of --print and --to-ref is required\"));\nI don't think this protects against 'git stash export --print --to-ref'. \nIt is a bit odd to have a single option marked with OPT_CMDMODE() it \nmight be worth changing that to OPT_SET_INT().\n\nBest Wishes\n\nPhillip\n\n"},{"id":"519149","messageId":"3d0697a6-e337-4b58-98d3-304e0f17ae66@gmail.com","threadId":"63424","inReplyTo":"20250522185524.18398-4-sandals@crustytoothpaste.net","subject":"Re: [PATCH v6 2/5] reflog-walk: expose read_complete_reflog","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-05-29T16:01:20Z","receivedAt":"2025-05-29T16:01:24Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 22/05/2025 19:55, brian m. carlson wrote:\n> In a future commit, we'll use this function and the corresponding free\n> function to read the entire reflog.  Expose it in the header so we can\n> do so.\n\nWe already have refs_for_each_reflog_entry() and \nrefs_for_each_reflog_entry_reverse() for traversing the reflog entries \nso I'm a bit confused as to why we need to make the internal details public.\n\nThanks\n\nPhillip\n> Include the appropriate header files so that our header is complete.\n> \n> Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n> ---\n>   reflog-walk.c | 17 ++---------------\n>   reflog-walk.h | 18 ++++++++++++++++++\n>   2 files changed, 20 insertions(+), 15 deletions(-)\n> \n> diff --git a/reflog-walk.c b/reflog-walk.c\n> index c7070b13b0..b7a9d70966 100644\n> --- a/reflog-walk.c\n> +++ b/reflog-walk.c\n> @@ -9,19 +9,6 @@\n>   #include \"string-list.h\"\n>   #include \"reflog-walk.h\"\n>   \n> -struct complete_reflogs {\n> -\tchar *ref;\n> -\tchar *short_ref;\n> -\tstruct reflog_info {\n> -\t\tstruct object_id ooid, noid;\n> -\t\tchar *email;\n> -\t\ttimestamp_t timestamp;\n> -\t\tint tz;\n> -\t\tchar *message;\n> -\t} *items;\n> -\tint nr, alloc;\n> -};\n> -\n>   static int read_one_reflog(struct object_id *ooid, struct object_id *noid,\n>   \t\tconst char *email, timestamp_t timestamp, int tz,\n>   \t\tconst char *message, void *cb_data)\n> @@ -41,7 +28,7 @@ static int read_one_reflog(struct object_id *ooid, struct object_id *noid,\n>   \treturn 0;\n>   }\n>   \n> -static void free_complete_reflog(struct complete_reflogs *array)\n> +void free_complete_reflog(struct complete_reflogs *array)\n>   {\n>   \tint i;\n>   \n> @@ -64,7 +51,7 @@ static void complete_reflogs_clear(void *util, const char *str UNUSED)\n>   \tfree_complete_reflog(array);\n>   }\n>   \n> -static struct complete_reflogs *read_complete_reflog(const char *ref)\n> +struct complete_reflogs *read_complete_reflog(const char *ref)\n>   {\n>   \tstruct complete_reflogs *reflogs =\n>   \t\txcalloc(1, sizeof(struct complete_reflogs));\n> diff --git a/reflog-walk.h b/reflog-walk.h\n> index 989583dc55..8f0640f662 100644\n> --- a/reflog-walk.h\n> +++ b/reflog-walk.h\n> @@ -1,9 +1,24 @@\n>   #ifndef REFLOG_WALK_H\n>   #define REFLOG_WALK_H\n>   \n> +#include \"git-compat-util.h\"\n> +#include \"hash.h\"\n> +\n>   struct commit;\n>   struct reflog_walk_info;\n>   struct date_mode;\n> +struct complete_reflogs {\n> +\tchar *ref;\n> +\tchar *short_ref;\n> +\tstruct reflog_info {\n> +\t\tstruct object_id ooid, noid;\n> +\t\tchar *email;\n> +\t\ttimestamp_t timestamp;\n> +\t\tint tz;\n> +\t\tchar *message;\n> +\t} *items;\n> +\tint nr, alloc;\n> +};\n>   \n>   void init_reflog_walk(struct reflog_walk_info **info);\n>   void reflog_walk_info_release(struct reflog_walk_info *info);\n> @@ -24,4 +39,7 @@ int reflog_walk_empty(struct reflog_walk_info *walk);\n>   \n>   struct commit *next_reflog_entry(struct reflog_walk_info *reflog_info);\n>   \n> +void free_complete_reflog(struct complete_reflogs *array);\n> +struct complete_reflogs *read_complete_reflog(const char *ref);\n> +\n>   #endif\n> \n"},{"id":"519183","messageId":"xmqqo6vbhydw.fsf@gitster.g","threadId":"63424","inReplyTo":"3d0697a6-e337-4b58-98d3-304e0f17ae66@gmail.com","subject":"Re: [PATCH v6 2/5] reflog-walk: expose read_complete_reflog","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-29T21:59:07Z","receivedAt":"2025-05-29T21:59:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> On 22/05/2025 19:55, brian m. carlson wrote:\n>> In a future commit, we'll use this function and the corresponding free\n>> function to read the entire reflog.  Expose it in the header so we can\n>> do so.\n>\n> We already have refs_for_each_reflog_entry() and\n> refs_for_each_reflog_entry_reverse() for traversing the reflog entries\n> so I'm a bit confused as to why we need to make the internal details\n> public.\n\nThe suggestion for using this function is largely fault of mine, who\ndid not think of using the for-each helpers.\n\nLooking at the function signature of for-each-reflog-ent callback\n\n        typedef int each_reflog_ent_fn(\n                        struct object_id *old_oid, struct object_id *new_oid,\n                        const char *committer, timestamp_t timestamp,\n                        int tz, const char *msg, void *cb_data);\n\nI think it gives us the same piece of information, and is\npreferrable.\n\nThanks for a dose of sanity.\n"},{"id":"519352","messageId":"20250601223225.464076-2-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250601223225.464076-1-sandals@crustytoothpaste.net","subject":"[PATCH v7 1/4] object-name: make get_oid quietly return an error","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-01T22:32:22Z","receivedAt":"2025-06-01T22:32:43Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"A reasonable person looking at the signature and usage of get_oid and\nfriends might conclude that in the event of an error, it always returns\n-1.  However, this is not the case.  Instead, get_oid_basic dies if we\ngo too far back into the history of a reflog (or, when quiet, simply\nexits).\n\nThis is not especially useful, since in many cases, we might want to\nhandle this error differently.  Let's add a flag here to make it just\nreturn -1 like elsewhere in these code paths.\n\nNote that we cannot make this behavior the default, since we have many\nother codepaths that rely on the existing behavior, including in tests.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n hash.h        | 1 +\n object-name.c | 6 +++++-\n 2 files changed, 6 insertions(+), 1 deletion(-)\n\ndiff --git a/hash.h b/hash.h\nindex d6422ddf45..ec594c63a6 100644\n--- a/hash.h\n+++ b/hash.h\n@@ -216,6 +216,7 @@ struct object_id {\n #define GET_OID_REQUIRE_PATH         010000\n #define GET_OID_HASH_ANY             020000\n #define GET_OID_SKIP_AMBIGUITY_CHECK 040000\n+#define GET_OID_GENTLY              0100000\n \n #define GET_OID_DISAMBIGUATORS \\\n \t(GET_OID_COMMIT | GET_OID_COMMITTISH | \\\ndiff --git a/object-name.c b/object-name.c\nindex 9288b2dd24..851858975f 100644\n--- a/object-name.c\n+++ b/object-name.c\n@@ -1081,13 +1081,17 @@ static int get_oid_basic(struct repository *r, const char *str, int len,\n \t\t\t\t * still fill in the oid with the \"old\" value,\n \t\t\t\t * which we can use.\n \t\t\t\t */\n-\t\t\t} else {\n+\t\t\t} else if (!(flags & GET_OID_GENTLY)) {\n \t\t\t\tif (flags & GET_OID_QUIETLY) {\n \t\t\t\t\texit(128);\n \t\t\t\t}\n \t\t\t\tdie(_(\"log for '%.*s' only has %d entries\"),\n \t\t\t\t    len, str, co_cnt);\n \t\t\t}\n+\t\t\tif (flags & GET_OID_GENTLY) {\n+\t\t\t\tfree(real_ref);\n+\t\t\t\treturn -1;\n+\t\t\t}\n \t\t}\n \t}\n \n"},{"id":"519353","messageId":"20250601223225.464076-3-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250601223225.464076-1-sandals@crustytoothpaste.net","subject":"[PATCH v7 2/4] builtin/stash: factor out revision parsing into a function","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-01T22:32:23Z","receivedAt":"2025-06-01T22:32:45Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"We allow several special forms of stash names in this code.  In the\nfuture, we'll want to allow these same forms without parsing a stash\ncommit, so let's refactor this code out into a function for reuse.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n builtin/stash.c | 33 ++++++++++++++++++++++-----------\n 1 file changed, 22 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex cfbd92852a..ab491d5ff6 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -169,6 +169,25 @@ static void assert_stash_like(struct stash_info *info, const char *revision)\n \t\tdie(_(\"'%s' is not a stash-like commit\"), revision);\n }\n \n+static int parse_stash_revision(struct strbuf *revision, const char *commit, int quiet)\n+{\n+\tstrbuf_reset(revision);\n+\tif (!commit) {\n+\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n+\t\t\tif (!quiet)\n+\t\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tstrbuf_addf(revision, \"%s@{0}\", ref_stash);\n+\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n+\t\tstrbuf_addf(revision, \"%s@{%s}\", ref_stash, commit);\n+\t} else {\n+\t\tstrbuf_addstr(revision, commit);\n+\t}\n+\treturn 0;\n+}\n+\n static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n {\n \tint ret;\n@@ -196,17 +215,9 @@ static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n \tif (argc == 1)\n \t\tcommit = argv[0];\n \n-\tif (!commit) {\n-\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n-\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tstrbuf_addf(&info->revision, \"%s@{0}\", ref_stash);\n-\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n-\t\tstrbuf_addf(&info->revision, \"%s@{%s}\", ref_stash, commit);\n-\t} else {\n-\t\tstrbuf_addstr(&info->revision, commit);\n+\tstrbuf_init(&info->revision, 0);\n+\tif (parse_stash_revision(&info->revision, commit, 0)) {\n+\t\treturn -1;\n \t}\n \n \trevision = info->revision.buf;\n"},{"id":"519354","messageId":"20250601223225.464076-5-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250601223225.464076-1-sandals@crustytoothpaste.net","subject":"[PATCH v7 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-01T22:32:25Z","receivedAt":"2025-06-01T22:32:45Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Now that we have a way to export stashes to a ref, let's provide a way\nto import them from such a ref back to the stash.  This works much the\nway the export code does, except that we strip off the first parent\nchain commit and then store each resulting commit back to the stash.\n\nWe don't clear the stash first and instead add the specified stashes to\nthe top of the stash.  This is because users may want to export just a\nfew stashes, such as to share a small amount of work in progress with a\ncolleague, and it would be undesirable for the receiving user to lose\nall of their data.  For users who do want to replace the stash, it's\neasy to do to: simply run \"git stash clear\" first.\n\nWe specifically rely on the fact that we'll produce identical stash\ncommits on both sides in our tests.  This provides a cheap,\nstraightforward check for our tests and also makes it easy for users to\nsee if they already have the same data in both repositories.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n Documentation/git-stash.adoc |   7 ++\n builtin/stash.c              | 167 +++++++++++++++++++++++++++++++++++\n t/t3903-stash.sh             |  94 ++++++++++++++++++++\n 3 files changed, 268 insertions(+)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex e8efd43ba4..f8193b712e 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -24,6 +24,7 @@ SYNOPSIS\n 'git stash' create [<message>]\n 'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>\n 'git stash' export (--print | --to-ref <ref>) [<stash>...]\n+'git stash' import <commit>\n \n DESCRIPTION\n -----------\n@@ -161,6 +162,12 @@ export ( --print | --to-ref <ref> ) [<stash>...]::\n \ta chain of commits which can be transferred using the normal fetch and\n \tpush mechanisms, then imported using the `import` subcommand.\n \n+import <commit>::\n+\n+\tImport the specified stashes from the specified commit, which must have been\n+\tcreated by `export`, and add them to the list of stashes.  To replace the\n+\texisting stashes, use `clear` first.\n+\n OPTIONS\n -------\n -a::\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex bebe004e4d..2c5715eba9 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -61,6 +61,8 @@\n \tN_(\"git stash create [<message>]\")\n #define BUILTIN_STASH_EXPORT_USAGE \\\n \tN_(\"git stash export (--print | --to-ref <ref>) [<stash>...]\")\n+#define BUILTIN_STASH_IMPORT_USAGE \\\n+\tN_(\"git stash import <commit>\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -77,6 +79,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_CREATE_USAGE,\n \tBUILTIN_STASH_STORE_USAGE,\n \tBUILTIN_STASH_EXPORT_USAGE,\n+\tBUILTIN_STASH_IMPORT_USAGE,\n \tNULL\n };\n \n@@ -135,6 +138,10 @@ static const char * const git_stash_export_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_import_usage[] = {\n+\tBUILTIN_STASH_IMPORT_USAGE,\n+\tNULL\n+};\n \n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n@@ -144,6 +151,7 @@ static struct strbuf stash_index_path = STRBUF_INIT;\n  * b_commit is set to the base commit\n  * i_commit is set to the commit containing the index tree\n  * u_commit is set to the commit containing the untracked files tree\n+ * c_commit is set to the first parent (chain commit) when importing and is otherwise unset\n  * w_tree is set to the working tree\n  * b_tree is set to the base tree\n  * i_tree is set to the index tree\n@@ -154,6 +162,7 @@ struct stash_info {\n \tstruct object_id b_commit;\n \tstruct object_id i_commit;\n \tstruct object_id u_commit;\n+\tstruct object_id c_commit;\n \tstruct object_id w_tree;\n \tstruct object_id b_tree;\n \tstruct object_id i_tree;\n@@ -1992,6 +2001,163 @@ static int write_commit_with_parents(struct repository *r,\n \treturn ret;\n }\n \n+static int do_import_stash(struct repository *r, const char *rev)\n+{\n+\tstruct object_id chain;\n+\tint res = 0;\n+\tconst char *buffer = NULL;\n+\tunsigned long bufsize;\n+\tstruct commit *this = NULL;\n+\tstruct commit_list *items = NULL, *cur;\n+\tchar *msg = NULL;\n+\n+\tif (repo_get_oid(r, rev, &chain))\n+\t\treturn error(_(\"not a valid revision: %s\"), rev);\n+\n+\tthis = lookup_commit_reference(r, &chain);\n+\tif (!this)\n+\t\treturn error(_(\"not a commit: %s\"), rev);\n+\n+\t/*\n+\t * Walk the commit history, finding each stash entry, and load data into\n+\t * the array.\n+\t */\n+\tfor (;;) {\n+\t\tconst char *author, *committer;\n+\t\tsize_t author_len, committer_len;\n+\t\tconst char *p;\n+\t\tconst char *expected = \"git stash <git@stash> 1000684800 +0000\";\n+\t\tconst char *prefix = \"git stash: \";\n+\t\tstruct commit *stash;\n+\t\tstruct tree *tree = repo_get_commit_tree(r, this);\n+\n+\t\tif (!tree ||\n+\t\t    !oideq(&tree->object.oid, r->hash_algo->empty_tree) ||\n+\t\t    (this->parents &&\n+\t\t     (!this->parents->next || this->parents->next->next))) {\n+\t\t\tres = error(_(\"%s is not a valid exported stash commit\"),\n+\t\t\t\t\toid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\n+\t\tif (!this->parents) {\n+\t\t\t/*\n+\t\t\t * We don't have any parents.  Make sure this is our\n+\t\t\t * root commit.\n+\t\t\t */\n+\t\t\tauthor = find_commit_header(buffer, \"author\", &author_len);\n+\t\t\tcommitter = find_commit_header(buffer, \"committer\", &committer_len);\n+\n+\t\t\tif (!author || !committer) {\n+\t\t\t\terror(_(\"cannot parse commit %s\"), oid_to_hex(&this->object.oid));\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\n+\t\t\tif (author_len != strlen(expected) ||\n+\t\t\t    committer_len != strlen(expected) ||\n+\t\t\t    memcmp(author, expected, author_len) ||\n+\t\t\t    memcmp(committer, expected, committer_len)) {\n+\t\t\t\tres = error(_(\"found root commit %s with invalid data\"), oid_to_hex(&this->object.oid));\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tp = strstr(buffer, \"\\n\\n\");\n+\t\tif (!p) {\n+\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp += 2;\n+\t\tif (((size_t)(bufsize - (p - buffer)) < strlen(prefix)) ||\n+\t\t    memcmp(prefix, p, strlen(prefix))) {\n+\t\t\tres = error(_(\"found stash commit %s without expected prefix\"), oid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstash = this->parents->next->item;\n+\n+\t\tif (repo_parse_commit(r, this->parents->item) ||\n+\t\t    repo_parse_commit(r, stash)) {\n+\t\t\tres = error(_(\"cannot parse parents of commit: %s\"),\n+\t\t\t\t\toid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (check_stash_topology(r, stash)) {\n+\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t\toid_to_hex(&stash->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\t\tbuffer = NULL;\n+\t\titems = commit_list_insert(stash, &items);\n+\t\tthis = this->parents->item;\n+\t}\n+\n+\t/*\n+\t * Now, walk each entry, adding it to the stash as a normal stash\n+\t * commit.\n+\t */\n+\tfor (cur = items; cur; cur = cur->next) {\n+\t\tconst char *p;\n+\t\tstruct object_id *oid;\n+\n+\t\tthis = cur->item;\n+\t\toid = &this->object.oid;\n+\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\t\tif (!buffer) {\n+\t\t\tres = error(_(\"cannot read commit buffer for %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp = strstr(buffer, \"\\n\\n\");\n+\t\tif (!p) {\n+\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp += 2;\n+\t\tmsg = xmemdupz(p, bufsize - (p - buffer));\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\t\tbuffer = NULL;\n+\n+\t\tif (do_store_stash(oid, msg, 1)) {\n+\t\t\tres = error(_(\"cannot save the stash for %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\t\tFREE_AND_NULL(msg);\n+\t}\n+out:\n+\tif (this && buffer)\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\tfree_commit_list(items);\n+\tfree(msg);\n+\n+\treturn res;\n+}\n+\n+static int import_stash(int argc, const char **argv, const char *prefix,\n+\t\t\tstruct repository *repo)\n+{\n+\tstruct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_import_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (argc != 1)\n+\t\tusage_msg_opt(\"a revision is required\", git_stash_import_usage, options);\n+\n+\treturn do_import_stash(repo, argv[0]);\n+}\n+\n struct stash_entry_data {\n \tstruct repository *r;\n \tstruct commit_list **items;\n@@ -2175,6 +2341,7 @@ int cmd_stash(int argc,\n \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n \t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n \t\tOPT_SUBCOMMAND(\"export\", &fn, export_stash),\n+\t\tOPT_SUBCOMMAND(\"import\", &fn, import_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex 74666ff3e4..ca3dd45664 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -11,6 +11,13 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-unique-files.sh\n \n+test_expect_success 'setup' '\n+\ttest_oid_cache <<-EOF\n+\texport_base sha1:73c9bab443d1f88ac61aa533d2eeaaa15451239c\n+\texport_base sha256:f210fa6346e3e2ce047bdb570426b17075980c1ac01fec8fc4b75bd3ab4bcfe4\n+\tEOF\n+'\n+\n test_expect_success 'usage on cmd and subcommand invalid option' '\n \ttest_expect_code 129 git stash --invalid-option 2>usage &&\n \tgrep \"or: git stash\" usage &&\n@@ -1412,6 +1419,93 @@ test_expect_success 'stash --keep-index --include-untracked with empty tree' '\n \t)\n '\n \n+test_expect_success 'stash export and import round-trip stashes' '\n+\tgit reset &&\n+\t>untracked &&\n+\t>tracked1 &&\n+\t>tracked2 &&\n+\tgit add tracked* &&\n+\tgit stash -- &&\n+\t>subdir/untracked &&\n+\t>subdir/tracked1 &&\n+\t>subdir/tracked2 &&\n+\tgit add subdir/tracked* &&\n+\tgit stash --include-untracked -- subdir/ &&\n+\tgit tag t-stash0 stash@{0} &&\n+\tgit tag t-stash1 stash@{1} &&\n+\tsimple=$(git stash export --print) &&\n+\tgit stash clear &&\n+\tgit stash import \"$simple\" &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\ttest_cmp_rev stash@{1} t-stash1 &&\n+\tgit stash export --to-ref refs/heads/foo &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" foo: &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" foo^: &&\n+\ttest_cmp_rev t-stash0 foo^2 &&\n+\ttest_cmp_rev t-stash1 foo^^2 &&\n+\tgit log --first-parent --format=\"%s\" refs/heads/foo >log &&\n+\tgrep \"^git stash: \" log >log2 &&\n+\ttest_line_count = 13 log2 &&\n+\tgit stash clear &&\n+\tgit stash import foo &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\ttest_cmp_rev stash@{1} t-stash1\n+'\n+\n+test_expect_success 'stash import appends commits' '\n+\tgit log --format=oneline -g refs/stash >out &&\n+\tcat out out >out2 &&\n+\tgit stash import refs/heads/foo &&\n+\tgit log --format=oneline -g refs/stash >actual &&\n+\ttest_line_count = $(wc -l <out2) actual\n+'\n+\n+test_expect_success 'stash export can accept specified stashes' '\n+\tgit stash clear &&\n+\tgit stash import foo &&\n+\tgit stash export --to-ref refs/heads/bar stash@{1} stash@{0} &&\n+\tgit stash clear &&\n+\tgit stash import refs/heads/bar &&\n+\ttest_cmp_rev stash@{1} t-stash0 &&\n+\ttest_cmp_rev stash@{0} t-stash1 &&\n+\tgit log --format=oneline -g refs/stash >actual &&\n+\ttest_line_count = 2 actual\n+'\n+\n+test_expect_success 'stash can import and export zero stashes' '\n+\tgit stash clear &&\n+\tgit stash export --to-ref refs/heads/baz &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" baz: &&\n+\ttest_cmp_rev \"$(test_oid export_base)\" baz &&\n+\ttest_must_fail git rev-parse baz^1 &&\n+\tgit stash import baz &&\n+\ttest_must_fail git rev-parse refs/stash\n+'\n+\n+test_expect_success 'stash rejects invalid attempts to import commits' '\n+\tgit stash import foo &&\n+\ttest_must_fail git stash import HEAD 2>output &&\n+\toid=$(git rev-parse HEAD) &&\n+\tgrep \"$oid is not a valid exported stash commit\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\n+\tgit checkout --orphan orphan &&\n+\tgit commit-tree $(test_oid empty_tree) -p \"$oid\" -p \"$oid^\" -m \"\" >fake-commit &&\n+\tgit update-ref refs/heads/orphan \"$(cat fake-commit)\" &&\n+\toid=$(git rev-parse HEAD) &&\n+\ttest_must_fail git stash import orphan 2>output &&\n+\tgrep \"found stash commit $oid without expected prefix\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\n+\tgit checkout --orphan orphan2 &&\n+\tgit commit-tree $(test_oid empty_tree) -m \"\" >fake-commit &&\n+\tgit update-ref refs/heads/orphan2 \"$(cat fake-commit)\" &&\n+\toid=$(git rev-parse HEAD) &&\n+\ttest_must_fail git stash import orphan2 2>output &&\n+\tgrep \"found root commit $oid with invalid data\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0\n+'\n+\n test_expect_success 'stash apply should succeed with unmodified file' '\n \techo base >file &&\n \tgit add file &&\n"},{"id":"519355","messageId":"20250601223225.464076-4-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250601223225.464076-1-sandals@crustytoothpaste.net","subject":"[PATCH v7 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-01T22:32:24Z","receivedAt":"2025-06-01T22:32:45Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"A common user problem is how to sync in-progress work to another\nmachine.  Users currently must use some sort of transfer of the working\ntree, which poses security risks and also necessarily causes the index\nto become dirty.  The experience is suboptimal and frustrating for\nusers.\n\nA reasonable idea is to use the stash for this purpose, but the stash is\nstored in the reflog, not in a ref, and as such it cannot be pushed or\npulled.  This also means that it cannot be saved into a bundle or\npreserved elsewhere, which is a problem when using throwaway development\nenvironments.\n\nIn addition, users often want to replicate stashes across machines, such\nas when they must use multiple machines or when they use throwaway dev\nenvironments, such as those based on the Devcontainer spec, where they\nmight otherwise lose various in-progress work.\n\nLet's solve this problem by allowing the user to export the stash to a\nref (or, to just write it into the repository and print the hash, à la\ngit commit-tree).  Introduce git stash export, which writes a chain of\ncommits where the first parent is always a chain to the previous stash,\nor to a single, empty commit (for the final item) and the second is the\nstash commit normally written to the reflog.\n\nIterate over each stash from top to bottom, looking up the data for each\none, and then create the chain from the single empty commit back up in\nreverse order.  Generate a predictable empty commit so our behavior is\nreproducible.  Create a useful commit message, preserving the author and\ncommitter information, to help users identify stash commits when viewing\nthem as normal commits.\n\nIf the user has specified specific stashes they'd like to export\ninstead, use those instead of iterating over all of the stashes.\n\nAs part of this, specifically request quiet behavior when looking up the\nOID for a revision because we will eventually hit a revision that\ndoesn't exist and we don't want to die when that occurs.\n\nWhen exporting stashes, be sure to verify that they look like valid\nstashes and don't contain invalid data.  This will help avoid failures\non import or problems due to attempting to export invalid refs that are\nnot stashes.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n Documentation/git-stash.adoc |  22 ++-\n builtin/stash.c              | 260 +++++++++++++++++++++++++++++++++++\n 2 files changed, 281 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 1a5177f498..e8efd43ba4 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -23,6 +23,7 @@ SYNOPSIS\n 'git stash' clear\n 'git stash' create [<message>]\n 'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>\n+'git stash' export (--print | --to-ref <ref>) [<stash>...]\n \n DESCRIPTION\n -----------\n@@ -154,6 +155,12 @@ store::\n \treflog.  This is intended to be useful for scripts.  It is\n \tprobably not the command you want to use; see \"push\" above.\n \n+export ( --print | --to-ref <ref> ) [<stash>...]::\n+\n+\tExport the specified stashes, or all of them if none are specified, to\n+\ta chain of commits which can be transferred using the normal fetch and\n+\tpush mechanisms, then imported using the `import` subcommand.\n+\n OPTIONS\n -------\n -a::\n@@ -242,6 +249,19 @@ literally (including newlines and quotes).\n +\n Quiet, suppress feedback messages.\n \n+--print::\n+\tThis option is only valid for `export`.\n++\n+Create the chain of commits representing the exported stashes without\n+storing it anywhere in the ref namespace and print the object ID to\n+standard output.  This is designed for scripts.\n+\n+--to-ref::\n+\tThis option is only valid for `export`.\n++\n+Create the chain of commits representing the exported stashes and store\n+it to the specified ref.\n+\n \\--::\n \tThis option is only valid for `push` command.\n +\n@@ -259,7 +279,7 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].\n \n <stash>::\n \tThis option is only valid for `apply`, `branch`, `drop`, `pop`,\n-\t`show` commands.\n+\t`show`, and `export` commands.\n +\n A reference of the form `stash@{<revision>}`. When no `<stash>` is\n given, the latest stash is assumed (that is, `stash@{0}`).\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex ab491d5ff6..bebe004e4d 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -28,7 +28,10 @@\n #include \"log-tree.h\"\n #include \"diffcore.h\"\n #include \"reflog.h\"\n+#include \"reflog-walk.h\"\n #include \"add-interactive.h\"\n+#include \"oid-array.h\"\n+#include \"commit.h\"\n \n #define INCLUDE_ALL_FILES 2\n \n@@ -56,6 +59,8 @@\n \t   \"          [-u | --include-untracked] [-a | --all] [<message>]\")\n #define BUILTIN_STASH_CREATE_USAGE \\\n \tN_(\"git stash create [<message>]\")\n+#define BUILTIN_STASH_EXPORT_USAGE \\\n+\tN_(\"git stash export (--print | --to-ref <ref>) [<stash>...]\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -71,6 +76,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_CLEAR_USAGE,\n \tBUILTIN_STASH_CREATE_USAGE,\n \tBUILTIN_STASH_STORE_USAGE,\n+\tBUILTIN_STASH_EXPORT_USAGE,\n \tNULL\n };\n \n@@ -124,6 +130,12 @@ static const char * const git_stash_save_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_export_usage[] = {\n+\tBUILTIN_STASH_EXPORT_USAGE,\n+\tNULL\n+};\n+\n+\n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n \n@@ -160,6 +172,33 @@ static void free_stash_info(struct stash_info *info)\n \tstrbuf_release(&info->revision);\n }\n \n+static int check_stash_topology(struct repository *r, struct commit *stash)\n+{\n+\tstruct commit *p1, *p2, *p3 = NULL;\n+\n+\t/* stash must have two or three parents */\n+\tif (!stash->parents || !stash->parents->next ||\n+\t\t\t(stash->parents->next->next && stash->parents->next->next->next))\n+\t\treturn -1;\n+\tp1 = stash->parents->item;\n+\tp2 = stash->parents->next->item;\n+\tif (stash->parents->next->next)\n+\t\tp3 = stash->parents->next->next->item;\n+\tif (repo_parse_commit(r, p1) || repo_parse_commit(r, p2) ||\n+\t\t\t(p3 && repo_parse_commit(r, p3)))\n+\t\treturn -1;\n+\t/* p2 must have a single parent, p3 must have no parents */\n+\tif (!p2->parents || p2->parents->next || (p3 && p3->parents))\n+\t\treturn -1;\n+\tif (repo_parse_commit(r, p2->parents->item))\n+\t\treturn -1;\n+\t/* p2^1 must equal p1 */\n+\tif (!oideq(&p1->object.oid, &p2->parents->item->object.oid))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n static void assert_stash_like(struct stash_info *info, const char *revision)\n {\n \tif (get_oidf(&info->b_commit, \"%s^1\", revision) ||\n@@ -1895,6 +1934,226 @@ static int save_stash(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int write_commit_with_parents(struct repository *r,\n+\t\t\t\t     struct object_id *out,\n+\t\t\t\t     const struct object_id *oid,\n+\t\t\t\t     struct commit_list *parents)\n+{\n+\tsize_t author_len, committer_len;\n+\tstruct commit *this;\n+\tconst char *orig_author, *orig_committer;\n+\tchar *author = NULL, *committer = NULL;\n+\tconst char *buffer;\n+\tunsigned long bufsize;\n+\tconst char *p;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tint ret = 0;\n+\tstruct ident_split id;\n+\n+\tthis = lookup_commit_reference(r, oid);\n+\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\torig_author = find_commit_header(buffer, \"author\", &author_len);\n+\torig_committer = find_commit_header(buffer, \"committer\", &committer_len);\n+\n+\tif (!orig_author || !orig_committer) {\n+\t\tret = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\n+\tif (split_ident_line(&id, orig_author, author_len) < 0 ||\n+\t    split_ident_line(&id, orig_committer, committer_len) < 0) {\n+\t\tret = error(_(\"invalid author or committer for %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\n+\tp = strstr(buffer, \"\\n\\n\");\n+\tstrbuf_addstr(&msg, \"git stash: \");\n+\n+\tif (p)\n+\t\tstrbuf_add(&msg, p + 2, bufsize - (p + 2 - buffer));\n+\tstrbuf_complete_line(&msg);\n+\n+\tauthor = xmemdupz(orig_author, author_len);\n+\tcommitter = xmemdupz(orig_committer, committer_len);\n+\n+\tif (commit_tree_extended(msg.buf, msg.len,\n+\t\t\t\t r->hash_algo->empty_tree, parents,\n+\t\t\t\t out, author, committer,\n+\t\t\t\t NULL, NULL)) {\n+\t\tret = error(_(\"could not write commit\"));\n+\t\tgoto out;\n+\t}\n+out:\n+\tstrbuf_release(&msg);\n+\trepo_unuse_commit_buffer(r, this, buffer);\n+\tfree_commit_list(parents);\n+\tfree(author);\n+\tfree(committer);\n+\treturn ret;\n+}\n+\n+struct stash_entry_data {\n+\tstruct repository *r;\n+\tstruct commit_list **items;\n+\tsize_t count;\n+};\n+\n+static int collect_stash_entries(struct object_id *old_oid UNUSED,\n+\t\t\t\t struct object_id *new_oid,\n+\t\t\t\t const char *committer UNUSED,\n+\t\t\t\t timestamp_t timestamp UNUSED,\n+\t\t\t\t int tz UNUSED, const char *msg UNUSED,\n+\t\t\t\t void *cb_data)\n+{\n+\tstruct stash_entry_data *data = cb_data;\n+\tstruct commit *stash;\n+\n+\tdata->count++;\n+\tstash = lookup_commit_reference(data->r, new_oid);\n+\tif (!stash || check_stash_topology(data->r, stash)) {\n+\t\treturn error(_(\"%s does not look like a stash commit\"),\n+\t\t\t     oid_to_hex(new_oid));\n+\t}\n+\tdata->items = commit_list_append(stash, data->items);\n+\treturn 0;\n+}\n+\n+static int do_export_stash(struct repository *r,\n+\t\t\t   const char *ref,\n+\t\t\t   int argc,\n+\t\t\t   const char **argv)\n+{\n+\tstruct object_id base;\n+\tstruct object_context unused;\n+\tstruct commit *prev;\n+\tstruct commit_list *items = NULL, **iter = &items, *cur;\n+\tint res = 0;\n+\tint i;\n+\tstruct strbuf revision = STRBUF_INIT;\n+\tconst char *author, *committer;\n+\n+\t/*\n+\t * This is an arbitrary, fixed date, specifically the one used by git\n+\t * format-patch.  The goal is merely to produce reproducible output.\n+\t */\n+\tprepare_fallback_ident(\"git stash\", \"git@stash\");\n+\tauthor = fmt_ident(\"git stash\", \"git@stash\", WANT_BLANK_IDENT,\n+\t\t\t   \"2001-09-17T00:00:00Z\", 0);\n+\tcommitter = fmt_ident(\"git stash\", \"git@stash\", WANT_BLANK_IDENT,\n+\t\t\t      \"2001-09-17T00:00:00Z\", 0);\n+\n+\t/* First, we create a single empty commit. */\n+\tif (commit_tree_extended(\"\", 0, r->hash_algo->empty_tree, NULL,\n+\t\t\t\t &base, author, committer, NULL, NULL))\n+\t\treturn error(_(\"unable to write base commit\"));\n+\n+\tprev = lookup_commit_reference(r, &base);\n+\n+\tif (argc) {\n+\t\t/*\n+\t\t * Find each specified stash, and load data into the array.\n+\t\t */\n+\t\tfor (i = 0; i < argc; i++) {\n+\t\t\tstruct object_id oid;\n+\t\t\tstruct commit *stash;\n+\n+\t\t\tif (parse_stash_revision(&revision, argv[i], 1) ||\n+\t\t\t    get_oid_with_context(r, revision.buf,\n+\t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n+\t\t\t\t\t\t &oid, &unused)) {\n+\t\t\t\tres = error(_(\"unable to find stash entry %s\"), argv[i]);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\n+\t\t\tstash = lookup_commit_reference(r, &oid);\n+\t\t\tif (!stash || check_stash_topology(r, stash)) {\n+\t\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t\t\trevision.buf);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t\titer = commit_list_append(stash, iter);\n+\t\t}\n+\t} else {\n+\t\t/*\n+\t\t * Walk the reflog, finding each stash entry, and load data into the\n+\t\t * array.\n+\t\t */\n+\t\tstruct stash_entry_data cb_data = {\n+\t\t\t.r = r, .items = iter,\n+\t\t};\n+\t\tif (refs_for_each_reflog_ent_reverse(get_main_ref_store(r),\n+\t\t\t\t\t\t     \"refs/stash\",\n+\t\t\t\t\t\t     collect_stash_entries,\n+\t\t\t\t\t\t     &cb_data) && cb_data.count)\n+\t\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Now, create a set of commits identical to the regular stash commits,\n+\t * but where their first parents form a chain to our original empty\n+\t * base commit.\n+\t */\n+\titems = reverse_commit_list(items);\n+\tfor (cur = items; cur; cur = cur->next) {\n+\t\tstruct commit_list *parents = NULL;\n+\t\tstruct commit_list **next = &parents;\n+\t\tstruct object_id out;\n+\t\tstruct commit *stash = cur->item;\n+\n+\t\tnext = commit_list_append(prev, next);\n+\t\tnext = commit_list_append(stash, next);\n+\t\tres = write_commit_with_parents(r, &out, &stash->object.oid, parents);\n+\t\tif (res)\n+\t\t\tgoto out;\n+\t\tprev = lookup_commit_reference(r, &out);\n+\t}\n+\tif (ref)\n+\t\trefs_update_ref(get_main_ref_store(r), NULL, ref,\n+\t\t\t\t&prev->object.oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR);\n+\telse\n+\t\tputs(oid_to_hex(&prev->object.oid));\n+out:\n+\tstrbuf_release(&revision);\n+\tfree_commit_list(items);\n+\n+\treturn res;\n+}\n+\n+enum export_action {\n+\tACTION_NONE,\n+\tACTION_PRINT,\n+\tACTION_TO_REF,\n+};\n+\n+static int export_stash(int argc,\n+\t\t\tconst char **argv,\n+\t\t\tconst char *prefix,\n+\t\t\tstruct repository *repo)\n+{\n+\tconst char *ref = NULL;\n+\tenum export_action action = ACTION_NONE;\n+\tstruct option options[] = {\n+\t\tOPT_CMDMODE(0, \"print\", &action,\n+\t\t\t    N_(\"print the object ID instead of writing it to a ref\"),\n+\t\t\t    ACTION_PRINT),\n+\t\tOPT_STRING(0, \"to-ref\", &ref, \"ref\",\n+\t\t\t    N_(\"save the data to the given ref\")),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_export_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (ref && action == ACTION_NONE)\n+\t\taction = ACTION_TO_REF;\n+\n+\tif (action == ACTION_NONE)\n+\t\treturn error(_(\"exactly one of --print and --to-ref is required\"));\n+\n+\treturn do_export_stash(repo, ref, argc, argv);\n+}\n+\n int cmd_stash(int argc,\n \t      const char **argv,\n \t      const char *prefix,\n@@ -1915,6 +2174,7 @@ int cmd_stash(int argc,\n \t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n \t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n+\t\tOPT_SUBCOMMAND(\"export\", &fn, export_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\n"},{"id":"519356","messageId":"20250601223225.464076-1-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250522185524.18398-2-sandals@crustytoothpaste.net","subject":"[PATCH v7 0/4] Importing and exporting stashes to refs","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-01T22:32:21Z","receivedAt":"2025-06-01T22:32:45Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Stashes are currently stored using the reflog in a given repository.\nThis is an interesting and novel way to handle them, but there is no way\nto easily move a set of stashes across machines.  For example, groups of\nstashes cannot be bundled, pushed, or fetched.\n\nLet's solve this problem by allowing users to import and export stashes\nto a chain of commits.  The commits used in a stash export contain two\nparents: one which is the pointer to the next exported stash (or to an\nempty commit with no parents if there are no more) and the second is the\nstash commit that would normally be stored in the reflog.\n\nOriginal thread at message-ID: <20220310173236.4165310-1-sandals@crustytoothpaste.net>\n\nChanges from v6:\n* Add Phillip's sign-off to the last patch.\n* Use `commit_list` for tracking commits.\n* Use reflog entry walker.\n* Fix some commit messages for improved legibility.\n* Rephrase some error messages for precision.\n* Drop the patch that exposes `read_complete_reflog` since it is no longer necessary.\n\nChanges from v5:\n* Rename `parse_revision`.\n* Remove extra call to `free_stash_info`.\n* Fix parsing of existing commit.\n* Add more validation of imported stash commits.\n* Add more tests for improved validation of imported stash commits.\n* Explicitly cast `items.nr` and make the iteration counter an `ssize_t`\n  to avoid casting problems.\n* Don't require a trailing `\\n\\n` in commit messages.\n* Use `read_complete_reflog` to walk reflogs.\n* Be more defensive when using `lookup_commit_reference`.\n* Apply parts of Phillip's patches for improved robustness.\n* Update commit message to explain additional use cases.\n* Use `OPT_STRING` for `--to-ref`.\n\nChanges from v4:\n* Fix another use of oid_array.\n* Fix various memory leaks.\n* Fix a segfault which appeared after a rebase.\n* Use strstr for commits since we don't need to worry about NUL.\n* Added some additional tests.\n* Verify the ident values we're using to avoid using bad values.\n* Various other code cleanups.\n* Rebase on `master`.\n\nChanges from v3:\n* Fix strbuf handling to avoid leaks and generally be more sensible.\n* Make use of the error return code more often.\n* Use oid_array.\n* Tidy various parts of the code and fix long lines.\n* Simplify tests using git tag.\n* Shorten and tidy tests.\n* Add an additional test covering the base commit OID and importing and\n  exporting empty stashes.\n\nChanges from v2:\n* Fix uninitialized strbuf.\n* Avoid C99-style initializations.\n\nChanges from v1:\n* Change storage format as suggested by Junio.\n* Rename to GIT_OID_GENTLY.\n* Remove unnecessary initializations.\n* Use ALLOC_GROW_BY.\n* Ensure completely reproducible exports.\n* Avoid size_t.\n* Various other code cleanups.\n\nbrian m. carlson (4):\n  object-name: make get_oid quietly return an error\n  builtin/stash: factor out revision parsing into a function\n  builtin/stash: provide a way to export stashes to a ref\n  builtin/stash: provide a way to import stashes from a ref\n\n Documentation/git-stash.adoc |  29 ++-\n builtin/stash.c              | 460 ++++++++++++++++++++++++++++++++++-\n hash.h                       |   1 +\n object-name.c                |   6 +-\n t/t3903-stash.sh             |  94 +++++++\n 5 files changed, 577 insertions(+), 13 deletions(-)\n\n"},{"id":"519757","messageId":"127d9d03-e94e-4928-9c6d-07a5396ca325@gmail.com","threadId":"63424","inReplyTo":"20250601223225.464076-4-sandals@crustytoothpaste.net","subject":"Re: [PATCH v7 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-06-05T09:25:42Z","receivedAt":"2025-06-05T09:25:48Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi brian\n\nThere are a couple of points outstanding from my last review. The first \npoint below about the option handling definitely needs fixing, the other \nis more of a matter of personal taste.\n\nOn 01/06/2025 23:32, brian m. carlson wrote:\n> \n> diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\n> index 1a5177f498..e8efd43ba4 100644\n> --- a/Documentation/git-stash.adoc\n> +++ b/Documentation/git-stash.adoc\n> @@ -23,6 +23,7 @@ SYNOPSIS\n>   'git stash' clear\n>   'git stash' create [<message>]\n>   'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>\n> +'git stash' export (--print | --to-ref <ref>) [<stash>...]\n>   \n>   DESCRIPTION\n>   -----------\n> @@ -154,6 +155,12 @@ store::\n>   \treflog.  This is intended to be useful for scripts.  It is\n>   \tprobably not the command you want to use; see \"push\" above.\n>   \n> +export ( --print | --to-ref <ref> ) [<stash>...]::\n\n--print and --to-ref are documented as mutually exclusive but that is \nnot implemented in the code.\n\n     git stash export --print --to-ref refs/exported-stash\n\ncreates refs/exported-stash but does not print the object id. We should \neither ensure they are actually mutually exclusive or just allow both. I \ndon't have a strong preference either way - I'm not sure it is \nparticularly useful for the user to be able to specify both but it \ndoesn't do any harm so long as we honor both options.\n\n>   \n> +static int write_commit_with_parents(struct repository *r,\n> +\t\t\t\t     struct object_id *out,\n> +\t\t\t\t     const struct object_id *oid,\n> +\t\t\t\t     struct commit_list *parents)\n> +{\n> [...]\n> +out:\n> +\tstrbuf_release(&msg);\n> +\trepo_unuse_commit_buffer(r, this, buffer);\n> +\tfree_commit_list(parents);\n\nI think taking ownership of function parameters like this makes the code \nharder to reason about because C has no way for the function prototype \nto signal to the reader that the ownership is transferred. It would be \neasier to see that the list of parent commits was not leaked if it was \nfreed by the caller.\n\nBest Wishes\n\nPhillip\n\n"},{"id":"519758","messageId":"1585978c-f284-41a0-a34c-e31fa8502309@gmail.com","threadId":"63424","inReplyTo":"20250601223225.464076-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH v7 0/4] Importing and exporting stashes to refs","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-06-05T09:38:42Z","receivedAt":"2025-06-05T09:38:48Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi brian\n\nI've left a couple of comments on patch 3 but everything else looks fine \nto me\n\nThanks\n\nPhillip\n\nOn 01/06/2025 23:32, brian m. carlson wrote:\n> Stashes are currently stored using the reflog in a given repository.\n> This is an interesting and novel way to handle them, but there is no way\n> to easily move a set of stashes across machines.  For example, groups of\n> stashes cannot be bundled, pushed, or fetched.\n> \n> Let's solve this problem by allowing users to import and export stashes\n> to a chain of commits.  The commits used in a stash export contain two\n> parents: one which is the pointer to the next exported stash (or to an\n> empty commit with no parents if there are no more) and the second is the\n> stash commit that would normally be stored in the reflog.\n> \n> Original thread at message-ID: <20220310173236.4165310-1-sandals@crustytoothpaste.net>\n> \n> Changes from v6:\n> * Add Phillip's sign-off to the last patch.\n> * Use `commit_list` for tracking commits.\n> * Use reflog entry walker.\n> * Fix some commit messages for improved legibility.\n> * Rephrase some error messages for precision.\n> * Drop the patch that exposes `read_complete_reflog` since it is no longer necessary.\n> \n> Changes from v5:\n> * Rename `parse_revision`.\n> * Remove extra call to `free_stash_info`.\n> * Fix parsing of existing commit.\n> * Add more validation of imported stash commits.\n> * Add more tests for improved validation of imported stash commits.\n> * Explicitly cast `items.nr` and make the iteration counter an `ssize_t`\n>    to avoid casting problems.\n> * Don't require a trailing `\\n\\n` in commit messages.\n> * Use `read_complete_reflog` to walk reflogs.\n> * Be more defensive when using `lookup_commit_reference`.\n> * Apply parts of Phillip's patches for improved robustness.\n> * Update commit message to explain additional use cases.\n> * Use `OPT_STRING` for `--to-ref`.\n> \n> Changes from v4:\n> * Fix another use of oid_array.\n> * Fix various memory leaks.\n> * Fix a segfault which appeared after a rebase.\n> * Use strstr for commits since we don't need to worry about NUL.\n> * Added some additional tests.\n> * Verify the ident values we're using to avoid using bad values.\n> * Various other code cleanups.\n> * Rebase on `master`.\n> \n> Changes from v3:\n> * Fix strbuf handling to avoid leaks and generally be more sensible.\n> * Make use of the error return code more often.\n> * Use oid_array.\n> * Tidy various parts of the code and fix long lines.\n> * Simplify tests using git tag.\n> * Shorten and tidy tests.\n> * Add an additional test covering the base commit OID and importing and\n>    exporting empty stashes.\n> \n> Changes from v2:\n> * Fix uninitialized strbuf.\n> * Avoid C99-style initializations.\n> \n> Changes from v1:\n> * Change storage format as suggested by Junio.\n> * Rename to GIT_OID_GENTLY.\n> * Remove unnecessary initializations.\n> * Use ALLOC_GROW_BY.\n> * Ensure completely reproducible exports.\n> * Avoid size_t.\n> * Various other code cleanups.\n> \n> brian m. carlson (4):\n>    object-name: make get_oid quietly return an error\n>    builtin/stash: factor out revision parsing into a function\n>    builtin/stash: provide a way to export stashes to a ref\n>    builtin/stash: provide a way to import stashes from a ref\n> \n>   Documentation/git-stash.adoc |  29 ++-\n>   builtin/stash.c              | 460 ++++++++++++++++++++++++++++++++++-\n>   hash.h                       |   1 +\n>   object-name.c                |   6 +-\n>   t/t3903-stash.sh             |  94 +++++++\n>   5 files changed, 577 insertions(+), 13 deletions(-)\n> \n\n"},{"id":"520084","messageId":"8498845e-3580-4f37-b60a-8f32c5d41f15@app.fastmail.com","threadId":"63424","inReplyTo":"20250601223225.464076-4-sandals@crustytoothpaste.net","subject":"Re: [PATCH v7 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-06-11T11:31:19Z","receivedAt":"2025-06-11T11:31:41Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"I only bring up this minor point since I read that there will be a next\nversion anyway.\n\nOn Mon, Jun 2, 2025, at 00:32, brian m. carlson wrote:\n> +--print::\n> +\tThis option is only valid for `export`.\n> ++\n> +Create the chain of commits representing the exported stashes without\n> +storing it anywhere in the ref namespace and print the object ID to\n> +standard output.  This is designed for scripts.\n> +\n> +--to-ref::\n> +\tThis option is only valid for `export`.\n> ++\n\nThe existing options say “for [the] `<cmd>` command”.  So that’s a minor\ndeviation from the convention.\n\n-- \nKristoffer Haugsbakk\n"},{"id":"520085","messageId":"3526b0c0-7020-4c39-bc09-99b63a58db8a@app.fastmail.com","threadId":"63424","inReplyTo":"20250508234458.3665894-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 0/4] Importing and exporting stashes to refs","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-06-11T11:35:12Z","receivedAt":"2025-06-11T11:35:33Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Fri, May 9, 2025, at 01:44, brian m. carlson wrote:\n> Stashes are currently stored using the reflog in a given repository.\n> This is an interesting and novel way to handle them, but there is no way\n> to easily move a stash across machines.  For example, stashes cannot be\n> bundled, pushed, or fetched.\n\nI think this feature works well.  Exporting locally is simple.  Using a\nwhatever-ref to export to the remote is a fine implementation.\nImporting locally is simple as well.  The `--print` option for\nscript/machine consumption is a nice touch.\n\nAlso I think the DAG implementation is cool.\n\n-- \nKristoffer Haugsbakk\n"},{"id":"520136","messageId":"aEoStanN-es2CyDr@fruit.crustytoothpaste.net","threadId":"63424","inReplyTo":"8498845e-3580-4f37-b60a-8f32c5d41f15@app.fastmail.com","subject":"Re: [PATCH v7 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-11T23:35:17Z","receivedAt":"2025-06-11T23:42:32Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-06-11 at 11:31:19, Kristoffer Haugsbakk wrote:\n> I only bring up this minor point since I read that there will be a next\n> version anyway.\n\nI was just about to send it out, so I'm glad I saw this before doing so.\n\n> On Mon, Jun 2, 2025, at 00:32, brian m. carlson wrote:\n> > +--print::\n> > +\tThis option is only valid for `export`.\n> > ++\n> > +Create the chain of commits representing the exported stashes without\n> > +storing it anywhere in the ref namespace and print the object ID to\n> > +standard output.  This is designed for scripts.\n> > +\n> > +--to-ref::\n> > +\tThis option is only valid for `export`.\n> > ++\n> \n> The existing options say “for [the] `<cmd>` command”.  So that’s a minor\n> deviation from the convention.\n\nI can change that here.  I'll use \"for the `<cmd>` command`\" because I\nthink it reads better with the article than without, even though that's\na little less consistent.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"520142","messageId":"aEojQ34wpqhCjGaV@fruit.crustytoothpaste.net","threadId":"63424","inReplyTo":"3526b0c0-7020-4c39-bc09-99b63a58db8a@app.fastmail.com","subject":"Re: [PATCH v5 0/4] Importing and exporting stashes to refs","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-12T00:45:55Z","receivedAt":"2025-06-12T00:45:58Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-06-11 at 11:35:12, Kristoffer Haugsbakk wrote:\n> I think this feature works well.  Exporting locally is simple.  Using a\n> whatever-ref to export to the remote is a fine implementation.\n> Importing locally is simple as well.  The `--print` option for\n> script/machine consumption is a nice touch.\n> \n> Also I think the DAG implementation is cool.\n\nThank you, I appreciate that feedback.  (I would also have appreciated\nfeedback about how it could be better, but I do like that you're happy\nwith it.)\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"520143","messageId":"20250612011221.4158484-3-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250612011221.4158484-1-sandals@crustytoothpaste.net","subject":"[PATCH v8 2/4] builtin/stash: factor out revision parsing into a function","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-12T01:12:18Z","receivedAt":"2025-06-12T01:12:24Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"We allow several special forms of stash names in this code.  In the\nfuture, we'll want to allow these same forms without parsing a stash\ncommit, so let's refactor this code out into a function for reuse.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n builtin/stash.c | 33 ++++++++++++++++++++++-----------\n 1 file changed, 22 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex cfbd92852a..ab491d5ff6 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -169,6 +169,25 @@ static void assert_stash_like(struct stash_info *info, const char *revision)\n \t\tdie(_(\"'%s' is not a stash-like commit\"), revision);\n }\n \n+static int parse_stash_revision(struct strbuf *revision, const char *commit, int quiet)\n+{\n+\tstrbuf_reset(revision);\n+\tif (!commit) {\n+\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n+\t\t\tif (!quiet)\n+\t\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tstrbuf_addf(revision, \"%s@{0}\", ref_stash);\n+\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n+\t\tstrbuf_addf(revision, \"%s@{%s}\", ref_stash, commit);\n+\t} else {\n+\t\tstrbuf_addstr(revision, commit);\n+\t}\n+\treturn 0;\n+}\n+\n static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n {\n \tint ret;\n@@ -196,17 +215,9 @@ static int get_stash_info(struct stash_info *info, int argc, const char **argv)\n \tif (argc == 1)\n \t\tcommit = argv[0];\n \n-\tif (!commit) {\n-\t\tif (!refs_ref_exists(get_main_ref_store(the_repository), ref_stash)) {\n-\t\t\tfprintf_ln(stderr, _(\"No stash entries found.\"));\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tstrbuf_addf(&info->revision, \"%s@{0}\", ref_stash);\n-\t} else if (strspn(commit, \"0123456789\") == strlen(commit)) {\n-\t\tstrbuf_addf(&info->revision, \"%s@{%s}\", ref_stash, commit);\n-\t} else {\n-\t\tstrbuf_addstr(&info->revision, commit);\n+\tstrbuf_init(&info->revision, 0);\n+\tif (parse_stash_revision(&info->revision, commit, 0)) {\n+\t\treturn -1;\n \t}\n \n \trevision = info->revision.buf;\n"},{"id":"520144","messageId":"20250612011221.4158484-2-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250612011221.4158484-1-sandals@crustytoothpaste.net","subject":"[PATCH v8 1/4] object-name: make get_oid quietly return an error","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-12T01:12:17Z","receivedAt":"2025-06-12T01:12:24Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"A reasonable person looking at the signature and usage of get_oid and\nfriends might conclude that in the event of an error, it always returns\n-1.  However, this is not the case.  Instead, get_oid_basic dies if we\ngo too far back into the history of a reflog (or, when quiet, simply\nexits).\n\nThis is not especially useful, since in many cases, we might want to\nhandle this error differently.  Let's add a flag here to make it just\nreturn -1 like elsewhere in these code paths.\n\nNote that we cannot make this behavior the default, since we have many\nother codepaths that rely on the existing behavior, including in tests.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n hash.h        | 1 +\n object-name.c | 6 +++++-\n 2 files changed, 6 insertions(+), 1 deletion(-)\n\ndiff --git a/hash.h b/hash.h\nindex d6422ddf45..ec594c63a6 100644\n--- a/hash.h\n+++ b/hash.h\n@@ -216,6 +216,7 @@ struct object_id {\n #define GET_OID_REQUIRE_PATH         010000\n #define GET_OID_HASH_ANY             020000\n #define GET_OID_SKIP_AMBIGUITY_CHECK 040000\n+#define GET_OID_GENTLY              0100000\n \n #define GET_OID_DISAMBIGUATORS \\\n \t(GET_OID_COMMIT | GET_OID_COMMITTISH | \\\ndiff --git a/object-name.c b/object-name.c\nindex 9288b2dd24..851858975f 100644\n--- a/object-name.c\n+++ b/object-name.c\n@@ -1081,13 +1081,17 @@ static int get_oid_basic(struct repository *r, const char *str, int len,\n \t\t\t\t * still fill in the oid with the \"old\" value,\n \t\t\t\t * which we can use.\n \t\t\t\t */\n-\t\t\t} else {\n+\t\t\t} else if (!(flags & GET_OID_GENTLY)) {\n \t\t\t\tif (flags & GET_OID_QUIETLY) {\n \t\t\t\t\texit(128);\n \t\t\t\t}\n \t\t\t\tdie(_(\"log for '%.*s' only has %d entries\"),\n \t\t\t\t    len, str, co_cnt);\n \t\t\t}\n+\t\t\tif (flags & GET_OID_GENTLY) {\n+\t\t\t\tfree(real_ref);\n+\t\t\t\treturn -1;\n+\t\t\t}\n \t\t}\n \t}\n \n"},{"id":"520145","messageId":"20250612011221.4158484-4-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250612011221.4158484-1-sandals@crustytoothpaste.net","subject":"[PATCH v8 3/4] builtin/stash: provide a way to export stashes to a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-12T01:12:19Z","receivedAt":"2025-06-12T01:12:24Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"A common user problem is how to sync in-progress work to another\nmachine.  Users currently must use some sort of transfer of the working\ntree, which poses security risks and also necessarily causes the index\nto become dirty.  The experience is suboptimal and frustrating for\nusers.\n\nA reasonable idea is to use the stash for this purpose, but the stash is\nstored in the reflog, not in a ref, and as such it cannot be pushed or\npulled.  This also means that it cannot be saved into a bundle or\npreserved elsewhere, which is a problem when using throwaway development\nenvironments.\n\nIn addition, users often want to replicate stashes across machines, such\nas when they must use multiple machines or when they use throwaway dev\nenvironments, such as those based on the Devcontainer spec, where they\nmight otherwise lose various in-progress work.\n\nLet's solve this problem by allowing the user to export the stash to a\nref (or, to just write it into the repository and print the hash, à la\ngit commit-tree).  Introduce git stash export, which writes a chain of\ncommits where the first parent is always a chain to the previous stash,\nor to a single, empty commit (for the final item) and the second is the\nstash commit normally written to the reflog.\n\nIterate over each stash from top to bottom, looking up the data for each\none, and then create the chain from the single empty commit back up in\nreverse order.  Generate a predictable empty commit so our behavior is\nreproducible.  Create a useful commit message, preserving the author and\ncommitter information, to help users identify stash commits when viewing\nthem as normal commits.\n\nIf the user has specified specific stashes they'd like to export\ninstead, use those instead of iterating over all of the stashes.\n\nAs part of this, specifically request quiet behavior when looking up the\nOID for a revision because we will eventually hit a revision that\ndoesn't exist and we don't want to die when that occurs.\n\nWhen exporting stashes, be sure to verify that they look like valid\nstashes and don't contain invalid data.  This will help avoid failures\non import or problems due to attempting to export invalid refs that are\nnot stashes.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n Documentation/git-stash.adoc |  22 ++-\n builtin/stash.c              | 260 +++++++++++++++++++++++++++++++++++\n 2 files changed, 281 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 1a5177f498..0aef0a5b86 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -23,6 +23,7 @@ SYNOPSIS\n 'git stash' clear\n 'git stash' create [<message>]\n 'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>\n+'git stash' export (--print | --to-ref <ref>) [<stash>...]\n \n DESCRIPTION\n -----------\n@@ -154,6 +155,12 @@ store::\n \treflog.  This is intended to be useful for scripts.  It is\n \tprobably not the command you want to use; see \"push\" above.\n \n+export ( --print | --to-ref <ref> ) [<stash>...]::\n+\n+\tExport the specified stashes, or all of them if none are specified, to\n+\ta chain of commits which can be transferred using the normal fetch and\n+\tpush mechanisms, then imported using the `import` subcommand.\n+\n OPTIONS\n -------\n -a::\n@@ -242,6 +249,19 @@ literally (including newlines and quotes).\n +\n Quiet, suppress feedback messages.\n \n+--print::\n+\tThis option is only valid for the `export` command.\n++\n+Create the chain of commits representing the exported stashes without\n+storing it anywhere in the ref namespace and print the object ID to\n+standard output.  This is designed for scripts.\n+\n+--to-ref::\n+\tThis option is only valid for the `export` command.\n++\n+Create the chain of commits representing the exported stashes and store\n+it to the specified ref.\n+\n \\--::\n \tThis option is only valid for `push` command.\n +\n@@ -259,7 +279,7 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].\n \n <stash>::\n \tThis option is only valid for `apply`, `branch`, `drop`, `pop`,\n-\t`show` commands.\n+\t`show`, and `export` commands.\n +\n A reference of the form `stash@{<revision>}`. When no `<stash>` is\n given, the latest stash is assumed (that is, `stash@{0}`).\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex ab491d5ff6..192b0d9969 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -28,7 +28,10 @@\n #include \"log-tree.h\"\n #include \"diffcore.h\"\n #include \"reflog.h\"\n+#include \"reflog-walk.h\"\n #include \"add-interactive.h\"\n+#include \"oid-array.h\"\n+#include \"commit.h\"\n \n #define INCLUDE_ALL_FILES 2\n \n@@ -56,6 +59,8 @@\n \t   \"          [-u | --include-untracked] [-a | --all] [<message>]\")\n #define BUILTIN_STASH_CREATE_USAGE \\\n \tN_(\"git stash create [<message>]\")\n+#define BUILTIN_STASH_EXPORT_USAGE \\\n+\tN_(\"git stash export (--print | --to-ref <ref>) [<stash>...]\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -71,6 +76,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_CLEAR_USAGE,\n \tBUILTIN_STASH_CREATE_USAGE,\n \tBUILTIN_STASH_STORE_USAGE,\n+\tBUILTIN_STASH_EXPORT_USAGE,\n \tNULL\n };\n \n@@ -124,6 +130,12 @@ static const char * const git_stash_save_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_export_usage[] = {\n+\tBUILTIN_STASH_EXPORT_USAGE,\n+\tNULL\n+};\n+\n+\n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n \n@@ -160,6 +172,33 @@ static void free_stash_info(struct stash_info *info)\n \tstrbuf_release(&info->revision);\n }\n \n+static int check_stash_topology(struct repository *r, struct commit *stash)\n+{\n+\tstruct commit *p1, *p2, *p3 = NULL;\n+\n+\t/* stash must have two or three parents */\n+\tif (!stash->parents || !stash->parents->next ||\n+\t\t\t(stash->parents->next->next && stash->parents->next->next->next))\n+\t\treturn -1;\n+\tp1 = stash->parents->item;\n+\tp2 = stash->parents->next->item;\n+\tif (stash->parents->next->next)\n+\t\tp3 = stash->parents->next->next->item;\n+\tif (repo_parse_commit(r, p1) || repo_parse_commit(r, p2) ||\n+\t\t\t(p3 && repo_parse_commit(r, p3)))\n+\t\treturn -1;\n+\t/* p2 must have a single parent, p3 must have no parents */\n+\tif (!p2->parents || p2->parents->next || (p3 && p3->parents))\n+\t\treturn -1;\n+\tif (repo_parse_commit(r, p2->parents->item))\n+\t\treturn -1;\n+\t/* p2^1 must equal p1 */\n+\tif (!oideq(&p1->object.oid, &p2->parents->item->object.oid))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n static void assert_stash_like(struct stash_info *info, const char *revision)\n {\n \tif (get_oidf(&info->b_commit, \"%s^1\", revision) ||\n@@ -1895,6 +1934,226 @@ static int save_stash(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int write_commit_with_parents(struct repository *r,\n+\t\t\t\t     struct object_id *out,\n+\t\t\t\t     const struct object_id *oid,\n+\t\t\t\t     struct commit_list *parents)\n+{\n+\tsize_t author_len, committer_len;\n+\tstruct commit *this;\n+\tconst char *orig_author, *orig_committer;\n+\tchar *author = NULL, *committer = NULL;\n+\tconst char *buffer;\n+\tunsigned long bufsize;\n+\tconst char *p;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tint ret = 0;\n+\tstruct ident_split id;\n+\n+\tthis = lookup_commit_reference(r, oid);\n+\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\torig_author = find_commit_header(buffer, \"author\", &author_len);\n+\torig_committer = find_commit_header(buffer, \"committer\", &committer_len);\n+\n+\tif (!orig_author || !orig_committer) {\n+\t\tret = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\n+\tif (split_ident_line(&id, orig_author, author_len) < 0 ||\n+\t    split_ident_line(&id, orig_committer, committer_len) < 0) {\n+\t\tret = error(_(\"invalid author or committer for %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\n+\tp = strstr(buffer, \"\\n\\n\");\n+\tstrbuf_addstr(&msg, \"git stash: \");\n+\n+\tif (p)\n+\t\tstrbuf_add(&msg, p + 2, bufsize - (p + 2 - buffer));\n+\tstrbuf_complete_line(&msg);\n+\n+\tauthor = xmemdupz(orig_author, author_len);\n+\tcommitter = xmemdupz(orig_committer, committer_len);\n+\n+\tif (commit_tree_extended(msg.buf, msg.len,\n+\t\t\t\t r->hash_algo->empty_tree, parents,\n+\t\t\t\t out, author, committer,\n+\t\t\t\t NULL, NULL)) {\n+\t\tret = error(_(\"could not write commit\"));\n+\t\tgoto out;\n+\t}\n+out:\n+\tstrbuf_release(&msg);\n+\trepo_unuse_commit_buffer(r, this, buffer);\n+\tfree(author);\n+\tfree(committer);\n+\treturn ret;\n+}\n+\n+struct stash_entry_data {\n+\tstruct repository *r;\n+\tstruct commit_list **items;\n+\tsize_t count;\n+};\n+\n+static int collect_stash_entries(struct object_id *old_oid UNUSED,\n+\t\t\t\t struct object_id *new_oid,\n+\t\t\t\t const char *committer UNUSED,\n+\t\t\t\t timestamp_t timestamp UNUSED,\n+\t\t\t\t int tz UNUSED, const char *msg UNUSED,\n+\t\t\t\t void *cb_data)\n+{\n+\tstruct stash_entry_data *data = cb_data;\n+\tstruct commit *stash;\n+\n+\tdata->count++;\n+\tstash = lookup_commit_reference(data->r, new_oid);\n+\tif (!stash || check_stash_topology(data->r, stash)) {\n+\t\treturn error(_(\"%s does not look like a stash commit\"),\n+\t\t\t     oid_to_hex(new_oid));\n+\t}\n+\tdata->items = commit_list_append(stash, data->items);\n+\treturn 0;\n+}\n+\n+static int do_export_stash(struct repository *r,\n+\t\t\t   const char *ref,\n+\t\t\t   int argc,\n+\t\t\t   const char **argv)\n+{\n+\tstruct object_id base;\n+\tstruct object_context unused;\n+\tstruct commit *prev;\n+\tstruct commit_list *items = NULL, **iter = &items, *cur;\n+\tint res = 0;\n+\tint i;\n+\tstruct strbuf revision = STRBUF_INIT;\n+\tconst char *author, *committer;\n+\n+\t/*\n+\t * This is an arbitrary, fixed date, specifically the one used by git\n+\t * format-patch.  The goal is merely to produce reproducible output.\n+\t */\n+\tprepare_fallback_ident(\"git stash\", \"git@stash\");\n+\tauthor = fmt_ident(\"git stash\", \"git@stash\", WANT_BLANK_IDENT,\n+\t\t\t   \"2001-09-17T00:00:00Z\", 0);\n+\tcommitter = fmt_ident(\"git stash\", \"git@stash\", WANT_BLANK_IDENT,\n+\t\t\t      \"2001-09-17T00:00:00Z\", 0);\n+\n+\t/* First, we create a single empty commit. */\n+\tif (commit_tree_extended(\"\", 0, r->hash_algo->empty_tree, NULL,\n+\t\t\t\t &base, author, committer, NULL, NULL))\n+\t\treturn error(_(\"unable to write base commit\"));\n+\n+\tprev = lookup_commit_reference(r, &base);\n+\n+\tif (argc) {\n+\t\t/*\n+\t\t * Find each specified stash, and load data into the array.\n+\t\t */\n+\t\tfor (i = 0; i < argc; i++) {\n+\t\t\tstruct object_id oid;\n+\t\t\tstruct commit *stash;\n+\n+\t\t\tif (parse_stash_revision(&revision, argv[i], 1) ||\n+\t\t\t    get_oid_with_context(r, revision.buf,\n+\t\t\t\t\t\t GET_OID_QUIETLY | GET_OID_GENTLY,\n+\t\t\t\t\t\t &oid, &unused)) {\n+\t\t\t\tres = error(_(\"unable to find stash entry %s\"), argv[i]);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\n+\t\t\tstash = lookup_commit_reference(r, &oid);\n+\t\t\tif (!stash || check_stash_topology(r, stash)) {\n+\t\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t\t\trevision.buf);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t\titer = commit_list_append(stash, iter);\n+\t\t}\n+\t} else {\n+\t\t/*\n+\t\t * Walk the reflog, finding each stash entry, and load data into the\n+\t\t * array.\n+\t\t */\n+\t\tstruct stash_entry_data cb_data = {\n+\t\t\t.r = r, .items = iter,\n+\t\t};\n+\t\tif (refs_for_each_reflog_ent_reverse(get_main_ref_store(r),\n+\t\t\t\t\t\t     \"refs/stash\",\n+\t\t\t\t\t\t     collect_stash_entries,\n+\t\t\t\t\t\t     &cb_data) && cb_data.count)\n+\t\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Now, create a set of commits identical to the regular stash commits,\n+\t * but where their first parents form a chain to our original empty\n+\t * base commit.\n+\t */\n+\titems = reverse_commit_list(items);\n+\tfor (cur = items; cur; cur = cur->next) {\n+\t\tstruct commit_list *parents = NULL;\n+\t\tstruct commit_list **next = &parents;\n+\t\tstruct object_id out;\n+\t\tstruct commit *stash = cur->item;\n+\n+\t\tnext = commit_list_append(prev, next);\n+\t\tnext = commit_list_append(stash, next);\n+\t\tres = write_commit_with_parents(r, &out, &stash->object.oid, parents);\n+\t\tfree_commit_list(parents);\n+\t\tif (res)\n+\t\t\tgoto out;\n+\t\tprev = lookup_commit_reference(r, &out);\n+\t}\n+\tif (ref)\n+\t\trefs_update_ref(get_main_ref_store(r), NULL, ref,\n+\t\t\t\t&prev->object.oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR);\n+\telse\n+\t\tputs(oid_to_hex(&prev->object.oid));\n+out:\n+\tstrbuf_release(&revision);\n+\tfree_commit_list(items);\n+\n+\treturn res;\n+}\n+\n+enum export_action {\n+\tACTION_NONE,\n+\tACTION_PRINT,\n+\tACTION_TO_REF,\n+};\n+\n+static int export_stash(int argc,\n+\t\t\tconst char **argv,\n+\t\t\tconst char *prefix,\n+\t\t\tstruct repository *repo)\n+{\n+\tconst char *ref = NULL;\n+\tenum export_action action = ACTION_NONE;\n+\tstruct option options[] = {\n+\t\tOPT_CMDMODE(0, \"print\", &action,\n+\t\t\t    N_(\"print the object ID instead of writing it to a ref\"),\n+\t\t\t    ACTION_PRINT),\n+\t\tOPT_STRING(0, \"to-ref\", &ref, \"ref\",\n+\t\t\t    N_(\"save the data to the given ref\")),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_export_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (ref && action == ACTION_NONE)\n+\t\taction = ACTION_TO_REF;\n+\n+\tif (action == ACTION_NONE || (ref && action == ACTION_PRINT))\n+\t\treturn error(_(\"exactly one of --print and --to-ref is required\"));\n+\n+\treturn do_export_stash(repo, ref, argc, argv);\n+}\n+\n int cmd_stash(int argc,\n \t      const char **argv,\n \t      const char *prefix,\n@@ -1915,6 +2174,7 @@ int cmd_stash(int argc,\n \t\tOPT_SUBCOMMAND(\"store\", &fn, store_stash),\n \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n \t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n+\t\tOPT_SUBCOMMAND(\"export\", &fn, export_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\n"},{"id":"520146","messageId":"20250612011221.4158484-5-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250612011221.4158484-1-sandals@crustytoothpaste.net","subject":"[PATCH v8 4/4] builtin/stash: provide a way to import stashes from a ref","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-12T01:12:20Z","receivedAt":"2025-06-12T01:12:24Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Now that we have a way to export stashes to a ref, let's provide a way\nto import them from such a ref back to the stash.  This works much the\nway the export code does, except that we strip off the first parent\nchain commit and then store each resulting commit back to the stash.\n\nWe don't clear the stash first and instead add the specified stashes to\nthe top of the stash.  This is because users may want to export just a\nfew stashes, such as to share a small amount of work in progress with a\ncolleague, and it would be undesirable for the receiving user to lose\nall of their data.  For users who do want to replace the stash, it's\neasy to do to: simply run \"git stash clear\" first.\n\nWe specifically rely on the fact that we'll produce identical stash\ncommits on both sides in our tests.  This provides a cheap,\nstraightforward check for our tests and also makes it easy for users to\nsee if they already have the same data in both repositories.\n\nSigned-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n Documentation/git-stash.adoc |   7 ++\n builtin/stash.c              | 167 +++++++++++++++++++++++++++++++++++\n t/t3903-stash.sh             | 101 +++++++++++++++++++++\n 3 files changed, 275 insertions(+)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 0aef0a5b86..e5e6c9d37f 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -24,6 +24,7 @@ SYNOPSIS\n 'git stash' create [<message>]\n 'git stash' store [(-m | --message) <message>] [-q | --quiet] <commit>\n 'git stash' export (--print | --to-ref <ref>) [<stash>...]\n+'git stash' import <commit>\n \n DESCRIPTION\n -----------\n@@ -161,6 +162,12 @@ export ( --print | --to-ref <ref> ) [<stash>...]::\n \ta chain of commits which can be transferred using the normal fetch and\n \tpush mechanisms, then imported using the `import` subcommand.\n \n+import <commit>::\n+\n+\tImport the specified stashes from the specified commit, which must have been\n+\tcreated by `export`, and add them to the list of stashes.  To replace the\n+\texisting stashes, use `clear` first.\n+\n OPTIONS\n -------\n -a::\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 192b0d9969..0e77fa94ee 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -61,6 +61,8 @@\n \tN_(\"git stash create [<message>]\")\n #define BUILTIN_STASH_EXPORT_USAGE \\\n \tN_(\"git stash export (--print | --to-ref <ref>) [<stash>...]\")\n+#define BUILTIN_STASH_IMPORT_USAGE \\\n+\tN_(\"git stash import <commit>\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -77,6 +79,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_CREATE_USAGE,\n \tBUILTIN_STASH_STORE_USAGE,\n \tBUILTIN_STASH_EXPORT_USAGE,\n+\tBUILTIN_STASH_IMPORT_USAGE,\n \tNULL\n };\n \n@@ -135,6 +138,10 @@ static const char * const git_stash_export_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_import_usage[] = {\n+\tBUILTIN_STASH_IMPORT_USAGE,\n+\tNULL\n+};\n \n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n@@ -144,6 +151,7 @@ static struct strbuf stash_index_path = STRBUF_INIT;\n  * b_commit is set to the base commit\n  * i_commit is set to the commit containing the index tree\n  * u_commit is set to the commit containing the untracked files tree\n+ * c_commit is set to the first parent (chain commit) when importing and is otherwise unset\n  * w_tree is set to the working tree\n  * b_tree is set to the base tree\n  * i_tree is set to the index tree\n@@ -154,6 +162,7 @@ struct stash_info {\n \tstruct object_id b_commit;\n \tstruct object_id i_commit;\n \tstruct object_id u_commit;\n+\tstruct object_id c_commit;\n \tstruct object_id w_tree;\n \tstruct object_id b_tree;\n \tstruct object_id i_tree;\n@@ -1991,6 +2000,163 @@ static int write_commit_with_parents(struct repository *r,\n \treturn ret;\n }\n \n+static int do_import_stash(struct repository *r, const char *rev)\n+{\n+\tstruct object_id chain;\n+\tint res = 0;\n+\tconst char *buffer = NULL;\n+\tunsigned long bufsize;\n+\tstruct commit *this = NULL;\n+\tstruct commit_list *items = NULL, *cur;\n+\tchar *msg = NULL;\n+\n+\tif (repo_get_oid(r, rev, &chain))\n+\t\treturn error(_(\"not a valid revision: %s\"), rev);\n+\n+\tthis = lookup_commit_reference(r, &chain);\n+\tif (!this)\n+\t\treturn error(_(\"not a commit: %s\"), rev);\n+\n+\t/*\n+\t * Walk the commit history, finding each stash entry, and load data into\n+\t * the array.\n+\t */\n+\tfor (;;) {\n+\t\tconst char *author, *committer;\n+\t\tsize_t author_len, committer_len;\n+\t\tconst char *p;\n+\t\tconst char *expected = \"git stash <git@stash> 1000684800 +0000\";\n+\t\tconst char *prefix = \"git stash: \";\n+\t\tstruct commit *stash;\n+\t\tstruct tree *tree = repo_get_commit_tree(r, this);\n+\n+\t\tif (!tree ||\n+\t\t    !oideq(&tree->object.oid, r->hash_algo->empty_tree) ||\n+\t\t    (this->parents &&\n+\t\t     (!this->parents->next || this->parents->next->next))) {\n+\t\t\tres = error(_(\"%s is not a valid exported stash commit\"),\n+\t\t\t\t\toid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\n+\t\tif (!this->parents) {\n+\t\t\t/*\n+\t\t\t * We don't have any parents.  Make sure this is our\n+\t\t\t * root commit.\n+\t\t\t */\n+\t\t\tauthor = find_commit_header(buffer, \"author\", &author_len);\n+\t\t\tcommitter = find_commit_header(buffer, \"committer\", &committer_len);\n+\n+\t\t\tif (!author || !committer) {\n+\t\t\t\terror(_(\"cannot parse commit %s\"), oid_to_hex(&this->object.oid));\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\n+\t\t\tif (author_len != strlen(expected) ||\n+\t\t\t    committer_len != strlen(expected) ||\n+\t\t\t    memcmp(author, expected, author_len) ||\n+\t\t\t    memcmp(committer, expected, committer_len)) {\n+\t\t\t\tres = error(_(\"found root commit %s with invalid data\"), oid_to_hex(&this->object.oid));\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tp = strstr(buffer, \"\\n\\n\");\n+\t\tif (!p) {\n+\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp += 2;\n+\t\tif (((size_t)(bufsize - (p - buffer)) < strlen(prefix)) ||\n+\t\t    memcmp(prefix, p, strlen(prefix))) {\n+\t\t\tres = error(_(\"found stash commit %s without expected prefix\"), oid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstash = this->parents->next->item;\n+\n+\t\tif (repo_parse_commit(r, this->parents->item) ||\n+\t\t    repo_parse_commit(r, stash)) {\n+\t\t\tres = error(_(\"cannot parse parents of commit: %s\"),\n+\t\t\t\t\toid_to_hex(&this->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (check_stash_topology(r, stash)) {\n+\t\t\tres = error(_(\"%s does not look like a stash commit\"),\n+\t\t\t\t\toid_to_hex(&stash->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\t\tbuffer = NULL;\n+\t\titems = commit_list_insert(stash, &items);\n+\t\tthis = this->parents->item;\n+\t}\n+\n+\t/*\n+\t * Now, walk each entry, adding it to the stash as a normal stash\n+\t * commit.\n+\t */\n+\tfor (cur = items; cur; cur = cur->next) {\n+\t\tconst char *p;\n+\t\tstruct object_id *oid;\n+\n+\t\tthis = cur->item;\n+\t\toid = &this->object.oid;\n+\t\tbuffer = repo_get_commit_buffer(r, this, &bufsize);\n+\t\tif (!buffer) {\n+\t\t\tres = error(_(\"cannot read commit buffer for %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp = strstr(buffer, \"\\n\\n\");\n+\t\tif (!p) {\n+\t\t\tres = error(_(\"cannot parse commit %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tp += 2;\n+\t\tmsg = xmemdupz(p, bufsize - (p - buffer));\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\t\tbuffer = NULL;\n+\n+\t\tif (do_store_stash(oid, msg, 1)) {\n+\t\t\tres = error(_(\"cannot save the stash for %s\"), oid_to_hex(oid));\n+\t\t\tgoto out;\n+\t\t}\n+\t\tFREE_AND_NULL(msg);\n+\t}\n+out:\n+\tif (this && buffer)\n+\t\trepo_unuse_commit_buffer(r, this, buffer);\n+\tfree_commit_list(items);\n+\tfree(msg);\n+\n+\treturn res;\n+}\n+\n+static int import_stash(int argc, const char **argv, const char *prefix,\n+\t\t\tstruct repository *repo)\n+{\n+\tstruct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_import_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (argc != 1)\n+\t\tusage_msg_opt(\"a revision is required\", git_stash_import_usage, options);\n+\n+\treturn do_import_stash(repo, argv[0]);\n+}\n+\n struct stash_entry_data {\n \tstruct repository *r;\n \tstruct commit_list **items;\n@@ -2175,6 +2341,7 @@ int cmd_stash(int argc,\n \t\tOPT_SUBCOMMAND(\"create\", &fn, create_stash),\n \t\tOPT_SUBCOMMAND(\"push\", &fn, push_stash_unassumed),\n \t\tOPT_SUBCOMMAND(\"export\", &fn, export_stash),\n+\t\tOPT_SUBCOMMAND(\"import\", &fn, import_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex 74666ff3e4..12d30a9865 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -11,6 +11,13 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-unique-files.sh\n \n+test_expect_success 'setup' '\n+\ttest_oid_cache <<-EOF\n+\texport_base sha1:73c9bab443d1f88ac61aa533d2eeaaa15451239c\n+\texport_base sha256:f210fa6346e3e2ce047bdb570426b17075980c1ac01fec8fc4b75bd3ab4bcfe4\n+\tEOF\n+'\n+\n test_expect_success 'usage on cmd and subcommand invalid option' '\n \ttest_expect_code 129 git stash --invalid-option 2>usage &&\n \tgrep \"or: git stash\" usage &&\n@@ -1412,6 +1419,100 @@ test_expect_success 'stash --keep-index --include-untracked with empty tree' '\n \t)\n '\n \n+test_expect_success 'stash export and import round-trip stashes' '\n+\tgit reset &&\n+\t>untracked &&\n+\t>tracked1 &&\n+\t>tracked2 &&\n+\tgit add tracked* &&\n+\tgit stash -- &&\n+\t>subdir/untracked &&\n+\t>subdir/tracked1 &&\n+\t>subdir/tracked2 &&\n+\tgit add subdir/tracked* &&\n+\tgit stash --include-untracked -- subdir/ &&\n+\tgit tag t-stash0 stash@{0} &&\n+\tgit tag t-stash1 stash@{1} &&\n+\tsimple=$(git stash export --print) &&\n+\tgit stash clear &&\n+\tgit stash import \"$simple\" &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\ttest_cmp_rev stash@{1} t-stash1 &&\n+\tgit stash export --to-ref refs/heads/foo &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" foo: &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" foo^: &&\n+\ttest_cmp_rev t-stash0 foo^2 &&\n+\ttest_cmp_rev t-stash1 foo^^2 &&\n+\tgit log --first-parent --format=\"%s\" refs/heads/foo >log &&\n+\tgrep \"^git stash: \" log >log2 &&\n+\ttest_line_count = 13 log2 &&\n+\tgit stash clear &&\n+\tgit stash import foo &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\ttest_cmp_rev stash@{1} t-stash1\n+'\n+\n+test_expect_success 'stash import appends commits' '\n+\tgit log --format=oneline -g refs/stash >out &&\n+\tcat out out >out2 &&\n+\tgit stash import refs/heads/foo &&\n+\tgit log --format=oneline -g refs/stash >actual &&\n+\ttest_line_count = $(wc -l <out2) actual\n+'\n+\n+test_expect_success 'stash export can accept specified stashes' '\n+\tgit stash clear &&\n+\tgit stash import foo &&\n+\tgit stash export --to-ref refs/heads/bar stash@{1} stash@{0} &&\n+\tgit stash clear &&\n+\tgit stash import refs/heads/bar &&\n+\ttest_cmp_rev stash@{1} t-stash0 &&\n+\ttest_cmp_rev stash@{0} t-stash1 &&\n+\tgit log --format=oneline -g refs/stash >actual &&\n+\ttest_line_count = 2 actual\n+'\n+\n+test_expect_success 'stash export rejects invalid arguments' '\n+\ttest_must_fail git stash export --print --to-ref refs/heads/invalid 2>err &&\n+\tgrep \"exactly one of --print and --to-ref is required\" err &&\n+\ttest_must_fail git stash export 2>err2 &&\n+\tgrep \"exactly one of --print and --to-ref is required\" err2\n+'\n+\n+test_expect_success 'stash can import and export zero stashes' '\n+\tgit stash clear &&\n+\tgit stash export --to-ref refs/heads/baz &&\n+\ttest_cmp_rev \"$(test_oid empty_tree)\" baz: &&\n+\ttest_cmp_rev \"$(test_oid export_base)\" baz &&\n+\ttest_must_fail git rev-parse baz^1 &&\n+\tgit stash import baz &&\n+\ttest_must_fail git rev-parse refs/stash\n+'\n+\n+test_expect_success 'stash rejects invalid attempts to import commits' '\n+\tgit stash import foo &&\n+\ttest_must_fail git stash import HEAD 2>output &&\n+\toid=$(git rev-parse HEAD) &&\n+\tgrep \"$oid is not a valid exported stash commit\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\n+\tgit checkout --orphan orphan &&\n+\tgit commit-tree $(test_oid empty_tree) -p \"$oid\" -p \"$oid^\" -m \"\" >fake-commit &&\n+\tgit update-ref refs/heads/orphan \"$(cat fake-commit)\" &&\n+\toid=$(git rev-parse HEAD) &&\n+\ttest_must_fail git stash import orphan 2>output &&\n+\tgrep \"found stash commit $oid without expected prefix\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0 &&\n+\n+\tgit checkout --orphan orphan2 &&\n+\tgit commit-tree $(test_oid empty_tree) -m \"\" >fake-commit &&\n+\tgit update-ref refs/heads/orphan2 \"$(cat fake-commit)\" &&\n+\toid=$(git rev-parse HEAD) &&\n+\ttest_must_fail git stash import orphan2 2>output &&\n+\tgrep \"found root commit $oid with invalid data\" output &&\n+\ttest_cmp_rev stash@{0} t-stash0\n+'\n+\n test_expect_success 'stash apply should succeed with unmodified file' '\n \techo base >file &&\n \tgit add file &&\n"},{"id":"520147","messageId":"20250612011221.4158484-1-sandals@crustytoothpaste.net","threadId":"63424","inReplyTo":"20250601223225.464076-1-sandals@crustytoothpaste.net","subject":"[PATCH v8 0/4] Importing and exporting stashes to refs","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-12T01:12:16Z","receivedAt":"2025-06-12T01:12:24Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Stashes are currently stored using the reflog in a given repository.\nThis is an interesting and novel way to handle them, but there is no way\nto easily move a set of stashes across machines.  For example, groups of\nstashes cannot be bundled, pushed, or fetched.\n\nLet's solve this problem by allowing users to import and export stashes\nto a chain of commits.  The commits used in a stash export contain two\nparents: one which is the pointer to the next exported stash (or to an\nempty commit with no parents if there are no more) and the second is the\nstash commit that would normally be stored in the reflog.\n\nOriginal thread at message-ID: <20220310173236.4165310-1-sandals@crustytoothpaste.net>\n\nChanges from v7:\n* Rephrase the documentation to be slightly more explicit.\n* Don't have `write_commit_with_parents` free its arguments, instead\n  letting the caller (who allocated them) handle them.\n* Handle invalid combinations of arguments to `export` and add tests for\n  this case.\n\nChanges from v6:\n* Add Phillip's sign-off to the last patch.\n* Use `commit_list` for tracking commits.\n* Use reflog entry walker.\n* Fix some commit messages for improved legibility.\n* Rephrase some error messages for precision.\n* Drop the patch that exposes `read_complete_reflog` since it is no longer necessary.\n\nChanges from v5:\n* Rename `parse_revision`.\n* Remove extra call to `free_stash_info`.\n* Fix parsing of existing commit.\n* Add more validation of imported stash commits.\n* Add more tests for improved validation of imported stash commits.\n* Explicitly cast `items.nr` and make the iteration counter an `ssize_t`\n  to avoid casting problems.\n* Don't require a trailing `\\n\\n` in commit messages.\n* Use `read_complete_reflog` to walk reflogs.\n* Be more defensive when using `lookup_commit_reference`.\n* Apply parts of Phillip's patches for improved robustness.\n* Update commit message to explain additional use cases.\n* Use `OPT_STRING` for `--to-ref`.\n\nChanges from v4:\n* Fix another use of oid_array.\n* Fix various memory leaks.\n* Fix a segfault which appeared after a rebase.\n* Use strstr for commits since we don't need to worry about NUL.\n* Added some additional tests.\n* Verify the ident values we're using to avoid using bad values.\n* Various other code cleanups.\n* Rebase on `master`.\n\nChanges from v3:\n* Fix strbuf handling to avoid leaks and generally be more sensible.\n* Make use of the error return code more often.\n* Use oid_array.\n* Tidy various parts of the code and fix long lines.\n* Simplify tests using git tag.\n* Shorten and tidy tests.\n* Add an additional test covering the base commit OID and importing and\n  exporting empty stashes.\n\nChanges from v2:\n* Fix uninitialized strbuf.\n* Avoid C99-style initializations.\n\nChanges from v1:\n* Change storage format as suggested by Junio.\n* Rename to GIT_OID_GENTLY.\n* Remove unnecessary initializations.\n* Use ALLOC_GROW_BY.\n* Ensure completely reproducible exports.\n* Avoid size_t.\n* Various other code cleanups.\n\nbrian m. carlson (4):\n  object-name: make get_oid quietly return an error\n  builtin/stash: factor out revision parsing into a function\n  builtin/stash: provide a way to export stashes to a ref\n  builtin/stash: provide a way to import stashes from a ref\n\n Documentation/git-stash.adoc |  29 ++-\n builtin/stash.c              | 460 ++++++++++++++++++++++++++++++++++-\n hash.h                       |   1 +\n object-name.c                |   6 +-\n t/t3903-stash.sh             | 101 ++++++++\n 5 files changed, 584 insertions(+), 13 deletions(-)\n\n"},{"id":"520679","messageId":"88915a18-de18-437b-bd74-91fb82ab760e@gmail.com","threadId":"63424","inReplyTo":"20250612011221.4158484-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH v8 0/4] Importing and exporting stashes to refs","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-06-25T08:40:46Z","receivedAt":"2025-06-25T08:40:51Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi brian\n\nOn 12/06/2025 02:12, brian m. carlson wrote:\n> Stashes are currently stored using the reflog in a given repository.\n> This is an interesting and novel way to handle them, but there is no way\n> to easily move a set of stashes across machines.  For example, groups of\n> stashes cannot be bundled, pushed, or fetched.\n> \n> Let's solve this problem by allowing users to import and export stashes\n> to a chain of commits.  The commits used in a stash export contain two\n> parents: one which is the pointer to the next exported stash (or to an\n> empty commit with no parents if there are no more) and the second is the\n> stash commit that would normally be stored in the reflog.\n> \n> Original thread at message-ID: <20220310173236.4165310-1-sandals@crustytoothpaste.net>\n> \n> Changes from v7:\n> * Rephrase the documentation to be slightly more explicit.\n> * Don't have `write_commit_with_parents` free its arguments, instead\n>    letting the caller (who allocated them) handle them.\n> * Handle invalid combinations of arguments to `export` and add tests for\n>    this case.\n\nThe range-diff between v7 and v8 looks good to me. Sorry for the slow \nreply, I was off the list last week.\n\nThanks\n\nPhillip\n\n"},{"id":"520714","messageId":"xmqqcyarwzpt.fsf@gitster.g","threadId":"63424","inReplyTo":"88915a18-de18-437b-bd74-91fb82ab760e@gmail.com","subject":"Re: [PATCH v8 0/4] Importing and exporting stashes to refs","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-06-25T16:30:38Z","receivedAt":"2025-06-25T16:30:41Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>> Changes from v7:\n>> * Rephrase the documentation to be slightly more explicit.\n>> * Don't have `write_commit_with_parents` free its arguments, instead\n>>    letting the caller (who allocated them) handle them.\n>> * Handle invalid combinations of arguments to `export` and add tests for\n>>    this case.\n>\n> The range-diff between v7 and v8 looks good to me. Sorry for the slow\n> reply, I was off the list last week.\n\nThanks.  The topic is in 'next' now.\n\n"}]}