{"thread":{"id":"66013","subject":"[PATCH] stash: add 'rename' subcommand","startedAt":"2026-07-16T08:31:49Z","lastAt":"2026-08-11T12:49:26Z","messageCount":12,"participants":["Emin Özata via GitGitGadget","Patrick Steinhardt","Junio C Hamano","brian m. carlson","erik88","Emin"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"548378","messageId":"pull.2180.git.1784190706028.gitgitgadget@gmail.com","threadId":"66013","inReplyTo":null,"subject":"[PATCH] stash: add 'rename' subcommand","fromName":"Emin Özata via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-16T08:31:45Z","receivedAt":"2026-07-16T08:31:49Z","isPatch":true,"body":"From: =?UTF-8?q?Emin=20=C3=96zata?= <eminozata@proton.me>\n\nThere is no way to change the message of a stash entry after the\nfact.  The only option is dropping the entry and re-storing it by\nhand, which moves it to the top of the stash list and gets fiddly\nfor deeper entries.\n\nAdd 'git stash rename <message> [<stash>]', defaulting to the\nlatest entry like the other subcommands do.  It reads the object id\nand reflog message of the target entry and of the entries above it,\ndrops them all like 'git stash drop' would, and stores them back in\nthe same order, with the new message going to the target.  Position,\ncontents and the reflog chain stay as they were.\n\nThe command checks every entry it is about to rewrite and refuses\nto start if one of them does not look like a stash commit, which\ncan only happen when refs/stash was written to by hand.  Finding\nthat out halfway through the sequence would lose entries.  Should a\nwrite-back fail anyway, the entry's object id is reported so it can\nbe recovered with 'git stash store', and the command only reports\nsuccess when the reflog ended up in the requested state.\n\nThis was proposed before: in 2010, as a \"git reflog update\" command\nthat edited reflog entries in place [1].  When it came up again in\n2013 [2], Junio rejected it on the grounds that reflogs are\nappend-only recovery logs, and that whoever really cares about a\nstash message can pop and re-stash [3].  Michael Haggerty pointed\nout in that thread that refs/stash does not fit the description:\nits reflog is the primary data store for stash entries, and 'git\nstash drop' rewrites it all the time [4].  So this patch stays away\nfrom the reflog machinery entirely and does the suggested\npop-and-re-stash workaround mechanically, without the detour\nthrough the working tree.\n\nThe sequence only works if entry positions hold still while it\nruns, so the command takes index-based selectors (stash@{1}) and\nrejects time-based ones.  It also refreshes the reflog timestamps\nof the rewritten entries, and renaming stash@{n} costs n+1 reflog\ndeletions and ref updates.\n\n[1] https://lore.kernel.org/git/20100620093142.GF24805@occam.hewgill.net/\n[2] https://lore.kernel.org/git/loom.20130104T192132-16@post.gmane.org/\n[3] https://lore.kernel.org/git/7vbod4tynt.fsf@alter.siamese.dyndns.org/\n[4] https://lore.kernel.org/git/50ED2C78.1030300@alum.mit.edu/\n\nSigned-off-by: Emin Özata <eminozata@proton.me>\n---\n    stash: add 'rename' subcommand\n    \n    eo/stash-rename\n    \n    \"git stash rename\" learned to change the message of an existing stash\n    entry without changing its position or its contents.\n    \n    This came up in 2010 and again in 2013, and was rejected back then\n    because the proposed implementation rewrote reflog entries in place.\n    This version doesn't: it does the drop-and-re-store dance that was\n    suggested as the manual workaround, with the machinery stash already\n    uses for drop and store, and touches nothing but refs/stash. Details and\n    links to the old threads are in the commit message.\n    \n    Costs, so nobody has to dig for them: rewritten entries get fresh reflog\n    timestamps (hence index-only selectors), and renaming stash@{n} does n+1\n    reflog deletions, each of them a locked rewrite of the whole reflog. The\n    sequence is not atomic either: a failure halfway is handled by writing\n    the collected entries back best-effort, whatever cannot be written back\n    is reported with its object id so \"git stash store\" can recover it, and\n    a process killed between the drop and store phases loses the collected\n    entries (git fsck still finds them). A single refs_reflog_expire() pass\n    would cut both the I/O and that window down, and closing the window for\n    real needs a new refs API operation; I'd rather do either as a follow-up\n    if the feature is wanted at all.\n    \n    I picked a positional <message> over -m <message> (\"stash store\" style);\n    no strong opinion, happy to switch.\n    \n    t3903 passes with GIT_TEST_DEFAULT_REF_FORMAT=files and reftable.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2180%2Fozemin%2Fstash-rename-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2180/ozemin/stash-rename-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2180\n\n Documentation/git-stash.adoc           |  11 +-\n builtin/stash.c                        | 197 +++++++++++++++++++++++--\n contrib/completion/git-completion.bash |   4 +-\n t/t3903-stash.sh                       |  79 ++++++++++\n 4 files changed, 276 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 50bb89f483..03f2e03096 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -25,6 +25,7 @@ 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+git stash rename [-q | --quiet] <message> [<stash>]\n \n DESCRIPTION\n -----------\n@@ -163,6 +164,12 @@ with no conflicts.\n \tcreated by `export`, and add them to the list of stashes.  To replace the\n \texisting stashes, use `clear` first.\n \n+`rename [-q | --quiet] <message> [<stash>]`::\n+\tChange the message of a single stash entry.  The entry keeps its\n+\tposition and its contents.  _<stash>_ must name an entry by\n+\tindex (e.g. `stash@{1}`); renaming refreshes the reflog\n+\ttimestamps of the entry and of the entries above it.\n+\n OPTIONS\n -------\n `-a`::\n@@ -258,7 +265,7 @@ literally (including newlines and quotes).\n `-q`::\n `--quiet`::\n \tThis option is only valid for `apply`, `drop`, `pop`, `push`,\n-\t`save`, `store` commands.\n+\t`rename`, `save`, `store` commands.\n +\n Quiet, suppress feedback messages.\n \n@@ -292,7 +299,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`, and `export` commands.\n+\t`show`, `export`, and `rename` 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 c4809f299a..94e66d6074 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -63,6 +63,8 @@\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_RENAME_USAGE \\\n+\tN_(\"git stash rename [-q | --quiet] <message> [<stash>]\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -80,6 +82,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_STORE_USAGE,\n \tBUILTIN_STASH_EXPORT_USAGE,\n \tBUILTIN_STASH_IMPORT_USAGE,\n+\tBUILTIN_STASH_RENAME_USAGE,\n \tNULL\n };\n \n@@ -143,6 +146,11 @@ static const char * const git_stash_import_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_rename_usage[] = {\n+\tBUILTIN_STASH_RENAME_USAGE,\n+\tNULL\n+};\n+\n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n \n@@ -820,18 +828,12 @@ static int reflog_is_empty(const char *refname)\n \t\t\t\t\t refname, reject_reflog_ent, NULL);\n }\n \n-static int do_drop_stash(struct stash_info *info, int quiet)\n+static int drop_reflog_entry(const char *revision)\n {\n-\tif (!reflog_delete(info->revision.buf,\n-\t\t\t   EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,\n-\t\t\t   0)) {\n-\t\tif (!quiet)\n-\t\t\tprintf_ln(_(\"Dropped %s (%s)\"), info->revision.buf,\n-\t\t\t\t  oid_to_hex(&info->w_commit));\n-\t} else {\n-\t\treturn error(_(\"%s: Could not drop stash entry\"),\n-\t\t\t     info->revision.buf);\n-\t}\n+\tif (reflog_delete(revision,\n+\t\t\t  EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,\n+\t\t\t  0))\n+\t\treturn error(_(\"%s: Could not drop stash entry\"), revision);\n \n \tif (reflog_is_empty(ref_stash))\n \t\tdo_clear_stash();\n@@ -839,6 +841,18 @@ static int do_drop_stash(struct stash_info *info, int quiet)\n \treturn 0;\n }\n \n+static int do_drop_stash(struct stash_info *info, int quiet)\n+{\n+\tif (drop_reflog_entry(info->revision.buf))\n+\t\treturn -1;\n+\n+\tif (!quiet)\n+\t\tprintf_ln(_(\"Dropped %s (%s)\"), info->revision.buf,\n+\t\t\t  oid_to_hex(&info->w_commit));\n+\n+\treturn 0;\n+}\n+\n static int get_stash_info_assert(struct stash_info *info, int argc,\n \t\t\t\t const char **argv)\n {\n@@ -1190,6 +1204,166 @@ out:\n \treturn ret;\n }\n \n+struct rename_entry {\n+\tstruct object_id oid;\n+\tchar *msg;\n+};\n+\n+struct rename_data {\n+\tstruct rename_entry *entries;\n+\tsize_t nr, alloc;\n+\tsize_t want;\n+};\n+\n+static int collect_rename_entries(const char *refname UNUSED,\n+\t\t\t\t  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,\n+\t\t\t\t  void *cb_data)\n+{\n+\tstruct rename_data *data = cb_data;\n+\tconst char *eol = strchrnul(msg, '\\n');\n+\n+\tALLOC_GROW(data->entries, data->nr + 1, data->alloc);\n+\toidcpy(&data->entries[data->nr].oid, new_oid);\n+\tdata->entries[data->nr].msg = xstrndup(msg, eol - msg);\n+\tdata->nr++;\n+\n+\treturn data->nr >= data->want;\n+}\n+\n+static int parse_stash_index(const char *revision, size_t *idx)\n+{\n+\tconst char *num = strstr(revision, \"@{\");\n+\tchar *end;\n+\n+\tif (!num || !isdigit(num[2]))\n+\t\treturn -1;\n+\t*idx = strtoumax(num + 2, &end, 10);\n+\tif (*end != '}' || end[1])\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+static int store_rename_entry(struct rename_entry *entry, const char *msg)\n+{\n+\tif (!do_store_stash(&entry->oid, msg, 1))\n+\t\treturn 0;\n+\twarning(_(\"could not restore stash entry %s; \"\n+\t\t  \"recover it with 'git stash store %s'\"),\n+\t\toid_to_hex(&entry->oid), oid_to_hex(&entry->oid));\n+\treturn -1;\n+}\n+\n+static int do_rename_stash(struct stash_info *info, size_t idx,\n+\t\t\t   const char *msg, int quiet)\n+{\n+\tstruct rename_data data = { .want = idx + 1 };\n+\tsize_t i, missing = 0;\n+\tint ret = -1;\n+\n+\trefs_for_each_reflog_ent_reverse(get_main_ref_store(the_repository),\n+\t\t\t\t\t ref_stash, collect_rename_entries,\n+\t\t\t\t\t &data);\n+\tif (data.nr <= idx) {\n+\t\terror(_(\"%s does not exist\"), info->revision.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (!oideq(&info->w_commit, &data.entries[idx].oid)) {\n+\t\terror(_(\"%s changed concurrently; try again\"),\n+\t\t      info->revision.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/* refuse up front; do_store_stash() would die halfway through */\n+\tfor (i = 0; i < data.nr; i++) {\n+\t\tstruct commit *stash = lookup_commit_reference(the_repository,\n+\t\t\t\t\t\t\t       &data.entries[i].oid);\n+\n+\t\tif (!stash || check_stash_topology(the_repository, stash)) {\n+\t\t\terror(_(\"%s does not look like a stash commit\"),\n+\t\t\t      oid_to_hex(&data.entries[i].oid));\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\twhile (missing <= idx) {\n+\t\tif (drop_reflog_entry(\"stash@{0}\"))\n+\t\t\tgoto restore;\n+\t\tmissing++;\n+\t}\n+\n+\tret = 0;\n+\twhile (missing) {\n+\t\ti = missing - 1;\n+\t\tif (store_rename_entry(&data.entries[i],\n+\t\t\t\t       i == idx ? msg : data.entries[i].msg))\n+\t\t\tret = -1;\n+\t\tmissing--;\n+\t}\n+\n+\tif (!ret && !quiet)\n+\t\tprintf_ln(_(\"Renamed %s (%s)\"), info->revision.buf,\n+\t\t\t  oid_to_hex(&data.entries[idx].oid));\n+\tgoto cleanup;\n+\n+restore:\n+\t/* dropping failed midway; put the dropped entries back */\n+\twhile (missing) {\n+\t\tstore_rename_entry(&data.entries[missing - 1],\n+\t\t\t\t   data.entries[missing - 1].msg);\n+\t\tmissing--;\n+\t}\n+cleanup:\n+\tfor (i = 0; i < data.nr; i++)\n+\t\tfree(data.entries[i].msg);\n+\tfree(data.entries);\n+\treturn ret;\n+}\n+\n+static int rename_stash(int argc, const char **argv, const char *prefix,\n+\t\t\tstruct repository *repo UNUSED)\n+{\n+\tint ret = -1;\n+\tint quiet = 0;\n+\tsize_t idx;\n+\tstruct stash_info info = STASH_INFO_INIT;\n+\tstruct option options[] = {\n+\t\tOPT__QUIET(&quiet, N_(\"be quiet, only report errors\")),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_rename_usage, 0);\n+\n+\tif (!argc)\n+\t\tusage_with_options(git_stash_rename_usage, options);\n+\n+\tif (!argv[0][strspn(argv[0], \" \\t\\r\\n\")]) {\n+\t\tret = error(_(\"stash message cannot be empty\"));\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (get_stash_info_assert(&info, argc - 1, argv + 1))\n+\t\tgoto cleanup;\n+\n+\t/* positions must stay stable across the drop-and-store sequence */\n+\tif (parse_stash_index(info.revision.buf, &idx)) {\n+\t\terror(_(\"cannot rename '%s': name the entry by index, \"\n+\t\t\t\"like 'stash@{1}'\"), info.revision.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tret = do_rename_stash(&info, idx, argv[0], quiet);\n+cleanup:\n+\tfree_stash_info(&info);\n+\treturn ret;\n+}\n+\n static void add_pathspecs(struct strvec *args,\n \t\t\t  const struct pathspec *ps) {\n \tint i;\n@@ -2472,6 +2646,7 @@ int cmd_stash(int argc,\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(\"rename\", &fn, rename_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\ndiff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash\nindex e875787710..08c53cea49 100644\n--- a/contrib/completion/git-completion.bash\n+++ b/contrib/completion/git-completion.bash\n@@ -3465,7 +3465,7 @@ _git_sparse_checkout ()\n \n _git_stash ()\n {\n-\tlocal subcommands='push list show apply clear drop pop create branch import export'\n+\tlocal subcommands='push list show apply clear drop pop create branch import export rename'\n \tlocal subcommand=\"$(__git_find_on_cmdline \"$subcommands save\")\"\n \n \tif [ -z \"$subcommand\" ]; then\n@@ -3508,7 +3508,7 @@ _git_stash ()\n \timport,*)\n \t\t__git_complete_refs\n \t\t;;\n-\tshow,*|apply,*|drop,*|pop,*|export,*)\n+\tshow,*|apply,*|drop,*|pop,*|export,*|rename,*)\n \t\t__gitcomp_nl \"$(__git stash list \\\n \t\t\t\t| sed -n -e 's/:.*//p')\"\n \t\t;;\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex ecc35aae82..f175302c1a 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -1831,4 +1831,83 @@ test_expect_success 'stash show --include-untracked includes untracked files' '\n \ttest_grep \"untracked\" actual\n '\n \n+test_expect_success 'rename a stash entry' '\n+\tgit stash clear &&\n+\t>file-to-rename &&\n+\tgit add file-to-rename &&\n+\tgit stash push -m \"original message\" &&\n+\tgit stash rename \"new message\" stash@{0} >out &&\n+\ttest_grep \"Renamed stash@{0}\" out &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{0}: new message\" list &&\n+\ttest_grep ! \"original message\" list\n+'\n+\n+test_expect_success 'rename defaults to the latest stash entry' '\n+\tgit stash rename \"default target\" >out &&\n+\ttest_grep \"Renamed refs/stash@{0}\" out &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{0}: default target\" list\n+'\n+\n+test_expect_success 'rename a deeper stash entry keeps positions and states' '\n+\tgit stash clear &&\n+\tfor i in 1 2 3\n+\tdo\n+\t\t>file$i &&\n+\t\tgit add file$i &&\n+\t\tgit stash push -m \"message $i\" || return 1\n+\tdone &&\n+\tgit rev-parse stash@{0} stash@{1} stash@{2} >expect &&\n+\tgit stash rename \"renamed middle\" stash@{1} &&\n+\tgit rev-parse stash@{0} stash@{1} stash@{2} >actual &&\n+\ttest_cmp expect actual &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{0}: On.*message 3\" list &&\n+\ttest_grep \"stash@{1}: renamed middle\" list &&\n+\ttest_grep \"stash@{2}: On.*message 1\" list\n+'\n+\n+test_expect_success 'rename the deepest stash entry' '\n+\tgit rev-parse stash@{0} stash@{1} stash@{2} >expect &&\n+\tgit stash rename \"renamed deepest\" stash@{2} &&\n+\tgit rev-parse stash@{0} stash@{1} stash@{2} >actual &&\n+\ttest_cmp expect actual &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{2}: renamed deepest\" list\n+'\n+\n+test_expect_success 'rename accepts a bare index and honors --quiet' '\n+\tgit stash rename -q \"quietly renamed\" 1 >out &&\n+\ttest_must_be_empty out &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{1}: quietly renamed\" list\n+'\n+\n+test_expect_success 'rename rejects bad arguments' '\n+\ttest_must_fail git stash rename \"no such entry\" stash@{99} &&\n+\ttest_must_fail git stash rename \"\" &&\n+\ttest_must_fail git stash rename \"   \" &&\n+\ttest_must_fail git stash rename \"not a stash\" HEAD &&\n+\ttest_must_fail git stash rename \"not an index\" \"stash@{now}\" &&\n+\ttest_expect_code 129 git stash rename &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{1}: quietly renamed\" list\n+'\n+\n+test_expect_success 'rename refuses to rewrite a non-stash reflog entry' '\n+\tgit stash clear &&\n+\t>real-a &&\n+\tgit add real-a &&\n+\tgit stash push -m \"real A\" &&\n+\tgit update-ref -m junk --create-reflog refs/stash HEAD &&\n+\t>real-b &&\n+\tgit add real-b &&\n+\tgit stash push -m \"real B\" &&\n+\tgit stash list >expect &&\n+\ttest_must_fail git stash rename \"renamed A\" stash@{2} &&\n+\tgit stash list >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: 55526a18268bbc1ddaf8a6b7850c33d984eac9e9\n-- \ngitgitgadget\n"},{"id":"548393","messageId":"alitkCsplW_DIaRw@pks.im","threadId":"66013","inReplyTo":"pull.2180.git.1784190706028.gitgitgadget@gmail.com","subject":"Re: [PATCH] stash: add 'rename' subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-16T10:08:16Z","receivedAt":"2026-07-16T10:08:23Z","isPatch":true,"body":"On Thu, Jul 16, 2026 at 08:31:45AM +0000, Emin Özata via GitGitGadget wrote:\n> From: =?UTF-8?q?Emin=20=C3=96zata?= <eminozata@proton.me>\n> \n> There is no way to change the message of a stash entry after the\n> fact.  The only option is dropping the entry and re-storing it by\n> hand, which moves it to the top of the stash list and gets fiddly\n> for deeper entries.\n> \n> Add 'git stash rename <message> [<stash>]', defaulting to the\n> latest entry like the other subcommands do.  It reads the object id\n> and reflog message of the target entry and of the entries above it,\n> drops them all like 'git stash drop' would, and stores them back in\n> the same order, with the new message going to the target.  Position,\n> contents and the reflog chain stay as they were.\n> \n> The command checks every entry it is about to rewrite and refuses\n> to start if one of them does not look like a stash commit, which\n> can only happen when refs/stash was written to by hand.  Finding\n> that out halfway through the sequence would lose entries.  Should a\n> write-back fail anyway, the entry's object id is reported so it can\n> be recovered with 'git stash store', and the command only reports\n> success when the reflog ended up in the requested state.\n> \n> This was proposed before: in 2010, as a \"git reflog update\" command\n> that edited reflog entries in place [1].  When it came up again in\n> 2013 [2], Junio rejected it on the grounds that reflogs are\n> append-only recovery logs, and that whoever really cares about a\n> stash message can pop and re-stash [3].  Michael Haggerty pointed\n> out in that thread that refs/stash does not fit the description:\n> its reflog is the primary data store for stash entries, and 'git\n> stash drop' rewrites it all the time [4].  So this patch stays away\n> from the reflog machinery entirely and does the suggested\n> pop-and-re-stash workaround mechanically, without the detour\n> through the working tree.\n\nHm. It's good to refer to to previous discussions. But I think it would\nmake sense to also document why explicitly _you_ want to have this\nfunctionality. Like, what use case does it enable that you currently\ncannot have right now? How is this different to what was proposed back\nthen that should make us reconsider whether or not to include it now?\n\n> diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\n> index 50bb89f483..03f2e03096 100644\n> --- a/Documentation/git-stash.adoc\n> +++ b/Documentation/git-stash.adoc\n> @@ -163,6 +164,12 @@ with no conflicts.\n>  \tcreated by `export`, and add them to the list of stashes.  To replace the\n>  \texisting stashes, use `clear` first.\n>  \n> +`rename [-q | --quiet] <message> [<stash>]`::\n> +\tChange the message of a single stash entry.  The entry keeps its\n> +\tposition and its contents.  _<stash>_ must name an entry by\n> +\tindex (e.g. `stash@{1}`); renaming refreshes the reflog\n> +\ttimestamps of the entry and of the entries above it.\n\nI think \"rename\" is a bit of a misleading name, doubly so with the\nrecently introduced `git refs rename` feature that renames a reference.\nI'd suggest \"reword\" instead.\n\n> diff --git a/builtin/stash.c b/builtin/stash.c\n> index c4809f299a..94e66d6074 100644\n> --- a/builtin/stash.c\n> +++ b/builtin/stash.c\n> @@ -1190,6 +1204,166 @@ out:\n[snip]\n> +static int do_rename_stash(struct stash_info *info, size_t idx,\n> +\t\t\t   const char *msg, int quiet)\n> +{\n> +\tstruct rename_data data = { .want = idx + 1 };\n> +\tsize_t i, missing = 0;\n> +\tint ret = -1;\n> +\n> +\trefs_for_each_reflog_ent_reverse(get_main_ref_store(the_repository),\n> +\t\t\t\t\t ref_stash, collect_rename_entries,\n> +\t\t\t\t\t &data);\n> +\tif (data.nr <= idx) {\n> +\t\terror(_(\"%s does not exist\"), info->revision.buf);\n> +\t\tgoto cleanup;\n> +\t}\n> +\n> +\tif (!oideq(&info->w_commit, &data.entries[idx].oid)) {\n> +\t\terror(_(\"%s changed concurrently; try again\"),\n> +\t\t      info->revision.buf);\n> +\t\tgoto cleanup;\n> +\t}\n> +\n> +\t/* refuse up front; do_store_stash() would die halfway through */\n> +\tfor (i = 0; i < data.nr; i++) {\n> +\t\tstruct commit *stash = lookup_commit_reference(the_repository,\n> +\t\t\t\t\t\t\t       &data.entries[i].oid);\n> +\n> +\t\tif (!stash || check_stash_topology(the_repository, stash)) {\n> +\t\t\terror(_(\"%s does not look like a stash commit\"),\n> +\t\t\t      oid_to_hex(&data.entries[i].oid));\n> +\t\t\tgoto cleanup;\n> +\t\t}\n> +\t}\n\nThis loop here has potentially-quadratic runtime. Not so much with the\n\"files\" backend, where we'll simply append the data to the log. But with\nthe reftable backend we'll basically end up writing each reflog entry\ninto a new table, and we'll end up compacting the tables many times\nover.\n\n> +\n> +\twhile (missing <= idx) {\n> +\t\tif (drop_reflog_entry(\"stash@{0}\"))\n> +\t\t\tgoto restore;\n> +\t\tmissing++;\n> +\t}\n\nSame here, this will not perform well if you have a huge reflog.\n\nWe really should do all of this atomically, where we ideally delete the\nold reflog and create the new reflog in a single transaction.\n\nThanks!\n\nPatrick\n"},{"id":"548441","messageId":"xmqqh5lyhlp6.fsf@gitster.g","threadId":"66013","inReplyTo":"alitkCsplW_DIaRw@pks.im","subject":"Re: [PATCH] stash: add 'rename' subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-16T17:46:13Z","receivedAt":"2026-07-16T17:46:16Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Hm. It's good to refer to to previous discussions. But I think it would\n> make sense to also document why explicitly _you_ want to have this\n> functionality. Like, what use case does it enable that you currently\n> cannot have right now? How is this different to what was proposed back\n> then that should make us reconsider whether or not to include it now?\n\nWe have one previous discussion thread on this, and there is no\nneed to refer to every single message in it.  Just pointing to\nthe discussion starter should be enough to let us read what was\nsaid.  It was also very nice of Emin to summarize the viewpoints\nexpressed there, so that readers can follow along without having to\ndig up external resources.\n\nAsking for the actual use case is indeed very useful, and I highly\nappreciate the nudge.\n\n\"I cannot reword the message shown in 'git stash list' without this\nchange\" is not a use case.  It would be far more useful to know\nwhat real-world task you are blocked from accomplishing just\nbecause you cannot reword a misspelled entry in 'git stash list'.\n\nPersonally, I cannot think of any such roadblock.  If I find that\na stash entry is worth tweaking right now, I would probably\ncreate a new branch for the task, unstash it there, and either\nmake further changes or record the exact state with an updated\nmessage to help my future self.  After all, having a branch I\ncan start working on immediately is far more useful than having\nthe same stash entry with its spelling corrected.\n\nThanks.\n"},{"id":"548457","messageId":"allISNh-b6Sc6y5-@fruit.crustytoothpaste.net","threadId":"66013","inReplyTo":"xmqqh5lyhlp6.fsf@gitster.g","subject":"Re: [PATCH] stash: add 'rename' subcommand","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-16T21:08:24Z","receivedAt":"2026-07-16T21:08:32Z","isPatch":true,"body":"On 2026-07-16 at 17:46:13, Junio C Hamano wrote:\n> Asking for the actual use case is indeed very useful, and I highly\n> appreciate the nudge.\n> \n> \"I cannot reword the message shown in 'git stash list' without this\n> change\" is not a use case.  It would be far more useful to know\n> what real-world task you are blocked from accomplishing just\n> because you cannot reword a misspelled entry in 'git stash list'.\n> \n> Personally, I cannot think of any such roadblock.  If I find that\n> a stash entry is worth tweaking right now, I would probably\n> create a new branch for the task, unstash it there, and either\n> make further changes or record the exact state with an updated\n> message to help my future self.  After all, having a branch I\n> can start working on immediately is far more useful than having\n> the same stash entry with its spelling corrected.\n\nI have a large number of stashes in my git.git repository (211 at\npresent) and it may be that I, in a mad rush to solve some problem or\nanother, went with the default message.  I do this kind of thing with\nrepositories at work all the time.  But it's inconvenient to have to\nsearch through `git stash list -p` to see which change it was and which\nstash I need to pop to resume my work, so I can see the utility of this\napproach.\n\nIn my case, the stash is likely on an existing branch (such as my\nsha256-interop branch), so I don't want to create another branch right\nnow, but I do want to distinguish a couple of different stashes.\n\nOf course, I don't know what Emin's use case is, but I think it would in\nfact be useful to have and I can see myself using it. If my use case is\nvaluable to others, anyone is free to summarize it in the commit message\nas a justification.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"548467","messageId":"xmqqpl0mbetb.fsf@gitster.g","threadId":"66013","inReplyTo":"allISNh-b6Sc6y5-@fruit.crustytoothpaste.net","subject":"Re: [PATCH] stash: add 'rename' subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-17T01:11:28Z","receivedAt":"2026-07-17T01:11:31Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> I have a large number of stashes in my git.git repository (211 at\n> present) and it may be that I, in a mad rush to solve some problem or\n> another, went with the default message.  I do this kind of thing with\n> repositories at work all the time.  But it's inconvenient to have to\n> search through `git stash list -p` to see which change it was and which\n> stash I need to pop to resume my work, so I can see the utility of this\n> approach.\n\nHmph, are you saying that you are going to wade through 211\nstash entries one by one and reword them?\n\n> In my case, the stash is likely on an existing branch (such as my\n> sha256-interop branch), so I don't want to create another branch right\n> now, but I do want to distinguish a couple of different stashes.\n\nThis is not necessarily related to what Emin's patch does or tries\nto do, but it suggests to me that it would be ultra-nice if we could\nhave a stash that is associated with each individual branch.  It is\nas if we have been living in a world with only detached HEADs and\nyou suddenly invented named branches.  ;-)\n\nWell, the original design motivation behind \"git stash\" was the\n\"boss is here\" emergency mode, and the expectation was that such\nan emergency would not nest to leave anyone with more than a\nhandful of entries.  Thus, a single stash shared repository-wide\nacross your branches was not a huge problem.  But our workflows\nhave evolved, so our tool support may have to evolve alongside\nthem.\n\nThanks for the food for thought.\n"},{"id":"548996","messageId":"amXJcVadL8btF_gh@vader","threadId":"66013","inReplyTo":"xmqqpl0mbetb.fsf@gitster.g","subject":"Re: [PATCH] stash: add 'rename' subcommand","fromName":"erik88","fromEmail":"erik88@gmail.com","sentAt":"2026-07-26T09:00:42Z","receivedAt":"2026-07-26T09:00:46Z","isPatch":true,"body":"On 26/07/16 06:11PM, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n>\n> > I have a large number of stashes in my git.git repository (211 at\n> > present) and it may be that I, in a mad rush to solve some problem or\n> > another, went with the default message.  I do this kind of thing with\n> > repositories at work all the time.  But it's inconvenient to have to\n> > search through `git stash list -p` to see which change it was and which\n> > stash I need to pop to resume my work, so I can see the utility of this\n> > approach.\n\nSame. I always accumulate 100+ stashes. I never bother with a message\nwhen I stash. 90-95% of the stashes are useless cruft but there's always\nan experiment, a script file of some sort, which turns out to be useful.\n\n> Hmph, are you saying that you are going to wade through 211\n> stash entries one by one and reword them?\n\nNot me but my AI agent ;-)\n"},{"id":"549065","messageId":"xmqqtsplouii.fsf@gitster.g","threadId":"66013","inReplyTo":"amXJcVadL8btF_gh@vader","subject":"Re: [PATCH] stash: add 'rename' subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-27T05:45:41Z","receivedAt":"2026-07-27T05:45:45Z","isPatch":true,"body":"erik88 <erik88@gmail.com> writes:\n\n> On 26/07/16 06:11PM, Junio C Hamano wrote:\n>> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n>>\n>> > I have a large number of stashes in my git.git repository (211 at\n>> > present) and it may be that I, in a mad rush to solve some problem or\n>> > another, went with the default message.  I do this kind of thing with\n>> > repositories at work all the time.  But it's inconvenient to have to\n>> > search through `git stash list -p` to see which change it was and which\n>> > stash I need to pop to resume my work, so I can see the utility of this\n>> > approach.\n>\n> Same. I always accumulate 100+ stashes. I never bother with a message\n> when I stash. 90-95% of the stashes are useless cruft but there's always\n> an experiment, a script file of some sort, which turns out to be useful.\n\nOK, so editing the log message for each stash entry is already a use\ncase.  In that case, I no longer object to the feature in principle.\nAt least, I cannot think of a way for it to encourage any bad\nworkflows.\n\nThe next step is for someone to review the implementation.  I have\nnot read the implementation at all, and it will be quite a while\nuntil I get to it (if I ever do).\n\n>> Hmph, are you saying that you are going to wade through 211\n>> stash entries one by one and reword them?\n>\n> Not me but my AI agent ;-)\n\nI have no sympathy for you, then.  Your AI agent should be able to\nunstash and restash 211 entries one by one and reword them just fine\n;-).\n"},{"id":"549074","messageId":"eMj51mk8rqcQ7OawqFMB15gzO_MX5ScmstepOBExhu0fqazg_6uNi-2Uw1ONg-JULoEu3MzY3kq2fUkQVK_NR36nF1bMVUeg6UHEnJ0BuPE=@proton.me","threadId":"66013","inReplyTo":"xmqqtsplouii.fsf@gitster.g","subject":"Re: [PATCH] stash: add 'rename' subcommand","fromName":"Emin","fromEmail":"eminozata@proton.me","sentAt":"2026-07-27T09:18:25Z","receivedAt":"2026-07-27T09:18:46Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> OK, so editing the log message for each stash entry is already a use\n> case.  In that case, I no longer object to the feature in principle.\n\nThanks, and thanks to brian and erik for describing the use case better\nthan I did in the original message.  Mine is the same: entries pile up\nwith the default \"WIP on <branch>\" message, and the ones worth keeping\nare the hardest to find again later.\n\nI will send a v2 shortly that addresses Patrick's review.  It renames\nthe subcommand to 'reword' and rewrites the reflog in a single\ntransaction.\n\nThanks.\n\n"},{"id":"549082","messageId":"pull.2180.v2.git.1785149687514.gitgitgadget@gmail.com","threadId":"66013","inReplyTo":"pull.2180.git.1784190706028.gitgitgadget@gmail.com","subject":"[PATCH v2] stash: add 'reword' subcommand","fromName":"Emin Özata via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-27T10:54:46Z","receivedAt":"2026-07-27T10:54:50Z","isPatch":true,"body":"From: =?UTF-8?q?Emin=20=C3=96zata?= <eminozata@proton.me>\n\nStash entries accumulate with the default \"WIP on <branch>\" message\nwhen they are created in a hurry, and there is no way to relabel the\nones worth keeping afterwards.  The only option is dropping an entry\nand re-storing it by hand, which moves it to the top of the stash\nlist and gets fiddly for deeper entries.\n\nAdd 'git stash reword <message> [<stash>]', which defaults to the\nlatest entry like the other subcommands do.  The name follows the\nverb git already uses for changing only a message, as in the reword\ncommand of 'git rebase -i' and in 'git history reword'.  It reads\nthe whole reflog and writes it back in a single transaction, with\nthe new message on the target and every other entry left as it was.\nPosition, contents, timestamps and the reflog chain all stay put.\n\nBefore touching anything, the command inspects the target entry and\nthe ones above it, and refuses to start if any of them does not look\nlike a stash commit; that can only happen when refs/stash was\nwritten to by hand.  The rewrite clears the reflog before writing\nthe new one, so if the transaction fails afterwards, the command\nreports the object id of each collected entry and re-stores it\nbest-effort.  Whatever it fails to re-store can be recovered with\n'git stash store'.\n\nThis was proposed before: in 2010, as a \"git reflog update\" command\nthat edited reflog entries in place [1].  When it came up again in\n2013 [2], Junio rejected it on the grounds that reflogs are\nappend-only recovery logs, and that whoever really cares about a\nstash message can pop and re-stash [3].  Michael Haggerty pointed\nout in that thread that refs/stash does not fit the description:\nits reflog is the primary data store for stash entries, and 'git\nstash drop' rewrites it all the time [4].  So this patch rewrites\nrefs/stash directly, the way 'git stash drop' already does, through\nref_transaction_update_reflog(), which 'git remote rename' and 'git\nrefs migrate' already use to rewrite existing reflogs.\n\nName the target by index (stash@{1}); the command rejects\ntime-based selectors, because it needs the entry's position in the\nreflog.  Writing the reflog back at once keeps the cost linear in\nits length whatever that position is, and spares the reftable\nbackend from emitting and compacting a table per entry, which is\nwhat dropping and re-storing the entries one by one would cost.\n\n[1] https://lore.kernel.org/git/20100620093142.GF24805@occam.hewgill.net/\n[2] https://lore.kernel.org/git/loom.20130104T192132-16@post.gmane.org/\n[3] https://lore.kernel.org/git/7vbod4tynt.fsf@alter.siamese.dyndns.org/\n[4] https://lore.kernel.org/git/50ED2C78.1030300@alum.mit.edu/\n\nSigned-off-by: Emin Özata <eminozata@proton.me>\n---\n    stash: add 'reword' subcommand\n    \n    eo/stash-reword\n    \n    \"git stash reword\" learned to change the message of an existing stash\n    entry without changing its position, its contents or its reflog\n    timestamp.\n    \n    This came up in 2010 and again in 2013, and was rejected back then on\n    the grounds that reflogs are append-only recovery logs. refs/stash is\n    the exception Michael Haggerty pointed out in that thread: its reflog is\n    the primary data store for stash entries, and \"git stash drop\" rewrites\n    it already. So this rewrites the refs/stash reflog through\n    ref_transaction_update_reflog(), which \"git remote rename\" and \"git refs\n    migrate\" already use to rewrite existing reflogs, and touches nothing\n    else. Details and links to the old threads are in the commit message.\n    \n    Costs, so nobody has to dig for them: the whole reflog is read and\n    written back once whatever the target's depth, so the cost is linear in\n    its length. The rewrite is not crash-safe. The reflog is cleared before\n    the transaction writes it back, so a process killed in that window\n    leaves refs/stash without the reflog that \"git stash list\" reads. If the\n    transaction itself fails, the collected entries are re-stored\n    best-effort, and whatever cannot be written back is reported with its\n    object id so \"git stash store\" can recover it. The stash commits are\n    never deleted either way, so \"git fsck\" still finds them. Closing that\n    window for real needs a refs API operation that replaces a reflog in one\n    step, which I would rather do as a follow-up.\n    \n    I picked a positional message argument over an -m option (\"stash store\"\n    style); no strong opinion, happy to switch.\n    \n    Changes since v1, all from Patrick's review: the subcommand is 'reword'\n    rather than 'rename', the reflog is rewritten in a single transaction\n    instead of one drop-and-store per entry, and the commit message opens\n    with the use case rather than with the observation that stash messages\n    cannot be changed.\n    \n    t3903 passes with GIT_TEST_DEFAULT_REF_FORMAT=files and reftable.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2180%2Fozemin%2Fstash-rename-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2180/ozemin/stash-rename-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2180\n\nRange-diff vs v1:\n\n 1:  86af7ff53c ! 1:  d9def7b461 stash: add 'rename' subcommand\n     @@ Metadata\n      Author: Emin Özata <eminozata@proton.me>\n      \n       ## Commit message ##\n     -    stash: add 'rename' subcommand\n     +    stash: add 'reword' subcommand\n      \n     -    There is no way to change the message of a stash entry after the\n     -    fact.  The only option is dropping the entry and re-storing it by\n     -    hand, which moves it to the top of the stash list and gets fiddly\n     -    for deeper entries.\n     +    Stash entries accumulate with the default \"WIP on <branch>\" message\n     +    when they are created in a hurry, and there is no way to relabel the\n     +    ones worth keeping afterwards.  The only option is dropping an entry\n     +    and re-storing it by hand, which moves it to the top of the stash\n     +    list and gets fiddly for deeper entries.\n      \n     -    Add 'git stash rename <message> [<stash>]', defaulting to the\n     -    latest entry like the other subcommands do.  It reads the object id\n     -    and reflog message of the target entry and of the entries above it,\n     -    drops them all like 'git stash drop' would, and stores them back in\n     -    the same order, with the new message going to the target.  Position,\n     -    contents and the reflog chain stay as they were.\n     +    Add 'git stash reword <message> [<stash>]', which defaults to the\n     +    latest entry like the other subcommands do.  The name follows the\n     +    verb git already uses for changing only a message, as in the reword\n     +    command of 'git rebase -i' and in 'git history reword'.  It reads\n     +    the whole reflog and writes it back in a single transaction, with\n     +    the new message on the target and every other entry left as it was.\n     +    Position, contents, timestamps and the reflog chain all stay put.\n      \n     -    The command checks every entry it is about to rewrite and refuses\n     -    to start if one of them does not look like a stash commit, which\n     -    can only happen when refs/stash was written to by hand.  Finding\n     -    that out halfway through the sequence would lose entries.  Should a\n     -    write-back fail anyway, the entry's object id is reported so it can\n     -    be recovered with 'git stash store', and the command only reports\n     -    success when the reflog ended up in the requested state.\n     +    Before touching anything, the command inspects the target entry and\n     +    the ones above it, and refuses to start if any of them does not look\n     +    like a stash commit; that can only happen when refs/stash was\n     +    written to by hand.  The rewrite clears the reflog before writing\n     +    the new one, so if the transaction fails afterwards, the command\n     +    reports the object id of each collected entry and re-stores it\n     +    best-effort.  Whatever it fails to re-store can be recovered with\n     +    'git stash store'.\n      \n          This was proposed before: in 2010, as a \"git reflog update\" command\n          that edited reflog entries in place [1].  When it came up again in\n     @@ Commit message\n          stash message can pop and re-stash [3].  Michael Haggerty pointed\n          out in that thread that refs/stash does not fit the description:\n          its reflog is the primary data store for stash entries, and 'git\n     -    stash drop' rewrites it all the time [4].  So this patch stays away\n     -    from the reflog machinery entirely and does the suggested\n     -    pop-and-re-stash workaround mechanically, without the detour\n     -    through the working tree.\n     +    stash drop' rewrites it all the time [4].  So this patch rewrites\n     +    refs/stash directly, the way 'git stash drop' already does, through\n     +    ref_transaction_update_reflog(), which 'git remote rename' and 'git\n     +    refs migrate' already use to rewrite existing reflogs.\n      \n     -    The sequence only works if entry positions hold still while it\n     -    runs, so the command takes index-based selectors (stash@{1}) and\n     -    rejects time-based ones.  It also refreshes the reflog timestamps\n     -    of the rewritten entries, and renaming stash@{n} costs n+1 reflog\n     -    deletions and ref updates.\n     +    Name the target by index (stash@{1}); the command rejects\n     +    time-based selectors, because it needs the entry's position in the\n     +    reflog.  Writing the reflog back at once keeps the cost linear in\n     +    its length whatever that position is, and spares the reftable\n     +    backend from emitting and compacting a table per entry, which is\n     +    what dropping and re-storing the entries one by one would cost.\n      \n          [1] https://lore.kernel.org/git/20100620093142.GF24805@occam.hewgill.net/\n          [2] https://lore.kernel.org/git/loom.20130104T192132-16@post.gmane.org/\n     @@ Documentation/git-stash.adoc: 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     -+git stash rename [-q | --quiet] <message> [<stash>]\n     ++git stash reword [-q | --quiet] <message> [<stash>]\n       \n       DESCRIPTION\n       -----------\n     @@ Documentation/git-stash.adoc: with no conflicts.\n       \tcreated by `export`, and add them to the list of stashes.  To replace the\n       \texisting stashes, use `clear` first.\n       \n     -+`rename [-q | --quiet] <message> [<stash>]`::\n     ++`reword [-q | --quiet] <message> [<stash>]`::\n      +\tChange the message of a single stash entry.  The entry keeps its\n     -+\tposition and its contents.  _<stash>_ must name an entry by\n     -+\tindex (e.g. `stash@{1}`); renaming refreshes the reflog\n     -+\ttimestamps of the entry and of the entries above it.\n     ++\tposition, its contents and its reflog timestamp.  _<stash>_ must\n     ++\tname an entry by index (e.g. `stash@{1}`).\n      +\n       OPTIONS\n       -------\n     @@ Documentation/git-stash.adoc: literally (including newlines and quotes).\n       `--quiet`::\n       \tThis option is only valid for `apply`, `drop`, `pop`, `push`,\n      -\t`save`, `store` commands.\n     -+\t`rename`, `save`, `store` commands.\n     ++\t`reword`, `save`, `store` commands.\n       +\n       Quiet, suppress feedback messages.\n       \n     @@ Documentation/git-stash.adoc: For more details, see the 'pathspec' entry in link\n       _<stash>_::\n       \tThis option is only valid for `apply`, `branch`, `drop`, `pop`,\n      -\t`show`, and `export` commands.\n     -+\t`show`, `export`, and `rename` commands.\n     ++\t`show`, `export`, and `reword` 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}`).\n      \n       ## builtin/stash.c ##\n     +@@\n     + #include \"abspath.h\"\n     + #include \"config.h\"\n     + #include \"environment.h\"\n     ++#include \"date.h\"\n     + #include \"gettext.h\"\n     + #include \"hash.h\"\n     + #include \"hex.h\"\n     ++#include \"ident.h\"\n     + #include \"object-name.h\"\n     + #include \"parse-options.h\"\n     + #include \"refs.h\"\n      @@\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_RENAME_USAGE \\\n     -+\tN_(\"git stash rename [-q | --quiet] <message> [<stash>]\")\n     ++#define BUILTIN_STASH_REWORD_USAGE \\\n     ++\tN_(\"git stash reword [-q | --quiet] <message> [<stash>]\")\n       #define BUILTIN_STASH_CLEAR_USAGE \\\n       \t\"git stash clear\"\n       \n     @@ builtin/stash.c: static const char * const git_stash_usage[] = {\n       \tBUILTIN_STASH_STORE_USAGE,\n       \tBUILTIN_STASH_EXPORT_USAGE,\n       \tBUILTIN_STASH_IMPORT_USAGE,\n     -+\tBUILTIN_STASH_RENAME_USAGE,\n     ++\tBUILTIN_STASH_REWORD_USAGE,\n       \tNULL\n       };\n       \n     @@ builtin/stash.c: static const char * const git_stash_import_usage[] = {\n       \tNULL\n       };\n       \n     -+static const char * const git_stash_rename_usage[] = {\n     -+\tBUILTIN_STASH_RENAME_USAGE,\n     ++static const char * const git_stash_reword_usage[] = {\n     ++\tBUILTIN_STASH_REWORD_USAGE,\n      +\tNULL\n      +};\n      +\n       static const char ref_stash[] = \"refs/stash\";\n       static struct strbuf stash_index_path = STRBUF_INIT;\n       \n     -@@ builtin/stash.c: static int reflog_is_empty(const char *refname)\n     - \t\t\t\t\t refname, reject_reflog_ent, NULL);\n     - }\n     - \n     --static int do_drop_stash(struct stash_info *info, int quiet)\n     -+static int drop_reflog_entry(const char *revision)\n     - {\n     --\tif (!reflog_delete(info->revision.buf,\n     --\t\t\t   EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,\n     --\t\t\t   0)) {\n     --\t\tif (!quiet)\n     --\t\t\tprintf_ln(_(\"Dropped %s (%s)\"), info->revision.buf,\n     --\t\t\t\t  oid_to_hex(&info->w_commit));\n     --\t} else {\n     --\t\treturn error(_(\"%s: Could not drop stash entry\"),\n     --\t\t\t     info->revision.buf);\n     --\t}\n     -+\tif (reflog_delete(revision,\n     -+\t\t\t  EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,\n     -+\t\t\t  0))\n     -+\t\treturn error(_(\"%s: Could not drop stash entry\"), revision);\n     - \n     - \tif (reflog_is_empty(ref_stash))\n     - \t\tdo_clear_stash();\n     -@@ builtin/stash.c: static int do_drop_stash(struct stash_info *info, int quiet)\n     - \treturn 0;\n     - }\n     - \n     -+static int do_drop_stash(struct stash_info *info, int quiet)\n     -+{\n     -+\tif (drop_reflog_entry(info->revision.buf))\n     -+\t\treturn -1;\n     -+\n     -+\tif (!quiet)\n     -+\t\tprintf_ln(_(\"Dropped %s (%s)\"), info->revision.buf,\n     -+\t\t\t  oid_to_hex(&info->w_commit));\n     -+\n     -+\treturn 0;\n     -+}\n     -+\n     - static int get_stash_info_assert(struct stash_info *info, int argc,\n     - \t\t\t\t const char **argv)\n     - {\n      @@ builtin/stash.c: out:\n       \treturn ret;\n       }\n       \n     -+struct rename_entry {\n     -+\tstruct object_id oid;\n     ++struct reword_entry {\n     ++\tstruct object_id old_oid;\n     ++\tstruct object_id new_oid;\n     ++\tchar *committer;\n      +\tchar *msg;\n      +};\n      +\n     -+struct rename_data {\n     -+\tstruct rename_entry *entries;\n     ++struct reword_data {\n     ++\tstruct reword_entry *entries;\n      +\tsize_t nr, alloc;\n     -+\tsize_t want;\n      +};\n      +\n     -+static int collect_rename_entries(const char *refname UNUSED,\n     -+\t\t\t\t  struct object_id *old_oid UNUSED,\n     ++static int collect_reword_entries(const char *refname UNUSED,\n     ++\t\t\t\t  struct object_id *old_oid,\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,\n     ++\t\t\t\t  const char *committer,\n     ++\t\t\t\t  timestamp_t timestamp,\n     ++\t\t\t\t  int tz, const char *msg,\n      +\t\t\t\t  void *cb_data)\n      +{\n     -+\tstruct rename_data *data = cb_data;\n     ++\tstruct reword_data *data = cb_data;\n      +\tconst char *eol = strchrnul(msg, '\\n');\n     ++\tstruct reword_entry *entry;\n     ++\tstruct ident_split ident;\n      +\n      +\tALLOC_GROW(data->entries, data->nr + 1, data->alloc);\n     -+\toidcpy(&data->entries[data->nr].oid, new_oid);\n     -+\tdata->entries[data->nr].msg = xstrndup(msg, eol - msg);\n     -+\tdata->nr++;\n     ++\tentry = &data->entries[data->nr];\n     ++\toidcpy(&entry->old_oid, old_oid);\n     ++\toidcpy(&entry->new_oid, new_oid);\n     ++\tentry->msg = xstrndup(msg, eol - msg);\n     ++\n     ++\tif (split_ident_line(&ident, committer, strlen(committer)) < 0) {\n     ++\t\tentry->committer = xstrdup(committer);\n     ++\t} else {\n     ++\t\tstruct strbuf name = STRBUF_INIT, mail = STRBUF_INIT;\n     ++\t\tconst char *date = show_date(timestamp, tz, DATE_MODE(NORMAL));\n     ++\n     ++\t\tstrbuf_add(&name, ident.name_begin,\n     ++\t\t\t   ident.name_end - ident.name_begin);\n     ++\t\tstrbuf_add(&mail, ident.mail_begin,\n     ++\t\t\t   ident.mail_end - ident.mail_begin);\n     ++\t\tentry->committer = xstrdup(fmt_ident(name.buf, mail.buf,\n     ++\t\t\t\t\t\t     WANT_BLANK_IDENT, date, 0));\n     ++\t\tstrbuf_release(&name);\n     ++\t\tstrbuf_release(&mail);\n     ++\t}\n      +\n     -+\treturn data->nr >= data->want;\n     ++\tdata->nr++;\n     ++\treturn 0;\n      +}\n      +\n      +static int parse_stash_index(const char *revision, size_t *idx)\n     @@ builtin/stash.c: out:\n      +\treturn 0;\n      +}\n      +\n     -+static int store_rename_entry(struct rename_entry *entry, const char *msg)\n     ++static int do_reword_stash(struct stash_info *info, size_t idx,\n     ++\t\t\t   const char *reworded_msg, int quiet)\n      +{\n     -+\tif (!do_store_stash(&entry->oid, msg, 1))\n     -+\t\treturn 0;\n     -+\twarning(_(\"could not restore stash entry %s; \"\n     -+\t\t  \"recover it with 'git stash store %s'\"),\n     -+\t\toid_to_hex(&entry->oid), oid_to_hex(&entry->oid));\n     -+\treturn -1;\n     -+}\n     -+\n     -+static int do_rename_stash(struct stash_info *info, size_t idx,\n     -+\t\t\t   const char *msg, int quiet)\n     -+{\n     -+\tstruct rename_data data = { .want = idx + 1 };\n     -+\tsize_t i, missing = 0;\n     ++\tstruct ref_store *refs = get_main_ref_store(the_repository);\n     ++\tstruct ref_transaction *transaction = NULL;\n     ++\tstruct reword_data data = { 0 };\n     ++\tstruct strbuf err = STRBUF_INIT;\n     ++\tuint64_t index = 0;\n     ++\tsize_t i;\n      +\tint ret = -1;\n      +\n     -+\trefs_for_each_reflog_ent_reverse(get_main_ref_store(the_repository),\n     -+\t\t\t\t\t ref_stash, collect_rename_entries,\n     -+\t\t\t\t\t &data);\n     ++\trefs_for_each_reflog_ent_reverse(refs, ref_stash,\n     ++\t\t\t\t\t collect_reword_entries, &data);\n      +\tif (data.nr <= idx) {\n      +\t\terror(_(\"%s does not exist\"), info->revision.buf);\n      +\t\tgoto cleanup;\n      +\t}\n      +\n     -+\tif (!oideq(&info->w_commit, &data.entries[idx].oid)) {\n     ++\tif (!oideq(&info->w_commit, &data.entries[idx].new_oid)) {\n      +\t\terror(_(\"%s changed concurrently; try again\"),\n      +\t\t      info->revision.buf);\n      +\t\tgoto cleanup;\n      +\t}\n      +\n     -+\t/* refuse up front; do_store_stash() would die halfway through */\n     -+\tfor (i = 0; i < data.nr; i++) {\n     ++\tfor (i = 0; i <= idx; i++) {\n      +\t\tstruct commit *stash = lookup_commit_reference(the_repository,\n     -+\t\t\t\t\t\t\t       &data.entries[i].oid);\n     ++\t\t\t\t\t\t\t       &data.entries[i].new_oid);\n      +\n      +\t\tif (!stash || check_stash_topology(the_repository, stash)) {\n      +\t\t\terror(_(\"%s does not look like a stash commit\"),\n     -+\t\t\t      oid_to_hex(&data.entries[i].oid));\n     ++\t\t\t      oid_to_hex(&data.entries[i].new_oid));\n      +\t\t\tgoto cleanup;\n      +\t\t}\n      +\t}\n      +\n     -+\twhile (missing <= idx) {\n     -+\t\tif (drop_reflog_entry(\"stash@{0}\"))\n     -+\t\t\tgoto restore;\n     -+\t\tmissing++;\n     ++\tif (refs_delete_reflog(refs, ref_stash)) {\n     ++\t\terror(_(\"could not rewrite %s\"), ref_stash);\n     ++\t\tgoto cleanup;\n      +\t}\n      +\n     -+\tret = 0;\n     -+\twhile (missing) {\n     -+\t\ti = missing - 1;\n     -+\t\tif (store_rename_entry(&data.entries[i],\n     -+\t\t\t\t       i == idx ? msg : data.entries[i].msg))\n     -+\t\t\tret = -1;\n     -+\t\tmissing--;\n     ++\ttransaction = ref_store_transaction_begin(refs, 0, &err);\n     ++\tif (!transaction)\n     ++\t\tgoto restore;\n     ++\n     ++\tfor (i = data.nr; i-- > 0; ) {\n     ++\t\tif (ref_transaction_update_reflog(transaction, ref_stash,\n     ++\t\t\t\t\t\t  &data.entries[i].new_oid,\n     ++\t\t\t\t\t\t  &data.entries[i].old_oid,\n     ++\t\t\t\t\t\t  data.entries[i].committer,\n     ++\t\t\t\t\t\t  i == idx ? reworded_msg :\n     ++\t\t\t\t\t\t\t     data.entries[i].msg,\n     ++\t\t\t\t\t\t  index++, &err))\n     ++\t\t\tgoto restore;\n      +\t}\n      +\n     -+\tif (!ret && !quiet)\n     -+\t\tprintf_ln(_(\"Renamed %s (%s)\"), info->revision.buf,\n     -+\t\t\t  oid_to_hex(&data.entries[idx].oid));\n     ++\tif (ref_transaction_commit(transaction, &err))\n     ++\t\tgoto restore;\n     ++\n     ++\tret = 0;\n     ++\tif (!quiet)\n     ++\t\tprintf_ln(_(\"Reworded %s (%s)\"), info->revision.buf,\n     ++\t\t\t  oid_to_hex(&data.entries[idx].new_oid));\n      +\tgoto cleanup;\n      +\n      +restore:\n     -+\t/* dropping failed midway; put the dropped entries back */\n     -+\twhile (missing) {\n     -+\t\tstore_rename_entry(&data.entries[missing - 1],\n     -+\t\t\t\t   data.entries[missing - 1].msg);\n     -+\t\tmissing--;\n     -+\t}\n     ++\tif (err.len)\n     ++\t\terror(\"%s\", err.buf);\n     ++\tref_transaction_free(transaction);\n     ++\ttransaction = NULL;\n     ++\tfor (i = data.nr; i-- > 0; )\n     ++\t\tif (do_store_stash(&data.entries[i].new_oid,\n     ++\t\t\t\t   data.entries[i].msg, 1))\n     ++\t\t\twarning(_(\"could not restore stash entry %s; \"\n     ++\t\t\t\t  \"recover it with 'git stash store %s'\"),\n     ++\t\t\t\toid_to_hex(&data.entries[i].new_oid),\n     ++\t\t\t\toid_to_hex(&data.entries[i].new_oid));\n      +cleanup:\n     -+\tfor (i = 0; i < data.nr; i++)\n     ++\tref_transaction_free(transaction);\n     ++\tstrbuf_release(&err);\n     ++\tfor (i = 0; i < data.nr; i++) {\n     ++\t\tfree(data.entries[i].committer);\n      +\t\tfree(data.entries[i].msg);\n     ++\t}\n      +\tfree(data.entries);\n      +\treturn ret;\n      +}\n      +\n     -+static int rename_stash(int argc, const char **argv, const char *prefix,\n     ++static int reword_stash(int argc, const char **argv, const char *prefix,\n      +\t\t\tstruct repository *repo UNUSED)\n      +{\n      +\tint ret = -1;\n     @@ builtin/stash.c: out:\n      +\t};\n      +\n      +\targc = parse_options(argc, argv, prefix, options,\n     -+\t\t\t     git_stash_rename_usage, 0);\n     ++\t\t\t     git_stash_reword_usage, 0);\n      +\n      +\tif (!argc)\n     -+\t\tusage_with_options(git_stash_rename_usage, options);\n     ++\t\tusage_with_options(git_stash_reword_usage, options);\n      +\n      +\tif (!argv[0][strspn(argv[0], \" \\t\\r\\n\")]) {\n      +\t\tret = error(_(\"stash message cannot be empty\"));\n     @@ builtin/stash.c: out:\n      +\tif (get_stash_info_assert(&info, argc - 1, argv + 1))\n      +\t\tgoto cleanup;\n      +\n     -+\t/* positions must stay stable across the drop-and-store sequence */\n      +\tif (parse_stash_index(info.revision.buf, &idx)) {\n     -+\t\terror(_(\"cannot rename '%s': name the entry by index, \"\n     ++\t\terror(_(\"cannot reword '%s': name the entry by index, \"\n      +\t\t\t\"like 'stash@{1}'\"), info.revision.buf);\n      +\t\tgoto cleanup;\n      +\t}\n      +\n     -+\tret = do_rename_stash(&info, idx, argv[0], quiet);\n     ++\tret = do_reword_stash(&info, idx, argv[0], quiet);\n      +cleanup:\n      +\tfree_stash_info(&info);\n      +\treturn ret;\n     @@ builtin/stash.c: int cmd_stash(int argc,\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(\"rename\", &fn, rename_stash),\n     ++\t\tOPT_SUBCOMMAND(\"reword\", &fn, reword_stash),\n       \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n       \t\tOPT_END()\n       \t};\n     @@ contrib/completion/git-completion.bash: _git_sparse_checkout ()\n       _git_stash ()\n       {\n      -\tlocal subcommands='push list show apply clear drop pop create branch import export'\n     -+\tlocal subcommands='push list show apply clear drop pop create branch import export rename'\n     ++\tlocal subcommands='push list show apply clear drop pop create branch import export reword'\n       \tlocal subcommand=\"$(__git_find_on_cmdline \"$subcommands save\")\"\n       \n       \tif [ -z \"$subcommand\" ]; then\n     @@ contrib/completion/git-completion.bash: _git_stash ()\n       \t\t__git_complete_refs\n       \t\t;;\n      -\tshow,*|apply,*|drop,*|pop,*|export,*)\n     -+\tshow,*|apply,*|drop,*|pop,*|export,*|rename,*)\n     ++\tshow,*|apply,*|drop,*|pop,*|export,*|reword,*)\n       \t\t__gitcomp_nl \"$(__git stash list \\\n       \t\t\t\t| sed -n -e 's/:.*//p')\"\n       \t\t;;\n     @@ t/t3903-stash.sh: test_expect_success 'stash show --include-untracked includes u\n       \ttest_grep \"untracked\" actual\n       '\n       \n     -+test_expect_success 'rename a stash entry' '\n     ++test_expect_success 'reword a stash entry' '\n      +\tgit stash clear &&\n     -+\t>file-to-rename &&\n     -+\tgit add file-to-rename &&\n     ++\t>file-to-reword &&\n     ++\tgit add file-to-reword &&\n      +\tgit stash push -m \"original message\" &&\n     -+\tgit stash rename \"new message\" stash@{0} >out &&\n     -+\ttest_grep \"Renamed stash@{0}\" out &&\n     ++\tgit stash reword \"new message\" stash@{0} >out &&\n     ++\ttest_grep \"Reworded stash@{0}\" out &&\n      +\tgit stash list >list &&\n      +\ttest_grep \"stash@{0}: new message\" list &&\n      +\ttest_grep ! \"original message\" list\n      +'\n      +\n     -+test_expect_success 'rename defaults to the latest stash entry' '\n     -+\tgit stash rename \"default target\" >out &&\n     -+\ttest_grep \"Renamed refs/stash@{0}\" out &&\n     ++test_expect_success 'reword defaults to the latest stash entry' '\n     ++\tgit stash reword \"default target\" >out &&\n     ++\ttest_grep \"Reworded refs/stash@{0}\" out &&\n      +\tgit stash list >list &&\n      +\ttest_grep \"stash@{0}: default target\" list\n      +'\n      +\n     -+test_expect_success 'rename a deeper stash entry keeps positions and states' '\n     ++test_expect_success 'reword a deeper stash entry keeps positions and states' '\n      +\tgit stash clear &&\n      +\tfor i in 1 2 3\n      +\tdo\n     @@ t/t3903-stash.sh: test_expect_success 'stash show --include-untracked includes u\n      +\t\tgit stash push -m \"message $i\" || return 1\n      +\tdone &&\n      +\tgit rev-parse stash@{0} stash@{1} stash@{2} >expect &&\n     -+\tgit stash rename \"renamed middle\" stash@{1} &&\n     ++\tgit stash reword \"reworded middle\" stash@{1} &&\n      +\tgit rev-parse stash@{0} stash@{1} stash@{2} >actual &&\n      +\ttest_cmp expect actual &&\n      +\tgit stash list >list &&\n      +\ttest_grep \"stash@{0}: On.*message 3\" list &&\n     -+\ttest_grep \"stash@{1}: renamed middle\" list &&\n     ++\ttest_grep \"stash@{1}: reworded middle\" list &&\n      +\ttest_grep \"stash@{2}: On.*message 1\" list\n      +'\n      +\n     -+test_expect_success 'rename the deepest stash entry' '\n     ++test_expect_success 'reword the deepest stash entry' '\n      +\tgit rev-parse stash@{0} stash@{1} stash@{2} >expect &&\n     -+\tgit stash rename \"renamed deepest\" stash@{2} &&\n     ++\tgit stash reword \"reworded deepest\" stash@{2} &&\n      +\tgit rev-parse stash@{0} stash@{1} stash@{2} >actual &&\n      +\ttest_cmp expect actual &&\n      +\tgit stash list >list &&\n     -+\ttest_grep \"stash@{2}: renamed deepest\" list\n     ++\ttest_grep \"stash@{2}: reworded deepest\" list\n      +'\n      +\n     -+test_expect_success 'rename accepts a bare index and honors --quiet' '\n     -+\tgit stash rename -q \"quietly renamed\" 1 >out &&\n     ++test_expect_success 'reword accepts a bare index and honors --quiet' '\n     ++\tgit stash reword -q \"quietly reworded\" 1 >out &&\n      +\ttest_must_be_empty out &&\n      +\tgit stash list >list &&\n     -+\ttest_grep \"stash@{1}: quietly renamed\" list\n     ++\ttest_grep \"stash@{1}: quietly reworded\" list\n      +'\n      +\n     -+test_expect_success 'rename rejects bad arguments' '\n     -+\ttest_must_fail git stash rename \"no such entry\" stash@{99} &&\n     -+\ttest_must_fail git stash rename \"\" &&\n     -+\ttest_must_fail git stash rename \"   \" &&\n     -+\ttest_must_fail git stash rename \"not a stash\" HEAD &&\n     -+\ttest_must_fail git stash rename \"not an index\" \"stash@{now}\" &&\n     -+\ttest_expect_code 129 git stash rename &&\n     ++test_expect_success 'reword rejects bad arguments' '\n     ++\ttest_must_fail git stash reword \"no such entry\" stash@{99} &&\n     ++\ttest_must_fail git stash reword \"\" &&\n     ++\ttest_must_fail git stash reword \"   \" &&\n     ++\ttest_must_fail git stash reword \"not a stash\" HEAD &&\n     ++\ttest_must_fail git stash reword \"not an index\" \"stash@{now}\" &&\n     ++\ttest_expect_code 129 git stash reword &&\n      +\tgit stash list >list &&\n     -+\ttest_grep \"stash@{1}: quietly renamed\" list\n     ++\ttest_grep \"stash@{1}: quietly reworded\" list\n      +'\n      +\n     -+test_expect_success 'rename refuses to rewrite a non-stash reflog entry' '\n     ++test_expect_success 'reword refuses to rewrite a non-stash reflog entry' '\n      +\tgit stash clear &&\n      +\t>real-a &&\n      +\tgit add real-a &&\n     @@ t/t3903-stash.sh: test_expect_success 'stash show --include-untracked includes u\n      +\tgit add real-b &&\n      +\tgit stash push -m \"real B\" &&\n      +\tgit stash list >expect &&\n     -+\ttest_must_fail git stash rename \"renamed A\" stash@{2} &&\n     ++\ttest_must_fail git stash reword \"reworded A\" stash@{2} &&\n      +\tgit stash list >actual &&\n      +\ttest_cmp expect actual\n      +'\n\n\n Documentation/git-stash.adoc           |  10 +-\n builtin/stash.c                        | 201 +++++++++++++++++++++++++\n contrib/completion/git-completion.bash |   4 +-\n t/t3903-stash.sh                       |  79 ++++++++++\n 4 files changed, 290 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc\nindex 50bb89f483..16a2a015f3 100644\n--- a/Documentation/git-stash.adoc\n+++ b/Documentation/git-stash.adoc\n@@ -25,6 +25,7 @@ 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+git stash reword [-q | --quiet] <message> [<stash>]\n \n DESCRIPTION\n -----------\n@@ -163,6 +164,11 @@ with no conflicts.\n \tcreated by `export`, and add them to the list of stashes.  To replace the\n \texisting stashes, use `clear` first.\n \n+`reword [-q | --quiet] <message> [<stash>]`::\n+\tChange the message of a single stash entry.  The entry keeps its\n+\tposition, its contents and its reflog timestamp.  _<stash>_ must\n+\tname an entry by index (e.g. `stash@{1}`).\n+\n OPTIONS\n -------\n `-a`::\n@@ -258,7 +264,7 @@ literally (including newlines and quotes).\n `-q`::\n `--quiet`::\n \tThis option is only valid for `apply`, `drop`, `pop`, `push`,\n-\t`save`, `store` commands.\n+\t`reword`, `save`, `store` commands.\n +\n Quiet, suppress feedback messages.\n \n@@ -292,7 +298,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`, and `export` commands.\n+\t`show`, `export`, and `reword` 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 c4809f299a..61471cf510 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -4,9 +4,11 @@\n #include \"abspath.h\"\n #include \"config.h\"\n #include \"environment.h\"\n+#include \"date.h\"\n #include \"gettext.h\"\n #include \"hash.h\"\n #include \"hex.h\"\n+#include \"ident.h\"\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n@@ -63,6 +65,8 @@\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_REWORD_USAGE \\\n+\tN_(\"git stash reword [-q | --quiet] <message> [<stash>]\")\n #define BUILTIN_STASH_CLEAR_USAGE \\\n \t\"git stash clear\"\n \n@@ -80,6 +84,7 @@ static const char * const git_stash_usage[] = {\n \tBUILTIN_STASH_STORE_USAGE,\n \tBUILTIN_STASH_EXPORT_USAGE,\n \tBUILTIN_STASH_IMPORT_USAGE,\n+\tBUILTIN_STASH_REWORD_USAGE,\n \tNULL\n };\n \n@@ -143,6 +148,11 @@ static const char * const git_stash_import_usage[] = {\n \tNULL\n };\n \n+static const char * const git_stash_reword_usage[] = {\n+\tBUILTIN_STASH_REWORD_USAGE,\n+\tNULL\n+};\n+\n static const char ref_stash[] = \"refs/stash\";\n static struct strbuf stash_index_path = STRBUF_INIT;\n \n@@ -1190,6 +1200,196 @@ out:\n \treturn ret;\n }\n \n+struct reword_entry {\n+\tstruct object_id old_oid;\n+\tstruct object_id new_oid;\n+\tchar *committer;\n+\tchar *msg;\n+};\n+\n+struct reword_data {\n+\tstruct reword_entry *entries;\n+\tsize_t nr, alloc;\n+};\n+\n+static int collect_reword_entries(const char *refname UNUSED,\n+\t\t\t\t  struct object_id *old_oid,\n+\t\t\t\t  struct object_id *new_oid,\n+\t\t\t\t  const char *committer,\n+\t\t\t\t  timestamp_t timestamp,\n+\t\t\t\t  int tz, const char *msg,\n+\t\t\t\t  void *cb_data)\n+{\n+\tstruct reword_data *data = cb_data;\n+\tconst char *eol = strchrnul(msg, '\\n');\n+\tstruct reword_entry *entry;\n+\tstruct ident_split ident;\n+\n+\tALLOC_GROW(data->entries, data->nr + 1, data->alloc);\n+\tentry = &data->entries[data->nr];\n+\toidcpy(&entry->old_oid, old_oid);\n+\toidcpy(&entry->new_oid, new_oid);\n+\tentry->msg = xstrndup(msg, eol - msg);\n+\n+\tif (split_ident_line(&ident, committer, strlen(committer)) < 0) {\n+\t\tentry->committer = xstrdup(committer);\n+\t} else {\n+\t\tstruct strbuf name = STRBUF_INIT, mail = STRBUF_INIT;\n+\t\tconst char *date = show_date(timestamp, tz, DATE_MODE(NORMAL));\n+\n+\t\tstrbuf_add(&name, ident.name_begin,\n+\t\t\t   ident.name_end - ident.name_begin);\n+\t\tstrbuf_add(&mail, ident.mail_begin,\n+\t\t\t   ident.mail_end - ident.mail_begin);\n+\t\tentry->committer = xstrdup(fmt_ident(name.buf, mail.buf,\n+\t\t\t\t\t\t     WANT_BLANK_IDENT, date, 0));\n+\t\tstrbuf_release(&name);\n+\t\tstrbuf_release(&mail);\n+\t}\n+\n+\tdata->nr++;\n+\treturn 0;\n+}\n+\n+static int parse_stash_index(const char *revision, size_t *idx)\n+{\n+\tconst char *num = strstr(revision, \"@{\");\n+\tchar *end;\n+\n+\tif (!num || !isdigit(num[2]))\n+\t\treturn -1;\n+\t*idx = strtoumax(num + 2, &end, 10);\n+\tif (*end != '}' || end[1])\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+static int do_reword_stash(struct stash_info *info, size_t idx,\n+\t\t\t   const char *reworded_msg, int quiet)\n+{\n+\tstruct ref_store *refs = get_main_ref_store(the_repository);\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct reword_data data = { 0 };\n+\tstruct strbuf err = STRBUF_INIT;\n+\tuint64_t index = 0;\n+\tsize_t i;\n+\tint ret = -1;\n+\n+\trefs_for_each_reflog_ent_reverse(refs, ref_stash,\n+\t\t\t\t\t collect_reword_entries, &data);\n+\tif (data.nr <= idx) {\n+\t\terror(_(\"%s does not exist\"), info->revision.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (!oideq(&info->w_commit, &data.entries[idx].new_oid)) {\n+\t\terror(_(\"%s changed concurrently; try again\"),\n+\t\t      info->revision.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tfor (i = 0; i <= idx; i++) {\n+\t\tstruct commit *stash = lookup_commit_reference(the_repository,\n+\t\t\t\t\t\t\t       &data.entries[i].new_oid);\n+\n+\t\tif (!stash || check_stash_topology(the_repository, stash)) {\n+\t\t\terror(_(\"%s does not look like a stash commit\"),\n+\t\t\t      oid_to_hex(&data.entries[i].new_oid));\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\tif (refs_delete_reflog(refs, ref_stash)) {\n+\t\terror(_(\"could not rewrite %s\"), ref_stash);\n+\t\tgoto cleanup;\n+\t}\n+\n+\ttransaction = ref_store_transaction_begin(refs, 0, &err);\n+\tif (!transaction)\n+\t\tgoto restore;\n+\n+\tfor (i = data.nr; i-- > 0; ) {\n+\t\tif (ref_transaction_update_reflog(transaction, ref_stash,\n+\t\t\t\t\t\t  &data.entries[i].new_oid,\n+\t\t\t\t\t\t  &data.entries[i].old_oid,\n+\t\t\t\t\t\t  data.entries[i].committer,\n+\t\t\t\t\t\t  i == idx ? reworded_msg :\n+\t\t\t\t\t\t\t     data.entries[i].msg,\n+\t\t\t\t\t\t  index++, &err))\n+\t\t\tgoto restore;\n+\t}\n+\n+\tif (ref_transaction_commit(transaction, &err))\n+\t\tgoto restore;\n+\n+\tret = 0;\n+\tif (!quiet)\n+\t\tprintf_ln(_(\"Reworded %s (%s)\"), info->revision.buf,\n+\t\t\t  oid_to_hex(&data.entries[idx].new_oid));\n+\tgoto cleanup;\n+\n+restore:\n+\tif (err.len)\n+\t\terror(\"%s\", err.buf);\n+\tref_transaction_free(transaction);\n+\ttransaction = NULL;\n+\tfor (i = data.nr; i-- > 0; )\n+\t\tif (do_store_stash(&data.entries[i].new_oid,\n+\t\t\t\t   data.entries[i].msg, 1))\n+\t\t\twarning(_(\"could not restore stash entry %s; \"\n+\t\t\t\t  \"recover it with 'git stash store %s'\"),\n+\t\t\t\toid_to_hex(&data.entries[i].new_oid),\n+\t\t\t\toid_to_hex(&data.entries[i].new_oid));\n+cleanup:\n+\tref_transaction_free(transaction);\n+\tstrbuf_release(&err);\n+\tfor (i = 0; i < data.nr; i++) {\n+\t\tfree(data.entries[i].committer);\n+\t\tfree(data.entries[i].msg);\n+\t}\n+\tfree(data.entries);\n+\treturn ret;\n+}\n+\n+static int reword_stash(int argc, const char **argv, const char *prefix,\n+\t\t\tstruct repository *repo UNUSED)\n+{\n+\tint ret = -1;\n+\tint quiet = 0;\n+\tsize_t idx;\n+\tstruct stash_info info = STASH_INFO_INIT;\n+\tstruct option options[] = {\n+\t\tOPT__QUIET(&quiet, N_(\"be quiet, only report errors\")),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_stash_reword_usage, 0);\n+\n+\tif (!argc)\n+\t\tusage_with_options(git_stash_reword_usage, options);\n+\n+\tif (!argv[0][strspn(argv[0], \" \\t\\r\\n\")]) {\n+\t\tret = error(_(\"stash message cannot be empty\"));\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (get_stash_info_assert(&info, argc - 1, argv + 1))\n+\t\tgoto cleanup;\n+\n+\tif (parse_stash_index(info.revision.buf, &idx)) {\n+\t\terror(_(\"cannot reword '%s': name the entry by index, \"\n+\t\t\t\"like 'stash@{1}'\"), info.revision.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tret = do_reword_stash(&info, idx, argv[0], quiet);\n+cleanup:\n+\tfree_stash_info(&info);\n+\treturn ret;\n+}\n+\n static void add_pathspecs(struct strvec *args,\n \t\t\t  const struct pathspec *ps) {\n \tint i;\n@@ -2472,6 +2672,7 @@ int cmd_stash(int argc,\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(\"reword\", &fn, reword_stash),\n \t\tOPT_SUBCOMMAND_F(\"save\", &fn, save_stash, PARSE_OPT_NOCOMPLETE),\n \t\tOPT_END()\n \t};\ndiff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash\nindex e875787710..261c6bf101 100644\n--- a/contrib/completion/git-completion.bash\n+++ b/contrib/completion/git-completion.bash\n@@ -3465,7 +3465,7 @@ _git_sparse_checkout ()\n \n _git_stash ()\n {\n-\tlocal subcommands='push list show apply clear drop pop create branch import export'\n+\tlocal subcommands='push list show apply clear drop pop create branch import export reword'\n \tlocal subcommand=\"$(__git_find_on_cmdline \"$subcommands save\")\"\n \n \tif [ -z \"$subcommand\" ]; then\n@@ -3508,7 +3508,7 @@ _git_stash ()\n \timport,*)\n \t\t__git_complete_refs\n \t\t;;\n-\tshow,*|apply,*|drop,*|pop,*|export,*)\n+\tshow,*|apply,*|drop,*|pop,*|export,*|reword,*)\n \t\t__gitcomp_nl \"$(__git stash list \\\n \t\t\t\t| sed -n -e 's/:.*//p')\"\n \t\t;;\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex ecc35aae82..07fbbddac8 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -1831,4 +1831,83 @@ test_expect_success 'stash show --include-untracked includes untracked files' '\n \ttest_grep \"untracked\" actual\n '\n \n+test_expect_success 'reword a stash entry' '\n+\tgit stash clear &&\n+\t>file-to-reword &&\n+\tgit add file-to-reword &&\n+\tgit stash push -m \"original message\" &&\n+\tgit stash reword \"new message\" stash@{0} >out &&\n+\ttest_grep \"Reworded stash@{0}\" out &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{0}: new message\" list &&\n+\ttest_grep ! \"original message\" list\n+'\n+\n+test_expect_success 'reword defaults to the latest stash entry' '\n+\tgit stash reword \"default target\" >out &&\n+\ttest_grep \"Reworded refs/stash@{0}\" out &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{0}: default target\" list\n+'\n+\n+test_expect_success 'reword a deeper stash entry keeps positions and states' '\n+\tgit stash clear &&\n+\tfor i in 1 2 3\n+\tdo\n+\t\t>file$i &&\n+\t\tgit add file$i &&\n+\t\tgit stash push -m \"message $i\" || return 1\n+\tdone &&\n+\tgit rev-parse stash@{0} stash@{1} stash@{2} >expect &&\n+\tgit stash reword \"reworded middle\" stash@{1} &&\n+\tgit rev-parse stash@{0} stash@{1} stash@{2} >actual &&\n+\ttest_cmp expect actual &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{0}: On.*message 3\" list &&\n+\ttest_grep \"stash@{1}: reworded middle\" list &&\n+\ttest_grep \"stash@{2}: On.*message 1\" list\n+'\n+\n+test_expect_success 'reword the deepest stash entry' '\n+\tgit rev-parse stash@{0} stash@{1} stash@{2} >expect &&\n+\tgit stash reword \"reworded deepest\" stash@{2} &&\n+\tgit rev-parse stash@{0} stash@{1} stash@{2} >actual &&\n+\ttest_cmp expect actual &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{2}: reworded deepest\" list\n+'\n+\n+test_expect_success 'reword accepts a bare index and honors --quiet' '\n+\tgit stash reword -q \"quietly reworded\" 1 >out &&\n+\ttest_must_be_empty out &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{1}: quietly reworded\" list\n+'\n+\n+test_expect_success 'reword rejects bad arguments' '\n+\ttest_must_fail git stash reword \"no such entry\" stash@{99} &&\n+\ttest_must_fail git stash reword \"\" &&\n+\ttest_must_fail git stash reword \"   \" &&\n+\ttest_must_fail git stash reword \"not a stash\" HEAD &&\n+\ttest_must_fail git stash reword \"not an index\" \"stash@{now}\" &&\n+\ttest_expect_code 129 git stash reword &&\n+\tgit stash list >list &&\n+\ttest_grep \"stash@{1}: quietly reworded\" list\n+'\n+\n+test_expect_success 'reword refuses to rewrite a non-stash reflog entry' '\n+\tgit stash clear &&\n+\t>real-a &&\n+\tgit add real-a &&\n+\tgit stash push -m \"real A\" &&\n+\tgit update-ref -m junk --create-reflog refs/stash HEAD &&\n+\t>real-b &&\n+\tgit add real-b &&\n+\tgit stash push -m \"real B\" &&\n+\tgit stash list >expect &&\n+\ttest_must_fail git stash reword \"reworded A\" stash@{2} &&\n+\tgit stash list >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: 55526a18268bbc1ddaf8a6b7850c33d984eac9e9\n-- \ngitgitgadget\n"},{"id":"549096","messageId":"xmqqbjbsmkom.fsf@gitster.g","threadId":"66013","inReplyTo":"pull.2180.v2.git.1785149687514.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] stash: add 'reword' subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-27T17:00:57Z","receivedAt":"2026-07-27T17:01:03Z","isPatch":true,"body":"\"Emin Özata via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> +\trefs_for_each_reflog_ent_reverse(refs, ref_stash,\n> +\t\t\t\t\t collect_reword_entries, &data);\n> +\tif (data.nr <= idx) {\n> +\t\terror(_(\"%s does not exist\"), info->revision.buf);\n> +\t\tgoto cleanup;\n> +\t}\n> +\n> +\tif (!oideq(&info->w_commit, &data.entries[idx].new_oid)) {\n> +\t\terror(_(\"%s changed concurrently; try again\"),\n> +\t\t      info->revision.buf);\n> +\t\tgoto cleanup;\n> +\t}\n> +\n> +\tfor (i = 0; i <= idx; i++) {\n> +\t\tstruct commit *stash = lookup_commit_reference(the_repository,\n> +\t\t\t\t\t\t\t       &data.entries[i].new_oid);\n> +\n> +\t\tif (!stash || check_stash_topology(the_repository, stash)) {\n> +\t\t\terror(_(\"%s does not look like a stash commit\"),\n> +\t\t\t      oid_to_hex(&data.entries[i].new_oid));\n> +\t\t\tgoto cleanup;\n> +\t\t}\n> +\t}\n> +\n> +\tif (refs_delete_reflog(refs, ref_stash)) {\n> +\t\terror(_(\"could not rewrite %s\"), ref_stash);\n> +\t\tgoto cleanup;\n> +\t}\n> +\n> +\ttransaction = ref_store_transaction_begin(refs, 0, &err);\n> +\tif (!transaction)\n> +\t\tgoto restore;\n> +\n> +\tfor (i = data.nr; i-- > 0; ) {\n> +\t\tif (ref_transaction_update_reflog(transaction, ref_stash,\n> +\t\t\t\t\t\t  &data.entries[i].new_oid,\n> +\t\t\t\t\t\t  &data.entries[i].old_oid,\n> +\t\t\t\t\t\t  data.entries[i].committer,\n> +\t\t\t\t\t\t  i == idx ? reworded_msg :\n> +\t\t\t\t\t\t\t     data.entries[i].msg,\n> +\t\t\t\t\t\t  index++, &err))\n> +\t\t\tgoto restore;\n> +\t}\n> +\n> +\tif (ref_transaction_commit(transaction, &err))\n> +\t\tgoto restore;\n\nIs this a joke implementation, or is our reflog API so feature-poor\nthat it does not even allow replacing a single entry, leaving the\napplication to slurp everything, remove it, and recreate everything\nfrom scratch with only a single entry modified in the middle?\n\nWhat happens if your process gets killed after refs_delete_reflog()\nreturns but before finishing writing out what you collected?  The\ncopy you hoard in memory is the only copy, and we will lose the\ndata.\n\nUse of a transaction here does not help us at all.  When we abort,\nwe end up losing the reflog we had on disk before starting that\ntransaction.\n\nAm I reading the code incorrectly?  If I am not, I doubt that the\nabove implementation is acceptable.\n\nI wonder if the reflog API needs to be extended before we can\nimplement this properly.  I imagine a set of functions like (there\nmay be others)\n\n * refs_reflog_replace(ref_stash, idx, &reflog_data);\n\n   This would replace the reflog entry at idx with the data supplied,\n   which would probably be a pointer to something like:\n\n        struct reflog_data {\n                const struct object_id *new_oid;\n                const struct object_id *old_oid;\n                const char *committer_info;\n                const char *msg;\n        } reflog_data;\n\n   The files backend would implement this by doing something like\n   the following sequence:\n\n   - open a temporary file for writing, and the current reflog file\n     for reading (with a lock);\n   - copy the contents of the current reflog file to it, up to the\n     specified index;\n   - write out the single entry supplied in the reflog_data\n     structure;\n   - skip one entry in the current reflog file (which we are\n     replacing);\n   - copy the remainder of the current reflog file;\n   - atomically replace the current reflog file with the temporary\n     file.\n\n   The implementation for reftable may be vastly different, but\n   being a more database-oriented backend, it may be simpler to\n   replace a single entry in it.  I dunno.\n\n * refs_reflog_edit_in_bulk(ref_stash, num_edit, reflog_edit[]);\n\n   This would give us a bulk-edit interface, where reflog_edit would\n   be an array of structures, perhaps like this:\n\n        struct reflog_edit {\n                int idx;\n                enum {\n                        DELETE_REFLOG_ENT,\n                        REPLACE_REFLOG_ENT,\n                        INSERT_REFLOG_ENT,\n                } what;\n                struct reflog_data data;\n        } reflog_edit[];\n\n   The '.what' member would instruct the function what to do at the\n   specified '.idx' in the reflog, whether to delete the existing\n   entry, replace it, or insert a new entry.  The '.data' member is\n   used when replacing or inserting, but is ignored when deleting.\n\n   You may require the caller to sort the elements in this array in\n   increasing order of the '.idx' member if it makes the\n   implementation easier.  Or the implementation can sort the array\n   internally before starting to process the request.\n\nwill become the foundations of such a feature.\n"},{"id":"549140","messageId":"xmqq4ihjf7ds.fsf@gitster.g","threadId":"66013","inReplyTo":"xmqqbjbsmkom.fsf@gitster.g","subject":"Re: [PATCH v2] stash: add 'reword' subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-28T15:42:23Z","receivedAt":"2026-07-28T15:42:26Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I wonder if the reflog API needs to be extended before we can\n> implement this properly.  I imagine a set of functions like (there\n> may be others)\n>\n>  * refs_reflog_replace(ref_stash, idx, &reflog_data);\n>  * refs_reflog_edit_in_bulk(ref_stash, num_edit, reflog_edit[]);\n>\n> will become the foundations of such a feature.\n\nOn further thought, I think this fits pretty well into the general\narchitecture of the refs subsystem.  Both backends would need\nrefs_reflog_edit_in_bulk() in their vtable, while the single-entry\nedit can just be a thin wrapper passing a single-element\nreflog_edit[] array with a 'replace' operation.\n\nIf someone is interested in implementing this, there are a few\ntricky details to be careful about:\n\n * With delete/insert, indices drift.  In \"insert at stash@{5},\n   replace stash@{10}\", the second instruction targets what was\n   originally position #10, which becomes #11 after the insertion\n   at #5.  Pre-scanning the reflog_edit[] array in user order to\n   annotate each element with an effective '.idx' value should\n   resolve this, or something along those lines.\n\n * Multiple reflog_edit[] elements may target the same '.idx'.  In\n   \"replace stash@{4} with 'hello', replace stash@{4} with 'bye'\",\n   stash@{4} should end up as 'bye'.  If a backend sorts\n   reflog_edit[] by '.idx' (or in reverse, as the files backend\n   might do when copying from largest index to smallest),\n   processing must produce the same result as unsorted execution.\n   The sort needs to be stable, probably keyed on effective '.idx'\n   and tiebroken by original array position.\n\n * A reflog_edit[] array with \"delete stash@{4}\" followed by\n   \"replace stash@{4}\" asks for an impossible operation and must\n   error out.  Swapping the order (edit then delete) is technically\n   valid, though it feels like a user mistake.  I am undecided on\n   that one.\n\nAlthough \"git stash reword\" needs only 'replace', edit_in_bulk()\ncould consolidate existing operations like \"reflog delete\", \"stash\ndrop\", and \"stash pop\", and help clean up refs_reflog_expire().\nEven if initial support is limited to 'replace', designing for\n'delete' and 'insert' upfront saves us from a future rewrite.\n\nAs for \"git stash reword\" handling multi-line messages, the\nflat-file reflog format pretty much expects single-line entries.\nSince \"git stash push -m\" already squishes contiguous whitespace\n(including newlines) into a single space, \"stash reword\" should\nprobably follow suit.\n\nThat is about all for now.\n"},{"id":"550285","messageId":"ansaTmLSGUjWegFG@pks.im","threadId":"66013","inReplyTo":"xmqq4ihjf7ds.fsf@gitster.g","subject":"Re: [PATCH v2] stash: add 'reword' subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-11T12:49:18Z","receivedAt":"2026-08-11T12:49:26Z","isPatch":true,"body":"On Tue, Jul 28, 2026 at 08:42:23AM -0700, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > I wonder if the reflog API needs to be extended before we can\n> > implement this properly.  I imagine a set of functions like (there\n> > may be others)\n> >\n> >  * refs_reflog_replace(ref_stash, idx, &reflog_data);\n> >  * refs_reflog_edit_in_bulk(ref_stash, num_edit, reflog_edit[]);\n> >\n> > will become the foundations of such a feature.\n> \n> On further thought, I think this fits pretty well into the general\n> architecture of the refs subsystem.  Both backends would need\n> refs_reflog_edit_in_bulk() in their vtable, while the single-entry\n> edit can just be a thin wrapper passing a single-element\n> reflog_edit[] array with a 'replace' operation.\n\nOur reflog interfaces are still quite awful, but they have recently\nevolved so that we can at least batch-commit reflog entries via our\ntransaction interfaces. They are still severely lacking though, and I\nhaven't yet had the time (or motivation) to improve upon this.\n\n> If someone is interested in implementing this, there are a few\n> tricky details to be careful about:\n\nI think instead of introducing the proposed interface, we should first\ninvestigate whether we can make the reference transactions more powerful\nto allow for all of these use cases.\n\nThanks!\n\nPatrick\n"}]}