* [PATCH] stash: add 'rename' subcommand
@ 2026-07-16 8:31 Emin Özata via GitGitGadget
2026-07-16 10:08 ` Patrick Steinhardt
2026-07-27 10:54 ` [PATCH v2] stash: add 'reword' subcommand Emin Özata via GitGitGadget
0 siblings, 2 replies; 9+ messages in thread
From: Emin Özata via GitGitGadget @ 2026-07-16 8:31 UTC (permalink / raw)
To: git
Cc: Junio C Hamano, Greg Hewgill, Micheil Smith, Michael Haggerty,
Ævar Arnfjörð Bjarmason, Emin Özata,
Emin Özata
From: =?UTF-8?q?Emin=20=C3=96zata?= <eminozata@proton.me>
There is no way to change the message of a stash entry after the
fact. The only option is dropping the entry and re-storing it by
hand, which moves it to the top of the stash list and gets fiddly
for deeper entries.
Add 'git stash rename <message> [<stash>]', defaulting to the
latest entry like the other subcommands do. It reads the object id
and reflog message of the target entry and of the entries above it,
drops them all like 'git stash drop' would, and stores them back in
the same order, with the new message going to the target. Position,
contents and the reflog chain stay as they were.
The command checks every entry it is about to rewrite and refuses
to start if one of them does not look like a stash commit, which
can only happen when refs/stash was written to by hand. Finding
that out halfway through the sequence would lose entries. Should a
write-back fail anyway, the entry's object id is reported so it can
be recovered with 'git stash store', and the command only reports
success when the reflog ended up in the requested state.
This was proposed before: in 2010, as a "git reflog update" command
that edited reflog entries in place [1]. When it came up again in
2013 [2], Junio rejected it on the grounds that reflogs are
append-only recovery logs, and that whoever really cares about a
stash message can pop and re-stash [3]. Michael Haggerty pointed
out in that thread that refs/stash does not fit the description:
its reflog is the primary data store for stash entries, and 'git
stash drop' rewrites it all the time [4]. So this patch stays away
from the reflog machinery entirely and does the suggested
pop-and-re-stash workaround mechanically, without the detour
through the working tree.
The sequence only works if entry positions hold still while it
runs, so the command takes index-based selectors (stash@{1}) and
rejects time-based ones. It also refreshes the reflog timestamps
of the rewritten entries, and renaming stash@{n} costs n+1 reflog
deletions and ref updates.
[1] https://lore.kernel.org/git/20100620093142.GF24805@occam.hewgill.net/
[2] https://lore.kernel.org/git/loom.20130104T192132-16@post.gmane.org/
[3] https://lore.kernel.org/git/7vbod4tynt.fsf@alter.siamese.dyndns.org/
[4] https://lore.kernel.org/git/50ED2C78.1030300@alum.mit.edu/
Signed-off-by: Emin Özata <eminozata@proton.me>
---
stash: add 'rename' subcommand
eo/stash-rename
"git stash rename" learned to change the message of an existing stash
entry without changing its position or its contents.
This came up in 2010 and again in 2013, and was rejected back then
because the proposed implementation rewrote reflog entries in place.
This version doesn't: it does the drop-and-re-store dance that was
suggested as the manual workaround, with the machinery stash already
uses for drop and store, and touches nothing but refs/stash. Details and
links to the old threads are in the commit message.
Costs, so nobody has to dig for them: rewritten entries get fresh reflog
timestamps (hence index-only selectors), and renaming stash@{n} does n+1
reflog deletions, each of them a locked rewrite of the whole reflog. The
sequence is not atomic either: a failure halfway is handled by writing
the collected entries back best-effort, whatever cannot be written back
is reported with its object id so "git stash store" can recover it, and
a process killed between the drop and store phases loses the collected
entries (git fsck still finds them). A single refs_reflog_expire() pass
would cut both the I/O and that window down, and closing the window for
real needs a new refs API operation; I'd rather do either as a follow-up
if the feature is wanted at all.
I picked a positional <message> over -m <message> ("stash store" style);
no strong opinion, happy to switch.
t3903 passes with GIT_TEST_DEFAULT_REF_FORMAT=files and reftable.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2180%2Fozemin%2Fstash-rename-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2180/ozemin/stash-rename-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2180
Documentation/git-stash.adoc | 11 +-
builtin/stash.c | 197 +++++++++++++++++++++++--
contrib/completion/git-completion.bash | 4 +-
t/t3903-stash.sh | 79 ++++++++++
4 files changed, 276 insertions(+), 15 deletions(-)
diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc
index 50bb89f483..03f2e03096 100644
--- a/Documentation/git-stash.adoc
+++ b/Documentation/git-stash.adoc
@@ -25,6 +25,7 @@ git stash create [<message>]
git stash store [(-m | --message) <message>] [-q | --quiet] <commit>
git stash export (--print | --to-ref <ref>) [<stash>...]
git stash import <commit>
+git stash rename [-q | --quiet] <message> [<stash>]
DESCRIPTION
-----------
@@ -163,6 +164,12 @@ with no conflicts.
created by `export`, and add them to the list of stashes. To replace the
existing stashes, use `clear` first.
+`rename [-q | --quiet] <message> [<stash>]`::
+ Change the message of a single stash entry. The entry keeps its
+ position and its contents. _<stash>_ must name an entry by
+ index (e.g. `stash@{1}`); renaming refreshes the reflog
+ timestamps of the entry and of the entries above it.
+
OPTIONS
-------
`-a`::
@@ -258,7 +265,7 @@ literally (including newlines and quotes).
`-q`::
`--quiet`::
This option is only valid for `apply`, `drop`, `pop`, `push`,
- `save`, `store` commands.
+ `rename`, `save`, `store` commands.
+
Quiet, suppress feedback messages.
@@ -292,7 +299,7 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
_<stash>_::
This option is only valid for `apply`, `branch`, `drop`, `pop`,
- `show`, and `export` commands.
+ `show`, `export`, and `rename` commands.
+
A reference of the form `stash@{<revision>}`. When no _<stash>_ is
given, the latest stash is assumed (that is, `stash@{0}`).
diff --git a/builtin/stash.c b/builtin/stash.c
index c4809f299a..94e66d6074 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -63,6 +63,8 @@
N_("git stash export (--print | --to-ref <ref>) [<stash>...]")
#define BUILTIN_STASH_IMPORT_USAGE \
N_("git stash import <commit>")
+#define BUILTIN_STASH_RENAME_USAGE \
+ N_("git stash rename [-q | --quiet] <message> [<stash>]")
#define BUILTIN_STASH_CLEAR_USAGE \
"git stash clear"
@@ -80,6 +82,7 @@ static const char * const git_stash_usage[] = {
BUILTIN_STASH_STORE_USAGE,
BUILTIN_STASH_EXPORT_USAGE,
BUILTIN_STASH_IMPORT_USAGE,
+ BUILTIN_STASH_RENAME_USAGE,
NULL
};
@@ -143,6 +146,11 @@ static const char * const git_stash_import_usage[] = {
NULL
};
+static const char * const git_stash_rename_usage[] = {
+ BUILTIN_STASH_RENAME_USAGE,
+ NULL
+};
+
static const char ref_stash[] = "refs/stash";
static struct strbuf stash_index_path = STRBUF_INIT;
@@ -820,18 +828,12 @@ static int reflog_is_empty(const char *refname)
refname, reject_reflog_ent, NULL);
}
-static int do_drop_stash(struct stash_info *info, int quiet)
+static int drop_reflog_entry(const char *revision)
{
- if (!reflog_delete(info->revision.buf,
- EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,
- 0)) {
- if (!quiet)
- printf_ln(_("Dropped %s (%s)"), info->revision.buf,
- oid_to_hex(&info->w_commit));
- } else {
- return error(_("%s: Could not drop stash entry"),
- info->revision.buf);
- }
+ if (reflog_delete(revision,
+ EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,
+ 0))
+ return error(_("%s: Could not drop stash entry"), revision);
if (reflog_is_empty(ref_stash))
do_clear_stash();
@@ -839,6 +841,18 @@ static int do_drop_stash(struct stash_info *info, int quiet)
return 0;
}
+static int do_drop_stash(struct stash_info *info, int quiet)
+{
+ if (drop_reflog_entry(info->revision.buf))
+ return -1;
+
+ if (!quiet)
+ printf_ln(_("Dropped %s (%s)"), info->revision.buf,
+ oid_to_hex(&info->w_commit));
+
+ return 0;
+}
+
static int get_stash_info_assert(struct stash_info *info, int argc,
const char **argv)
{
@@ -1190,6 +1204,166 @@ out:
return ret;
}
+struct rename_entry {
+ struct object_id oid;
+ char *msg;
+};
+
+struct rename_data {
+ struct rename_entry *entries;
+ size_t nr, alloc;
+ size_t want;
+};
+
+static int collect_rename_entries(const char *refname UNUSED,
+ struct object_id *old_oid UNUSED,
+ struct object_id *new_oid,
+ const char *committer UNUSED,
+ timestamp_t timestamp UNUSED,
+ int tz UNUSED, const char *msg,
+ void *cb_data)
+{
+ struct rename_data *data = cb_data;
+ const char *eol = strchrnul(msg, '\n');
+
+ ALLOC_GROW(data->entries, data->nr + 1, data->alloc);
+ oidcpy(&data->entries[data->nr].oid, new_oid);
+ data->entries[data->nr].msg = xstrndup(msg, eol - msg);
+ data->nr++;
+
+ return data->nr >= data->want;
+}
+
+static int parse_stash_index(const char *revision, size_t *idx)
+{
+ const char *num = strstr(revision, "@{");
+ char *end;
+
+ if (!num || !isdigit(num[2]))
+ return -1;
+ *idx = strtoumax(num + 2, &end, 10);
+ if (*end != '}' || end[1])
+ return -1;
+
+ return 0;
+}
+
+static int store_rename_entry(struct rename_entry *entry, const char *msg)
+{
+ if (!do_store_stash(&entry->oid, msg, 1))
+ return 0;
+ warning(_("could not restore stash entry %s; "
+ "recover it with 'git stash store %s'"),
+ oid_to_hex(&entry->oid), oid_to_hex(&entry->oid));
+ return -1;
+}
+
+static int do_rename_stash(struct stash_info *info, size_t idx,
+ const char *msg, int quiet)
+{
+ struct rename_data data = { .want = idx + 1 };
+ size_t i, missing = 0;
+ int ret = -1;
+
+ refs_for_each_reflog_ent_reverse(get_main_ref_store(the_repository),
+ ref_stash, collect_rename_entries,
+ &data);
+ if (data.nr <= idx) {
+ error(_("%s does not exist"), info->revision.buf);
+ goto cleanup;
+ }
+
+ if (!oideq(&info->w_commit, &data.entries[idx].oid)) {
+ error(_("%s changed concurrently; try again"),
+ info->revision.buf);
+ goto cleanup;
+ }
+
+ /* refuse up front; do_store_stash() would die halfway through */
+ for (i = 0; i < data.nr; i++) {
+ struct commit *stash = lookup_commit_reference(the_repository,
+ &data.entries[i].oid);
+
+ if (!stash || check_stash_topology(the_repository, stash)) {
+ error(_("%s does not look like a stash commit"),
+ oid_to_hex(&data.entries[i].oid));
+ goto cleanup;
+ }
+ }
+
+ while (missing <= idx) {
+ if (drop_reflog_entry("stash@{0}"))
+ goto restore;
+ missing++;
+ }
+
+ ret = 0;
+ while (missing) {
+ i = missing - 1;
+ if (store_rename_entry(&data.entries[i],
+ i == idx ? msg : data.entries[i].msg))
+ ret = -1;
+ missing--;
+ }
+
+ if (!ret && !quiet)
+ printf_ln(_("Renamed %s (%s)"), info->revision.buf,
+ oid_to_hex(&data.entries[idx].oid));
+ goto cleanup;
+
+restore:
+ /* dropping failed midway; put the dropped entries back */
+ while (missing) {
+ store_rename_entry(&data.entries[missing - 1],
+ data.entries[missing - 1].msg);
+ missing--;
+ }
+cleanup:
+ for (i = 0; i < data.nr; i++)
+ free(data.entries[i].msg);
+ free(data.entries);
+ return ret;
+}
+
+static int rename_stash(int argc, const char **argv, const char *prefix,
+ struct repository *repo UNUSED)
+{
+ int ret = -1;
+ int quiet = 0;
+ size_t idx;
+ struct stash_info info = STASH_INFO_INIT;
+ struct option options[] = {
+ OPT__QUIET(&quiet, N_("be quiet, only report errors")),
+ OPT_END()
+ };
+
+ argc = parse_options(argc, argv, prefix, options,
+ git_stash_rename_usage, 0);
+
+ if (!argc)
+ usage_with_options(git_stash_rename_usage, options);
+
+ if (!argv[0][strspn(argv[0], " \t\r\n")]) {
+ ret = error(_("stash message cannot be empty"));
+ goto cleanup;
+ }
+
+ if (get_stash_info_assert(&info, argc - 1, argv + 1))
+ goto cleanup;
+
+ /* positions must stay stable across the drop-and-store sequence */
+ if (parse_stash_index(info.revision.buf, &idx)) {
+ error(_("cannot rename '%s': name the entry by index, "
+ "like 'stash@{1}'"), info.revision.buf);
+ goto cleanup;
+ }
+
+ ret = do_rename_stash(&info, idx, argv[0], quiet);
+cleanup:
+ free_stash_info(&info);
+ return ret;
+}
+
static void add_pathspecs(struct strvec *args,
const struct pathspec *ps) {
int i;
@@ -2472,6 +2646,7 @@ int cmd_stash(int argc,
OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
OPT_SUBCOMMAND("export", &fn, export_stash),
OPT_SUBCOMMAND("import", &fn, import_stash),
+ OPT_SUBCOMMAND("rename", &fn, rename_stash),
OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
OPT_END()
};
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index e875787710..08c53cea49 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -3465,7 +3465,7 @@ _git_sparse_checkout ()
_git_stash ()
{
- local subcommands='push list show apply clear drop pop create branch import export'
+ local subcommands='push list show apply clear drop pop create branch import export rename'
local subcommand="$(__git_find_on_cmdline "$subcommands save")"
if [ -z "$subcommand" ]; then
@@ -3508,7 +3508,7 @@ _git_stash ()
import,*)
__git_complete_refs
;;
- show,*|apply,*|drop,*|pop,*|export,*)
+ show,*|apply,*|drop,*|pop,*|export,*|rename,*)
__gitcomp_nl "$(__git stash list \
| sed -n -e 's/:.*//p')"
;;
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index ecc35aae82..f175302c1a 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -1831,4 +1831,83 @@ test_expect_success 'stash show --include-untracked includes untracked files' '
test_grep "untracked" actual
'
+test_expect_success 'rename a stash entry' '
+ git stash clear &&
+ >file-to-rename &&
+ git add file-to-rename &&
+ git stash push -m "original message" &&
+ git stash rename "new message" stash@{0} >out &&
+ test_grep "Renamed stash@{0}" out &&
+ git stash list >list &&
+ test_grep "stash@{0}: new message" list &&
+ test_grep ! "original message" list
+'
+
+test_expect_success 'rename defaults to the latest stash entry' '
+ git stash rename "default target" >out &&
+ test_grep "Renamed refs/stash@{0}" out &&
+ git stash list >list &&
+ test_grep "stash@{0}: default target" list
+'
+
+test_expect_success 'rename a deeper stash entry keeps positions and states' '
+ git stash clear &&
+ for i in 1 2 3
+ do
+ >file$i &&
+ git add file$i &&
+ git stash push -m "message $i" || return 1
+ done &&
+ git rev-parse stash@{0} stash@{1} stash@{2} >expect &&
+ git stash rename "renamed middle" stash@{1} &&
+ git rev-parse stash@{0} stash@{1} stash@{2} >actual &&
+ test_cmp expect actual &&
+ git stash list >list &&
+ test_grep "stash@{0}: On.*message 3" list &&
+ test_grep "stash@{1}: renamed middle" list &&
+ test_grep "stash@{2}: On.*message 1" list
+'
+
+test_expect_success 'rename the deepest stash entry' '
+ git rev-parse stash@{0} stash@{1} stash@{2} >expect &&
+ git stash rename "renamed deepest" stash@{2} &&
+ git rev-parse stash@{0} stash@{1} stash@{2} >actual &&
+ test_cmp expect actual &&
+ git stash list >list &&
+ test_grep "stash@{2}: renamed deepest" list
+'
+
+test_expect_success 'rename accepts a bare index and honors --quiet' '
+ git stash rename -q "quietly renamed" 1 >out &&
+ test_must_be_empty out &&
+ git stash list >list &&
+ test_grep "stash@{1}: quietly renamed" list
+'
+
+test_expect_success 'rename rejects bad arguments' '
+ test_must_fail git stash rename "no such entry" stash@{99} &&
+ test_must_fail git stash rename "" &&
+ test_must_fail git stash rename " " &&
+ test_must_fail git stash rename "not a stash" HEAD &&
+ test_must_fail git stash rename "not an index" "stash@{now}" &&
+ test_expect_code 129 git stash rename &&
+ git stash list >list &&
+ test_grep "stash@{1}: quietly renamed" list
+'
+
+test_expect_success 'rename refuses to rewrite a non-stash reflog entry' '
+ git stash clear &&
+ >real-a &&
+ git add real-a &&
+ git stash push -m "real A" &&
+ git update-ref -m junk --create-reflog refs/stash HEAD &&
+ >real-b &&
+ git add real-b &&
+ git stash push -m "real B" &&
+ git stash list >expect &&
+ test_must_fail git stash rename "renamed A" stash@{2} &&
+ git stash list >actual &&
+ test_cmp expect actual
+'
+
test_done
base-commit: 55526a18268bbc1ddaf8a6b7850c33d984eac9e9
--
gitgitgadget
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH] stash: add 'rename' subcommand
2026-07-16 8:31 [PATCH] stash: add 'rename' subcommand Emin Özata via GitGitGadget
@ 2026-07-16 10:08 ` Patrick Steinhardt
2026-07-16 17:46 ` Junio C Hamano
2026-07-27 10:54 ` [PATCH v2] stash: add 'reword' subcommand Emin Özata via GitGitGadget
1 sibling, 1 reply; 9+ messages in thread
From: Patrick Steinhardt @ 2026-07-16 10:08 UTC (permalink / raw)
To: Emin Özata via GitGitGadget
Cc: git, Junio C Hamano, Greg Hewgill, Micheil Smith,
Michael Haggerty, Ævar Arnfjörð Bjarmason,
Emin Özata
On Thu, Jul 16, 2026 at 08:31:45AM +0000, Emin Özata via GitGitGadget wrote:
> From: =?UTF-8?q?Emin=20=C3=96zata?= <eminozata@proton.me>
>
> There is no way to change the message of a stash entry after the
> fact. The only option is dropping the entry and re-storing it by
> hand, which moves it to the top of the stash list and gets fiddly
> for deeper entries.
>
> Add 'git stash rename <message> [<stash>]', defaulting to the
> latest entry like the other subcommands do. It reads the object id
> and reflog message of the target entry and of the entries above it,
> drops them all like 'git stash drop' would, and stores them back in
> the same order, with the new message going to the target. Position,
> contents and the reflog chain stay as they were.
>
> The command checks every entry it is about to rewrite and refuses
> to start if one of them does not look like a stash commit, which
> can only happen when refs/stash was written to by hand. Finding
> that out halfway through the sequence would lose entries. Should a
> write-back fail anyway, the entry's object id is reported so it can
> be recovered with 'git stash store', and the command only reports
> success when the reflog ended up in the requested state.
>
> This was proposed before: in 2010, as a "git reflog update" command
> that edited reflog entries in place [1]. When it came up again in
> 2013 [2], Junio rejected it on the grounds that reflogs are
> append-only recovery logs, and that whoever really cares about a
> stash message can pop and re-stash [3]. Michael Haggerty pointed
> out in that thread that refs/stash does not fit the description:
> its reflog is the primary data store for stash entries, and 'git
> stash drop' rewrites it all the time [4]. So this patch stays away
> from the reflog machinery entirely and does the suggested
> pop-and-re-stash workaround mechanically, without the detour
> through the working tree.
Hm. It's good to refer to to previous discussions. But I think it would
make sense to also document why explicitly _you_ want to have this
functionality. Like, what use case does it enable that you currently
cannot have right now? How is this different to what was proposed back
then that should make us reconsider whether or not to include it now?
> diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc
> index 50bb89f483..03f2e03096 100644
> --- a/Documentation/git-stash.adoc
> +++ b/Documentation/git-stash.adoc
> @@ -163,6 +164,12 @@ with no conflicts.
> created by `export`, and add them to the list of stashes. To replace the
> existing stashes, use `clear` first.
>
> +`rename [-q | --quiet] <message> [<stash>]`::
> + Change the message of a single stash entry. The entry keeps its
> + position and its contents. _<stash>_ must name an entry by
> + index (e.g. `stash@{1}`); renaming refreshes the reflog
> + timestamps of the entry and of the entries above it.
I think "rename" is a bit of a misleading name, doubly so with the
recently introduced `git refs rename` feature that renames a reference.
I'd suggest "reword" instead.
> diff --git a/builtin/stash.c b/builtin/stash.c
> index c4809f299a..94e66d6074 100644
> --- a/builtin/stash.c
> +++ b/builtin/stash.c
> @@ -1190,6 +1204,166 @@ out:
[snip]
> +static int do_rename_stash(struct stash_info *info, size_t idx,
> + const char *msg, int quiet)
> +{
> + struct rename_data data = { .want = idx + 1 };
> + size_t i, missing = 0;
> + int ret = -1;
> +
> + refs_for_each_reflog_ent_reverse(get_main_ref_store(the_repository),
> + ref_stash, collect_rename_entries,
> + &data);
> + if (data.nr <= idx) {
> + error(_("%s does not exist"), info->revision.buf);
> + goto cleanup;
> + }
> +
> + if (!oideq(&info->w_commit, &data.entries[idx].oid)) {
> + error(_("%s changed concurrently; try again"),
> + info->revision.buf);
> + goto cleanup;
> + }
> +
> + /* refuse up front; do_store_stash() would die halfway through */
> + for (i = 0; i < data.nr; i++) {
> + struct commit *stash = lookup_commit_reference(the_repository,
> + &data.entries[i].oid);
> +
> + if (!stash || check_stash_topology(the_repository, stash)) {
> + error(_("%s does not look like a stash commit"),
> + oid_to_hex(&data.entries[i].oid));
> + goto cleanup;
> + }
> + }
This loop here has potentially-quadratic runtime. Not so much with the
"files" backend, where we'll simply append the data to the log. But with
the reftable backend we'll basically end up writing each reflog entry
into a new table, and we'll end up compacting the tables many times
over.
> +
> + while (missing <= idx) {
> + if (drop_reflog_entry("stash@{0}"))
> + goto restore;
> + missing++;
> + }
Same here, this will not perform well if you have a huge reflog.
We really should do all of this atomically, where we ideally delete the
old reflog and create the new reflog in a single transaction.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] stash: add 'rename' subcommand
2026-07-16 10:08 ` Patrick Steinhardt
@ 2026-07-16 17:46 ` Junio C Hamano
2026-07-16 21:08 ` brian m. carlson
0 siblings, 1 reply; 9+ messages in thread
From: Junio C Hamano @ 2026-07-16 17:46 UTC (permalink / raw)
To: Patrick Steinhardt
Cc: Emin Özata via GitGitGadget, git, Greg Hewgill,
Micheil Smith, Michael Haggerty,
Ævar Arnfjörð Bjarmason, Emin Özata
Patrick Steinhardt <ps@pks.im> writes:
> Hm. It's good to refer to to previous discussions. But I think it would
> make sense to also document why explicitly _you_ want to have this
> functionality. Like, what use case does it enable that you currently
> cannot have right now? How is this different to what was proposed back
> then that should make us reconsider whether or not to include it now?
We have one previous discussion thread on this, and there is no
need to refer to every single message in it. Just pointing to
the discussion starter should be enough to let us read what was
said. It was also very nice of Emin to summarize the viewpoints
expressed there, so that readers can follow along without having to
dig up external resources.
Asking for the actual use case is indeed very useful, and I highly
appreciate the nudge.
"I cannot reword the message shown in 'git stash list' without this
change" is not a use case. It would be far more useful to know
what real-world task you are blocked from accomplishing just
because you cannot reword a misspelled entry in 'git stash list'.
Personally, I cannot think of any such roadblock. If I find that
a stash entry is worth tweaking right now, I would probably
create a new branch for the task, unstash it there, and either
make further changes or record the exact state with an updated
message to help my future self. After all, having a branch I
can start working on immediately is far more useful than having
the same stash entry with its spelling corrected.
Thanks.
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] stash: add 'rename' subcommand
2026-07-16 17:46 ` Junio C Hamano
@ 2026-07-16 21:08 ` brian m. carlson
2026-07-17 1:11 ` Junio C Hamano
0 siblings, 1 reply; 9+ messages in thread
From: brian m. carlson @ 2026-07-16 21:08 UTC (permalink / raw)
To: Junio C Hamano
Cc: Patrick Steinhardt, Emin Özata via GitGitGadget, git,
Greg Hewgill, Micheil Smith, Michael Haggerty,
Ævar Arnfjörð Bjarmason, Emin Özata
[-- Attachment #1: Type: text/plain, Size: 1819 bytes --]
On 2026-07-16 at 17:46:13, Junio C Hamano wrote:
> Asking for the actual use case is indeed very useful, and I highly
> appreciate the nudge.
>
> "I cannot reword the message shown in 'git stash list' without this
> change" is not a use case. It would be far more useful to know
> what real-world task you are blocked from accomplishing just
> because you cannot reword a misspelled entry in 'git stash list'.
>
> Personally, I cannot think of any such roadblock. If I find that
> a stash entry is worth tweaking right now, I would probably
> create a new branch for the task, unstash it there, and either
> make further changes or record the exact state with an updated
> message to help my future self. After all, having a branch I
> can start working on immediately is far more useful than having
> the same stash entry with its spelling corrected.
I have a large number of stashes in my git.git repository (211 at
present) and it may be that I, in a mad rush to solve some problem or
another, went with the default message. I do this kind of thing with
repositories at work all the time. But it's inconvenient to have to
search through `git stash list -p` to see which change it was and which
stash I need to pop to resume my work, so I can see the utility of this
approach.
In my case, the stash is likely on an existing branch (such as my
sha256-interop branch), so I don't want to create another branch right
now, but I do want to distinguish a couple of different stashes.
Of course, I don't know what Emin's use case is, but I think it would in
fact be useful to have and I can see myself using it. If my use case is
valuable to others, anyone is free to summarize it in the commit message
as a justification.
--
brian m. carlson (they/them)
Toronto, Ontario, CA
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] stash: add 'rename' subcommand
2026-07-16 21:08 ` brian m. carlson
@ 2026-07-17 1:11 ` Junio C Hamano
2026-07-26 9:00 ` erik88
0 siblings, 1 reply; 9+ messages in thread
From: Junio C Hamano @ 2026-07-17 1:11 UTC (permalink / raw)
To: brian m. carlson
Cc: Patrick Steinhardt, Emin Özata via GitGitGadget, git,
Greg Hewgill, Micheil Smith, Michael Haggerty,
Ævar Arnfjörð Bjarmason, Emin Özata
"brian m. carlson" <sandals@crustytoothpaste.net> writes:
> I have a large number of stashes in my git.git repository (211 at
> present) and it may be that I, in a mad rush to solve some problem or
> another, went with the default message. I do this kind of thing with
> repositories at work all the time. But it's inconvenient to have to
> search through `git stash list -p` to see which change it was and which
> stash I need to pop to resume my work, so I can see the utility of this
> approach.
Hmph, are you saying that you are going to wade through 211
stash entries one by one and reword them?
> In my case, the stash is likely on an existing branch (such as my
> sha256-interop branch), so I don't want to create another branch right
> now, but I do want to distinguish a couple of different stashes.
This is not necessarily related to what Emin's patch does or tries
to do, but it suggests to me that it would be ultra-nice if we could
have a stash that is associated with each individual branch. It is
as if we have been living in a world with only detached HEADs and
you suddenly invented named branches. ;-)
Well, the original design motivation behind "git stash" was the
"boss is here" emergency mode, and the expectation was that such
an emergency would not nest to leave anyone with more than a
handful of entries. Thus, a single stash shared repository-wide
across your branches was not a huge problem. But our workflows
have evolved, so our tool support may have to evolve alongside
them.
Thanks for the food for thought.
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] stash: add 'rename' subcommand
2026-07-17 1:11 ` Junio C Hamano
@ 2026-07-26 9:00 ` erik88
2026-07-27 5:45 ` Junio C Hamano
0 siblings, 1 reply; 9+ messages in thread
From: erik88 @ 2026-07-26 9:00 UTC (permalink / raw)
To: Junio C Hamano
Cc: brian m. carlson, Patrick Steinhardt,
Emin Özata via GitGitGadget, git, Greg Hewgill,
Micheil Smith, Michael Haggerty,
Ævar Arnfjörð Bjarmason, Emin Özata
On 26/07/16 06:11PM, Junio C Hamano wrote:
> "brian m. carlson" <sandals@crustytoothpaste.net> writes:
>
> > I have a large number of stashes in my git.git repository (211 at
> > present) and it may be that I, in a mad rush to solve some problem or
> > another, went with the default message. I do this kind of thing with
> > repositories at work all the time. But it's inconvenient to have to
> > search through `git stash list -p` to see which change it was and which
> > stash I need to pop to resume my work, so I can see the utility of this
> > approach.
Same. I always accumulate 100+ stashes. I never bother with a message
when I stash. 90-95% of the stashes are useless cruft but there's always
an experiment, a script file of some sort, which turns out to be useful.
> Hmph, are you saying that you are going to wade through 211
> stash entries one by one and reword them?
Not me but my AI agent ;-)
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] stash: add 'rename' subcommand
2026-07-26 9:00 ` erik88
@ 2026-07-27 5:45 ` Junio C Hamano
2026-07-27 9:18 ` Emin
0 siblings, 1 reply; 9+ messages in thread
From: Junio C Hamano @ 2026-07-27 5:45 UTC (permalink / raw)
To: erik88
Cc: brian m. carlson, Patrick Steinhardt,
Emin Özata via GitGitGadget, git, Greg Hewgill,
Micheil Smith, Michael Haggerty,
Ævar Arnfjörð Bjarmason, Emin Özata
erik88 <erik88@gmail.com> writes:
> On 26/07/16 06:11PM, Junio C Hamano wrote:
>> "brian m. carlson" <sandals@crustytoothpaste.net> writes:
>>
>> > I have a large number of stashes in my git.git repository (211 at
>> > present) and it may be that I, in a mad rush to solve some problem or
>> > another, went with the default message. I do this kind of thing with
>> > repositories at work all the time. But it's inconvenient to have to
>> > search through `git stash list -p` to see which change it was and which
>> > stash I need to pop to resume my work, so I can see the utility of this
>> > approach.
>
> Same. I always accumulate 100+ stashes. I never bother with a message
> when I stash. 90-95% of the stashes are useless cruft but there's always
> an experiment, a script file of some sort, which turns out to be useful.
OK, so editing the log message for each stash entry is already a use
case. In that case, I no longer object to the feature in principle.
At least, I cannot think of a way for it to encourage any bad
workflows.
The next step is for someone to review the implementation. I have
not read the implementation at all, and it will be quite a while
until I get to it (if I ever do).
>> Hmph, are you saying that you are going to wade through 211
>> stash entries one by one and reword them?
>
> Not me but my AI agent ;-)
I have no sympathy for you, then. Your AI agent should be able to
unstash and restash 211 entries one by one and reword them just fine
;-).
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH] stash: add 'rename' subcommand
2026-07-27 5:45 ` Junio C Hamano
@ 2026-07-27 9:18 ` Emin
0 siblings, 0 replies; 9+ messages in thread
From: Emin @ 2026-07-27 9:18 UTC (permalink / raw)
To: Junio C Hamano
Cc: erik88, brian m. carlson, Patrick Steinhardt,
Emin Özata via GitGitGadget, git, Greg Hewgill,
Micheil Smith, Michael Haggerty,
Ævar Arnfjörð Bjarmason
Junio C Hamano <gitster@pobox.com> writes:
> OK, so editing the log message for each stash entry is already a use
> case. In that case, I no longer object to the feature in principle.
Thanks, and thanks to brian and erik for describing the use case better
than I did in the original message. Mine is the same: entries pile up
with the default "WIP on <branch>" message, and the ones worth keeping
are the hardest to find again later.
I will send a v2 shortly that addresses Patrick's review. It renames
the subcommand to 'reword' and rewrites the reflog in a single
transaction.
Thanks.
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2] stash: add 'reword' subcommand
2026-07-16 8:31 [PATCH] stash: add 'rename' subcommand Emin Özata via GitGitGadget
2026-07-16 10:08 ` Patrick Steinhardt
@ 2026-07-27 10:54 ` Emin Özata via GitGitGadget
1 sibling, 0 replies; 9+ messages in thread
From: Emin Özata via GitGitGadget @ 2026-07-27 10:54 UTC (permalink / raw)
To: git; +Cc: Emin Özata, Emin Özata
From: =?UTF-8?q?Emin=20=C3=96zata?= <eminozata@proton.me>
Stash entries accumulate with the default "WIP on <branch>" message
when they are created in a hurry, and there is no way to relabel the
ones worth keeping afterwards. The only option is dropping an entry
and re-storing it by hand, which moves it to the top of the stash
list and gets fiddly for deeper entries.
Add 'git stash reword <message> [<stash>]', which defaults to the
latest entry like the other subcommands do. The name follows the
verb git already uses for changing only a message, as in the reword
command of 'git rebase -i' and in 'git history reword'. It reads
the whole reflog and writes it back in a single transaction, with
the new message on the target and every other entry left as it was.
Position, contents, timestamps and the reflog chain all stay put.
Before touching anything, the command inspects the target entry and
the ones above it, and refuses to start if any of them does not look
like a stash commit; that can only happen when refs/stash was
written to by hand. The rewrite clears the reflog before writing
the new one, so if the transaction fails afterwards, the command
reports the object id of each collected entry and re-stores it
best-effort. Whatever it fails to re-store can be recovered with
'git stash store'.
This was proposed before: in 2010, as a "git reflog update" command
that edited reflog entries in place [1]. When it came up again in
2013 [2], Junio rejected it on the grounds that reflogs are
append-only recovery logs, and that whoever really cares about a
stash message can pop and re-stash [3]. Michael Haggerty pointed
out in that thread that refs/stash does not fit the description:
its reflog is the primary data store for stash entries, and 'git
stash drop' rewrites it all the time [4]. So this patch rewrites
refs/stash directly, the way 'git stash drop' already does, through
ref_transaction_update_reflog(), which 'git remote rename' and 'git
refs migrate' already use to rewrite existing reflogs.
Name the target by index (stash@{1}); the command rejects
time-based selectors, because it needs the entry's position in the
reflog. Writing the reflog back at once keeps the cost linear in
its length whatever that position is, and spares the reftable
backend from emitting and compacting a table per entry, which is
what dropping and re-storing the entries one by one would cost.
[1] https://lore.kernel.org/git/20100620093142.GF24805@occam.hewgill.net/
[2] https://lore.kernel.org/git/loom.20130104T192132-16@post.gmane.org/
[3] https://lore.kernel.org/git/7vbod4tynt.fsf@alter.siamese.dyndns.org/
[4] https://lore.kernel.org/git/50ED2C78.1030300@alum.mit.edu/
Signed-off-by: Emin Özata <eminozata@proton.me>
---
stash: add 'reword' subcommand
eo/stash-reword
"git stash reword" learned to change the message of an existing stash
entry without changing its position, its contents or its reflog
timestamp.
This came up in 2010 and again in 2013, and was rejected back then on
the grounds that reflogs are append-only recovery logs. refs/stash is
the exception Michael Haggerty pointed out in that thread: its reflog is
the primary data store for stash entries, and "git stash drop" rewrites
it already. So this rewrites the refs/stash reflog through
ref_transaction_update_reflog(), which "git remote rename" and "git refs
migrate" already use to rewrite existing reflogs, and touches nothing
else. Details and links to the old threads are in the commit message.
Costs, so nobody has to dig for them: the whole reflog is read and
written back once whatever the target's depth, so the cost is linear in
its length. The rewrite is not crash-safe. The reflog is cleared before
the transaction writes it back, so a process killed in that window
leaves refs/stash without the reflog that "git stash list" reads. If the
transaction itself fails, the collected entries are re-stored
best-effort, and whatever cannot be written back is reported with its
object id so "git stash store" can recover it. The stash commits are
never deleted either way, so "git fsck" still finds them. Closing that
window for real needs a refs API operation that replaces a reflog in one
step, which I would rather do as a follow-up.
I picked a positional message argument over an -m option ("stash store"
style); no strong opinion, happy to switch.
Changes since v1, all from Patrick's review: the subcommand is 'reword'
rather than 'rename', the reflog is rewritten in a single transaction
instead of one drop-and-store per entry, and the commit message opens
with the use case rather than with the observation that stash messages
cannot be changed.
t3903 passes with GIT_TEST_DEFAULT_REF_FORMAT=files and reftable.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2180%2Fozemin%2Fstash-rename-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2180/ozemin/stash-rename-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2180
Range-diff vs v1:
1: 86af7ff53c ! 1: d9def7b461 stash: add 'rename' subcommand
@@ Metadata
Author: Emin Özata <eminozata@proton.me>
## Commit message ##
- stash: add 'rename' subcommand
+ stash: add 'reword' subcommand
- There is no way to change the message of a stash entry after the
- fact. The only option is dropping the entry and re-storing it by
- hand, which moves it to the top of the stash list and gets fiddly
- for deeper entries.
+ Stash entries accumulate with the default "WIP on <branch>" message
+ when they are created in a hurry, and there is no way to relabel the
+ ones worth keeping afterwards. The only option is dropping an entry
+ and re-storing it by hand, which moves it to the top of the stash
+ list and gets fiddly for deeper entries.
- Add 'git stash rename <message> [<stash>]', defaulting to the
- latest entry like the other subcommands do. It reads the object id
- and reflog message of the target entry and of the entries above it,
- drops them all like 'git stash drop' would, and stores them back in
- the same order, with the new message going to the target. Position,
- contents and the reflog chain stay as they were.
+ Add 'git stash reword <message> [<stash>]', which defaults to the
+ latest entry like the other subcommands do. The name follows the
+ verb git already uses for changing only a message, as in the reword
+ command of 'git rebase -i' and in 'git history reword'. It reads
+ the whole reflog and writes it back in a single transaction, with
+ the new message on the target and every other entry left as it was.
+ Position, contents, timestamps and the reflog chain all stay put.
- The command checks every entry it is about to rewrite and refuses
- to start if one of them does not look like a stash commit, which
- can only happen when refs/stash was written to by hand. Finding
- that out halfway through the sequence would lose entries. Should a
- write-back fail anyway, the entry's object id is reported so it can
- be recovered with 'git stash store', and the command only reports
- success when the reflog ended up in the requested state.
+ Before touching anything, the command inspects the target entry and
+ the ones above it, and refuses to start if any of them does not look
+ like a stash commit; that can only happen when refs/stash was
+ written to by hand. The rewrite clears the reflog before writing
+ the new one, so if the transaction fails afterwards, the command
+ reports the object id of each collected entry and re-stores it
+ best-effort. Whatever it fails to re-store can be recovered with
+ 'git stash store'.
This was proposed before: in 2010, as a "git reflog update" command
that edited reflog entries in place [1]. When it came up again in
@@ Commit message
stash message can pop and re-stash [3]. Michael Haggerty pointed
out in that thread that refs/stash does not fit the description:
its reflog is the primary data store for stash entries, and 'git
- stash drop' rewrites it all the time [4]. So this patch stays away
- from the reflog machinery entirely and does the suggested
- pop-and-re-stash workaround mechanically, without the detour
- through the working tree.
+ stash drop' rewrites it all the time [4]. So this patch rewrites
+ refs/stash directly, the way 'git stash drop' already does, through
+ ref_transaction_update_reflog(), which 'git remote rename' and 'git
+ refs migrate' already use to rewrite existing reflogs.
- The sequence only works if entry positions hold still while it
- runs, so the command takes index-based selectors (stash@{1}) and
- rejects time-based ones. It also refreshes the reflog timestamps
- of the rewritten entries, and renaming stash@{n} costs n+1 reflog
- deletions and ref updates.
+ Name the target by index (stash@{1}); the command rejects
+ time-based selectors, because it needs the entry's position in the
+ reflog. Writing the reflog back at once keeps the cost linear in
+ its length whatever that position is, and spares the reftable
+ backend from emitting and compacting a table per entry, which is
+ what dropping and re-storing the entries one by one would cost.
[1] https://lore.kernel.org/git/20100620093142.GF24805@occam.hewgill.net/
[2] https://lore.kernel.org/git/loom.20130104T192132-16@post.gmane.org/
@@ Documentation/git-stash.adoc: git stash create [<message>]
git stash store [(-m | --message) <message>] [-q | --quiet] <commit>
git stash export (--print | --to-ref <ref>) [<stash>...]
git stash import <commit>
-+git stash rename [-q | --quiet] <message> [<stash>]
++git stash reword [-q | --quiet] <message> [<stash>]
DESCRIPTION
-----------
@@ Documentation/git-stash.adoc: with no conflicts.
created by `export`, and add them to the list of stashes. To replace the
existing stashes, use `clear` first.
-+`rename [-q | --quiet] <message> [<stash>]`::
++`reword [-q | --quiet] <message> [<stash>]`::
+ Change the message of a single stash entry. The entry keeps its
-+ position and its contents. _<stash>_ must name an entry by
-+ index (e.g. `stash@{1}`); renaming refreshes the reflog
-+ timestamps of the entry and of the entries above it.
++ position, its contents and its reflog timestamp. _<stash>_ must
++ name an entry by index (e.g. `stash@{1}`).
+
OPTIONS
-------
@@ Documentation/git-stash.adoc: literally (including newlines and quotes).
`--quiet`::
This option is only valid for `apply`, `drop`, `pop`, `push`,
- `save`, `store` commands.
-+ `rename`, `save`, `store` commands.
++ `reword`, `save`, `store` commands.
+
Quiet, suppress feedback messages.
@@ Documentation/git-stash.adoc: For more details, see the 'pathspec' entry in link
_<stash>_::
This option is only valid for `apply`, `branch`, `drop`, `pop`,
- `show`, and `export` commands.
-+ `show`, `export`, and `rename` commands.
++ `show`, `export`, and `reword` commands.
+
A reference of the form `stash@{<revision>}`. When no _<stash>_ is
given, the latest stash is assumed (that is, `stash@{0}`).
## builtin/stash.c ##
+@@
+ #include "abspath.h"
+ #include "config.h"
+ #include "environment.h"
++#include "date.h"
+ #include "gettext.h"
+ #include "hash.h"
+ #include "hex.h"
++#include "ident.h"
+ #include "object-name.h"
+ #include "parse-options.h"
+ #include "refs.h"
@@
N_("git stash export (--print | --to-ref <ref>) [<stash>...]")
#define BUILTIN_STASH_IMPORT_USAGE \
N_("git stash import <commit>")
-+#define BUILTIN_STASH_RENAME_USAGE \
-+ N_("git stash rename [-q | --quiet] <message> [<stash>]")
++#define BUILTIN_STASH_REWORD_USAGE \
++ N_("git stash reword [-q | --quiet] <message> [<stash>]")
#define BUILTIN_STASH_CLEAR_USAGE \
"git stash clear"
@@ builtin/stash.c: static const char * const git_stash_usage[] = {
BUILTIN_STASH_STORE_USAGE,
BUILTIN_STASH_EXPORT_USAGE,
BUILTIN_STASH_IMPORT_USAGE,
-+ BUILTIN_STASH_RENAME_USAGE,
++ BUILTIN_STASH_REWORD_USAGE,
NULL
};
@@ builtin/stash.c: static const char * const git_stash_import_usage[] = {
NULL
};
-+static const char * const git_stash_rename_usage[] = {
-+ BUILTIN_STASH_RENAME_USAGE,
++static const char * const git_stash_reword_usage[] = {
++ BUILTIN_STASH_REWORD_USAGE,
+ NULL
+};
+
static const char ref_stash[] = "refs/stash";
static struct strbuf stash_index_path = STRBUF_INIT;
-@@ builtin/stash.c: static int reflog_is_empty(const char *refname)
- refname, reject_reflog_ent, NULL);
- }
-
--static int do_drop_stash(struct stash_info *info, int quiet)
-+static int drop_reflog_entry(const char *revision)
- {
-- if (!reflog_delete(info->revision.buf,
-- EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,
-- 0)) {
-- if (!quiet)
-- printf_ln(_("Dropped %s (%s)"), info->revision.buf,
-- oid_to_hex(&info->w_commit));
-- } else {
-- return error(_("%s: Could not drop stash entry"),
-- info->revision.buf);
-- }
-+ if (reflog_delete(revision,
-+ EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,
-+ 0))
-+ return error(_("%s: Could not drop stash entry"), revision);
-
- if (reflog_is_empty(ref_stash))
- do_clear_stash();
-@@ builtin/stash.c: static int do_drop_stash(struct stash_info *info, int quiet)
- return 0;
- }
-
-+static int do_drop_stash(struct stash_info *info, int quiet)
-+{
-+ if (drop_reflog_entry(info->revision.buf))
-+ return -1;
-+
-+ if (!quiet)
-+ printf_ln(_("Dropped %s (%s)"), info->revision.buf,
-+ oid_to_hex(&info->w_commit));
-+
-+ return 0;
-+}
-+
- static int get_stash_info_assert(struct stash_info *info, int argc,
- const char **argv)
- {
@@ builtin/stash.c: out:
return ret;
}
-+struct rename_entry {
-+ struct object_id oid;
++struct reword_entry {
++ struct object_id old_oid;
++ struct object_id new_oid;
++ char *committer;
+ char *msg;
+};
+
-+struct rename_data {
-+ struct rename_entry *entries;
++struct reword_data {
++ struct reword_entry *entries;
+ size_t nr, alloc;
-+ size_t want;
+};
+
-+static int collect_rename_entries(const char *refname UNUSED,
-+ struct object_id *old_oid UNUSED,
++static int collect_reword_entries(const char *refname UNUSED,
++ struct object_id *old_oid,
+ struct object_id *new_oid,
-+ const char *committer UNUSED,
-+ timestamp_t timestamp UNUSED,
-+ int tz UNUSED, const char *msg,
++ const char *committer,
++ timestamp_t timestamp,
++ int tz, const char *msg,
+ void *cb_data)
+{
-+ struct rename_data *data = cb_data;
++ struct reword_data *data = cb_data;
+ const char *eol = strchrnul(msg, '\n');
++ struct reword_entry *entry;
++ struct ident_split ident;
+
+ ALLOC_GROW(data->entries, data->nr + 1, data->alloc);
-+ oidcpy(&data->entries[data->nr].oid, new_oid);
-+ data->entries[data->nr].msg = xstrndup(msg, eol - msg);
-+ data->nr++;
++ entry = &data->entries[data->nr];
++ oidcpy(&entry->old_oid, old_oid);
++ oidcpy(&entry->new_oid, new_oid);
++ entry->msg = xstrndup(msg, eol - msg);
++
++ if (split_ident_line(&ident, committer, strlen(committer)) < 0) {
++ entry->committer = xstrdup(committer);
++ } else {
++ struct strbuf name = STRBUF_INIT, mail = STRBUF_INIT;
++ const char *date = show_date(timestamp, tz, DATE_MODE(NORMAL));
++
++ strbuf_add(&name, ident.name_begin,
++ ident.name_end - ident.name_begin);
++ strbuf_add(&mail, ident.mail_begin,
++ ident.mail_end - ident.mail_begin);
++ entry->committer = xstrdup(fmt_ident(name.buf, mail.buf,
++ WANT_BLANK_IDENT, date, 0));
++ strbuf_release(&name);
++ strbuf_release(&mail);
++ }
+
-+ return data->nr >= data->want;
++ data->nr++;
++ return 0;
+}
+
+static int parse_stash_index(const char *revision, size_t *idx)
@@ builtin/stash.c: out:
+ return 0;
+}
+
-+static int store_rename_entry(struct rename_entry *entry, const char *msg)
++static int do_reword_stash(struct stash_info *info, size_t idx,
++ const char *reworded_msg, int quiet)
+{
-+ if (!do_store_stash(&entry->oid, msg, 1))
-+ return 0;
-+ warning(_("could not restore stash entry %s; "
-+ "recover it with 'git stash store %s'"),
-+ oid_to_hex(&entry->oid), oid_to_hex(&entry->oid));
-+ return -1;
-+}
-+
-+static int do_rename_stash(struct stash_info *info, size_t idx,
-+ const char *msg, int quiet)
-+{
-+ struct rename_data data = { .want = idx + 1 };
-+ size_t i, missing = 0;
++ struct ref_store *refs = get_main_ref_store(the_repository);
++ struct ref_transaction *transaction = NULL;
++ struct reword_data data = { 0 };
++ struct strbuf err = STRBUF_INIT;
++ uint64_t index = 0;
++ size_t i;
+ int ret = -1;
+
-+ refs_for_each_reflog_ent_reverse(get_main_ref_store(the_repository),
-+ ref_stash, collect_rename_entries,
-+ &data);
++ refs_for_each_reflog_ent_reverse(refs, ref_stash,
++ collect_reword_entries, &data);
+ if (data.nr <= idx) {
+ error(_("%s does not exist"), info->revision.buf);
+ goto cleanup;
+ }
+
-+ if (!oideq(&info->w_commit, &data.entries[idx].oid)) {
++ if (!oideq(&info->w_commit, &data.entries[idx].new_oid)) {
+ error(_("%s changed concurrently; try again"),
+ info->revision.buf);
+ goto cleanup;
+ }
+
-+ /* refuse up front; do_store_stash() would die halfway through */
-+ for (i = 0; i < data.nr; i++) {
++ for (i = 0; i <= idx; i++) {
+ struct commit *stash = lookup_commit_reference(the_repository,
-+ &data.entries[i].oid);
++ &data.entries[i].new_oid);
+
+ if (!stash || check_stash_topology(the_repository, stash)) {
+ error(_("%s does not look like a stash commit"),
-+ oid_to_hex(&data.entries[i].oid));
++ oid_to_hex(&data.entries[i].new_oid));
+ goto cleanup;
+ }
+ }
+
-+ while (missing <= idx) {
-+ if (drop_reflog_entry("stash@{0}"))
-+ goto restore;
-+ missing++;
++ if (refs_delete_reflog(refs, ref_stash)) {
++ error(_("could not rewrite %s"), ref_stash);
++ goto cleanup;
+ }
+
-+ ret = 0;
-+ while (missing) {
-+ i = missing - 1;
-+ if (store_rename_entry(&data.entries[i],
-+ i == idx ? msg : data.entries[i].msg))
-+ ret = -1;
-+ missing--;
++ transaction = ref_store_transaction_begin(refs, 0, &err);
++ if (!transaction)
++ goto restore;
++
++ for (i = data.nr; i-- > 0; ) {
++ if (ref_transaction_update_reflog(transaction, ref_stash,
++ &data.entries[i].new_oid,
++ &data.entries[i].old_oid,
++ data.entries[i].committer,
++ i == idx ? reworded_msg :
++ data.entries[i].msg,
++ index++, &err))
++ goto restore;
+ }
+
-+ if (!ret && !quiet)
-+ printf_ln(_("Renamed %s (%s)"), info->revision.buf,
-+ oid_to_hex(&data.entries[idx].oid));
++ if (ref_transaction_commit(transaction, &err))
++ goto restore;
++
++ ret = 0;
++ if (!quiet)
++ printf_ln(_("Reworded %s (%s)"), info->revision.buf,
++ oid_to_hex(&data.entries[idx].new_oid));
+ goto cleanup;
+
+restore:
-+ /* dropping failed midway; put the dropped entries back */
-+ while (missing) {
-+ store_rename_entry(&data.entries[missing - 1],
-+ data.entries[missing - 1].msg);
-+ missing--;
-+ }
++ if (err.len)
++ error("%s", err.buf);
++ ref_transaction_free(transaction);
++ transaction = NULL;
++ for (i = data.nr; i-- > 0; )
++ if (do_store_stash(&data.entries[i].new_oid,
++ data.entries[i].msg, 1))
++ warning(_("could not restore stash entry %s; "
++ "recover it with 'git stash store %s'"),
++ oid_to_hex(&data.entries[i].new_oid),
++ oid_to_hex(&data.entries[i].new_oid));
+cleanup:
-+ for (i = 0; i < data.nr; i++)
++ ref_transaction_free(transaction);
++ strbuf_release(&err);
++ for (i = 0; i < data.nr; i++) {
++ free(data.entries[i].committer);
+ free(data.entries[i].msg);
++ }
+ free(data.entries);
+ return ret;
+}
+
-+static int rename_stash(int argc, const char **argv, const char *prefix,
++static int reword_stash(int argc, const char **argv, const char *prefix,
+ struct repository *repo UNUSED)
+{
+ int ret = -1;
@@ builtin/stash.c: out:
+ };
+
+ argc = parse_options(argc, argv, prefix, options,
-+ git_stash_rename_usage, 0);
++ git_stash_reword_usage, 0);
+
+ if (!argc)
-+ usage_with_options(git_stash_rename_usage, options);
++ usage_with_options(git_stash_reword_usage, options);
+
+ if (!argv[0][strspn(argv[0], " \t\r\n")]) {
+ ret = error(_("stash message cannot be empty"));
@@ builtin/stash.c: out:
+ if (get_stash_info_assert(&info, argc - 1, argv + 1))
+ goto cleanup;
+
-+ /* positions must stay stable across the drop-and-store sequence */
+ if (parse_stash_index(info.revision.buf, &idx)) {
-+ error(_("cannot rename '%s': name the entry by index, "
++ error(_("cannot reword '%s': name the entry by index, "
+ "like 'stash@{1}'"), info.revision.buf);
+ goto cleanup;
+ }
+
-+ ret = do_rename_stash(&info, idx, argv[0], quiet);
++ ret = do_reword_stash(&info, idx, argv[0], quiet);
+cleanup:
+ free_stash_info(&info);
+ return ret;
@@ builtin/stash.c: int cmd_stash(int argc,
OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
OPT_SUBCOMMAND("export", &fn, export_stash),
OPT_SUBCOMMAND("import", &fn, import_stash),
-+ OPT_SUBCOMMAND("rename", &fn, rename_stash),
++ OPT_SUBCOMMAND("reword", &fn, reword_stash),
OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
OPT_END()
};
@@ contrib/completion/git-completion.bash: _git_sparse_checkout ()
_git_stash ()
{
- local subcommands='push list show apply clear drop pop create branch import export'
-+ local subcommands='push list show apply clear drop pop create branch import export rename'
++ local subcommands='push list show apply clear drop pop create branch import export reword'
local subcommand="$(__git_find_on_cmdline "$subcommands save")"
if [ -z "$subcommand" ]; then
@@ contrib/completion/git-completion.bash: _git_stash ()
__git_complete_refs
;;
- show,*|apply,*|drop,*|pop,*|export,*)
-+ show,*|apply,*|drop,*|pop,*|export,*|rename,*)
++ show,*|apply,*|drop,*|pop,*|export,*|reword,*)
__gitcomp_nl "$(__git stash list \
| sed -n -e 's/:.*//p')"
;;
@@ t/t3903-stash.sh: test_expect_success 'stash show --include-untracked includes u
test_grep "untracked" actual
'
-+test_expect_success 'rename a stash entry' '
++test_expect_success 'reword a stash entry' '
+ git stash clear &&
-+ >file-to-rename &&
-+ git add file-to-rename &&
++ >file-to-reword &&
++ git add file-to-reword &&
+ git stash push -m "original message" &&
-+ git stash rename "new message" stash@{0} >out &&
-+ test_grep "Renamed stash@{0}" out &&
++ git stash reword "new message" stash@{0} >out &&
++ test_grep "Reworded stash@{0}" out &&
+ git stash list >list &&
+ test_grep "stash@{0}: new message" list &&
+ test_grep ! "original message" list
+'
+
-+test_expect_success 'rename defaults to the latest stash entry' '
-+ git stash rename "default target" >out &&
-+ test_grep "Renamed refs/stash@{0}" out &&
++test_expect_success 'reword defaults to the latest stash entry' '
++ git stash reword "default target" >out &&
++ test_grep "Reworded refs/stash@{0}" out &&
+ git stash list >list &&
+ test_grep "stash@{0}: default target" list
+'
+
-+test_expect_success 'rename a deeper stash entry keeps positions and states' '
++test_expect_success 'reword a deeper stash entry keeps positions and states' '
+ git stash clear &&
+ for i in 1 2 3
+ do
@@ t/t3903-stash.sh: test_expect_success 'stash show --include-untracked includes u
+ git stash push -m "message $i" || return 1
+ done &&
+ git rev-parse stash@{0} stash@{1} stash@{2} >expect &&
-+ git stash rename "renamed middle" stash@{1} &&
++ git stash reword "reworded middle" stash@{1} &&
+ git rev-parse stash@{0} stash@{1} stash@{2} >actual &&
+ test_cmp expect actual &&
+ git stash list >list &&
+ test_grep "stash@{0}: On.*message 3" list &&
-+ test_grep "stash@{1}: renamed middle" list &&
++ test_grep "stash@{1}: reworded middle" list &&
+ test_grep "stash@{2}: On.*message 1" list
+'
+
-+test_expect_success 'rename the deepest stash entry' '
++test_expect_success 'reword the deepest stash entry' '
+ git rev-parse stash@{0} stash@{1} stash@{2} >expect &&
-+ git stash rename "renamed deepest" stash@{2} &&
++ git stash reword "reworded deepest" stash@{2} &&
+ git rev-parse stash@{0} stash@{1} stash@{2} >actual &&
+ test_cmp expect actual &&
+ git stash list >list &&
-+ test_grep "stash@{2}: renamed deepest" list
++ test_grep "stash@{2}: reworded deepest" list
+'
+
-+test_expect_success 'rename accepts a bare index and honors --quiet' '
-+ git stash rename -q "quietly renamed" 1 >out &&
++test_expect_success 'reword accepts a bare index and honors --quiet' '
++ git stash reword -q "quietly reworded" 1 >out &&
+ test_must_be_empty out &&
+ git stash list >list &&
-+ test_grep "stash@{1}: quietly renamed" list
++ test_grep "stash@{1}: quietly reworded" list
+'
+
-+test_expect_success 'rename rejects bad arguments' '
-+ test_must_fail git stash rename "no such entry" stash@{99} &&
-+ test_must_fail git stash rename "" &&
-+ test_must_fail git stash rename " " &&
-+ test_must_fail git stash rename "not a stash" HEAD &&
-+ test_must_fail git stash rename "not an index" "stash@{now}" &&
-+ test_expect_code 129 git stash rename &&
++test_expect_success 'reword rejects bad arguments' '
++ test_must_fail git stash reword "no such entry" stash@{99} &&
++ test_must_fail git stash reword "" &&
++ test_must_fail git stash reword " " &&
++ test_must_fail git stash reword "not a stash" HEAD &&
++ test_must_fail git stash reword "not an index" "stash@{now}" &&
++ test_expect_code 129 git stash reword &&
+ git stash list >list &&
-+ test_grep "stash@{1}: quietly renamed" list
++ test_grep "stash@{1}: quietly reworded" list
+'
+
-+test_expect_success 'rename refuses to rewrite a non-stash reflog entry' '
++test_expect_success 'reword refuses to rewrite a non-stash reflog entry' '
+ git stash clear &&
+ >real-a &&
+ git add real-a &&
@@ t/t3903-stash.sh: test_expect_success 'stash show --include-untracked includes u
+ git add real-b &&
+ git stash push -m "real B" &&
+ git stash list >expect &&
-+ test_must_fail git stash rename "renamed A" stash@{2} &&
++ test_must_fail git stash reword "reworded A" stash@{2} &&
+ git stash list >actual &&
+ test_cmp expect actual
+'
Documentation/git-stash.adoc | 10 +-
builtin/stash.c | 201 +++++++++++++++++++++++++
contrib/completion/git-completion.bash | 4 +-
t/t3903-stash.sh | 79 ++++++++++
4 files changed, 290 insertions(+), 4 deletions(-)
diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc
index 50bb89f483..16a2a015f3 100644
--- a/Documentation/git-stash.adoc
+++ b/Documentation/git-stash.adoc
@@ -25,6 +25,7 @@ git stash create [<message>]
git stash store [(-m | --message) <message>] [-q | --quiet] <commit>
git stash export (--print | --to-ref <ref>) [<stash>...]
git stash import <commit>
+git stash reword [-q | --quiet] <message> [<stash>]
DESCRIPTION
-----------
@@ -163,6 +164,11 @@ with no conflicts.
created by `export`, and add them to the list of stashes. To replace the
existing stashes, use `clear` first.
+`reword [-q | --quiet] <message> [<stash>]`::
+ Change the message of a single stash entry. The entry keeps its
+ position, its contents and its reflog timestamp. _<stash>_ must
+ name an entry by index (e.g. `stash@{1}`).
+
OPTIONS
-------
`-a`::
@@ -258,7 +264,7 @@ literally (including newlines and quotes).
`-q`::
`--quiet`::
This option is only valid for `apply`, `drop`, `pop`, `push`,
- `save`, `store` commands.
+ `reword`, `save`, `store` commands.
+
Quiet, suppress feedback messages.
@@ -292,7 +298,7 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
_<stash>_::
This option is only valid for `apply`, `branch`, `drop`, `pop`,
- `show`, and `export` commands.
+ `show`, `export`, and `reword` commands.
+
A reference of the form `stash@{<revision>}`. When no _<stash>_ is
given, the latest stash is assumed (that is, `stash@{0}`).
diff --git a/builtin/stash.c b/builtin/stash.c
index c4809f299a..61471cf510 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -4,9 +4,11 @@
#include "abspath.h"
#include "config.h"
#include "environment.h"
+#include "date.h"
#include "gettext.h"
#include "hash.h"
#include "hex.h"
+#include "ident.h"
#include "object-name.h"
#include "parse-options.h"
#include "refs.h"
@@ -63,6 +65,8 @@
N_("git stash export (--print | --to-ref <ref>) [<stash>...]")
#define BUILTIN_STASH_IMPORT_USAGE \
N_("git stash import <commit>")
+#define BUILTIN_STASH_REWORD_USAGE \
+ N_("git stash reword [-q | --quiet] <message> [<stash>]")
#define BUILTIN_STASH_CLEAR_USAGE \
"git stash clear"
@@ -80,6 +84,7 @@ static const char * const git_stash_usage[] = {
BUILTIN_STASH_STORE_USAGE,
BUILTIN_STASH_EXPORT_USAGE,
BUILTIN_STASH_IMPORT_USAGE,
+ BUILTIN_STASH_REWORD_USAGE,
NULL
};
@@ -143,6 +148,11 @@ static const char * const git_stash_import_usage[] = {
NULL
};
+static const char * const git_stash_reword_usage[] = {
+ BUILTIN_STASH_REWORD_USAGE,
+ NULL
+};
+
static const char ref_stash[] = "refs/stash";
static struct strbuf stash_index_path = STRBUF_INIT;
@@ -1190,6 +1200,196 @@ out:
return ret;
}
+struct reword_entry {
+ struct object_id old_oid;
+ struct object_id new_oid;
+ char *committer;
+ char *msg;
+};
+
+struct reword_data {
+ struct reword_entry *entries;
+ size_t nr, alloc;
+};
+
+static int collect_reword_entries(const char *refname UNUSED,
+ struct object_id *old_oid,
+ struct object_id *new_oid,
+ const char *committer,
+ timestamp_t timestamp,
+ int tz, const char *msg,
+ void *cb_data)
+{
+ struct reword_data *data = cb_data;
+ const char *eol = strchrnul(msg, '\n');
+ struct reword_entry *entry;
+ struct ident_split ident;
+
+ ALLOC_GROW(data->entries, data->nr + 1, data->alloc);
+ entry = &data->entries[data->nr];
+ oidcpy(&entry->old_oid, old_oid);
+ oidcpy(&entry->new_oid, new_oid);
+ entry->msg = xstrndup(msg, eol - msg);
+
+ if (split_ident_line(&ident, committer, strlen(committer)) < 0) {
+ entry->committer = xstrdup(committer);
+ } else {
+ struct strbuf name = STRBUF_INIT, mail = STRBUF_INIT;
+ const char *date = show_date(timestamp, tz, DATE_MODE(NORMAL));
+
+ strbuf_add(&name, ident.name_begin,
+ ident.name_end - ident.name_begin);
+ strbuf_add(&mail, ident.mail_begin,
+ ident.mail_end - ident.mail_begin);
+ entry->committer = xstrdup(fmt_ident(name.buf, mail.buf,
+ WANT_BLANK_IDENT, date, 0));
+ strbuf_release(&name);
+ strbuf_release(&mail);
+ }
+
+ data->nr++;
+ return 0;
+}
+
+static int parse_stash_index(const char *revision, size_t *idx)
+{
+ const char *num = strstr(revision, "@{");
+ char *end;
+
+ if (!num || !isdigit(num[2]))
+ return -1;
+ *idx = strtoumax(num + 2, &end, 10);
+ if (*end != '}' || end[1])
+ return -1;
+
+ return 0;
+}
+
+static int do_reword_stash(struct stash_info *info, size_t idx,
+ const char *reworded_msg, int quiet)
+{
+ struct ref_store *refs = get_main_ref_store(the_repository);
+ struct ref_transaction *transaction = NULL;
+ struct reword_data data = { 0 };
+ struct strbuf err = STRBUF_INIT;
+ uint64_t index = 0;
+ size_t i;
+ int ret = -1;
+
+ refs_for_each_reflog_ent_reverse(refs, ref_stash,
+ collect_reword_entries, &data);
+ if (data.nr <= idx) {
+ error(_("%s does not exist"), info->revision.buf);
+ goto cleanup;
+ }
+
+ if (!oideq(&info->w_commit, &data.entries[idx].new_oid)) {
+ error(_("%s changed concurrently; try again"),
+ info->revision.buf);
+ goto cleanup;
+ }
+
+ for (i = 0; i <= idx; i++) {
+ struct commit *stash = lookup_commit_reference(the_repository,
+ &data.entries[i].new_oid);
+
+ if (!stash || check_stash_topology(the_repository, stash)) {
+ error(_("%s does not look like a stash commit"),
+ oid_to_hex(&data.entries[i].new_oid));
+ goto cleanup;
+ }
+ }
+
+ if (refs_delete_reflog(refs, ref_stash)) {
+ error(_("could not rewrite %s"), ref_stash);
+ goto cleanup;
+ }
+
+ transaction = ref_store_transaction_begin(refs, 0, &err);
+ if (!transaction)
+ goto restore;
+
+ for (i = data.nr; i-- > 0; ) {
+ if (ref_transaction_update_reflog(transaction, ref_stash,
+ &data.entries[i].new_oid,
+ &data.entries[i].old_oid,
+ data.entries[i].committer,
+ i == idx ? reworded_msg :
+ data.entries[i].msg,
+ index++, &err))
+ goto restore;
+ }
+
+ if (ref_transaction_commit(transaction, &err))
+ goto restore;
+
+ ret = 0;
+ if (!quiet)
+ printf_ln(_("Reworded %s (%s)"), info->revision.buf,
+ oid_to_hex(&data.entries[idx].new_oid));
+ goto cleanup;
+
+restore:
+ if (err.len)
+ error("%s", err.buf);
+ ref_transaction_free(transaction);
+ transaction = NULL;
+ for (i = data.nr; i-- > 0; )
+ if (do_store_stash(&data.entries[i].new_oid,
+ data.entries[i].msg, 1))
+ warning(_("could not restore stash entry %s; "
+ "recover it with 'git stash store %s'"),
+ oid_to_hex(&data.entries[i].new_oid),
+ oid_to_hex(&data.entries[i].new_oid));
+cleanup:
+ ref_transaction_free(transaction);
+ strbuf_release(&err);
+ for (i = 0; i < data.nr; i++) {
+ free(data.entries[i].committer);
+ free(data.entries[i].msg);
+ }
+ free(data.entries);
+ return ret;
+}
+
+static int reword_stash(int argc, const char **argv, const char *prefix,
+ struct repository *repo UNUSED)
+{
+ int ret = -1;
+ int quiet = 0;
+ size_t idx;
+ struct stash_info info = STASH_INFO_INIT;
+ struct option options[] = {
+ OPT__QUIET(&quiet, N_("be quiet, only report errors")),
+ OPT_END()
+ };
+
+ argc = parse_options(argc, argv, prefix, options,
+ git_stash_reword_usage, 0);
+
+ if (!argc)
+ usage_with_options(git_stash_reword_usage, options);
+
+ if (!argv[0][strspn(argv[0], " \t\r\n")]) {
+ ret = error(_("stash message cannot be empty"));
+ goto cleanup;
+ }
+
+ if (get_stash_info_assert(&info, argc - 1, argv + 1))
+ goto cleanup;
+
+ if (parse_stash_index(info.revision.buf, &idx)) {
+ error(_("cannot reword '%s': name the entry by index, "
+ "like 'stash@{1}'"), info.revision.buf);
+ goto cleanup;
+ }
+
+ ret = do_reword_stash(&info, idx, argv[0], quiet);
+cleanup:
+ free_stash_info(&info);
+ return ret;
+}
+
static void add_pathspecs(struct strvec *args,
const struct pathspec *ps) {
int i;
@@ -2472,6 +2672,7 @@ int cmd_stash(int argc,
OPT_SUBCOMMAND("push", &fn, push_stash_unassumed),
OPT_SUBCOMMAND("export", &fn, export_stash),
OPT_SUBCOMMAND("import", &fn, import_stash),
+ OPT_SUBCOMMAND("reword", &fn, reword_stash),
OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE),
OPT_END()
};
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index e875787710..261c6bf101 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -3465,7 +3465,7 @@ _git_sparse_checkout ()
_git_stash ()
{
- local subcommands='push list show apply clear drop pop create branch import export'
+ local subcommands='push list show apply clear drop pop create branch import export reword'
local subcommand="$(__git_find_on_cmdline "$subcommands save")"
if [ -z "$subcommand" ]; then
@@ -3508,7 +3508,7 @@ _git_stash ()
import,*)
__git_complete_refs
;;
- show,*|apply,*|drop,*|pop,*|export,*)
+ show,*|apply,*|drop,*|pop,*|export,*|reword,*)
__gitcomp_nl "$(__git stash list \
| sed -n -e 's/:.*//p')"
;;
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index ecc35aae82..07fbbddac8 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -1831,4 +1831,83 @@ test_expect_success 'stash show --include-untracked includes untracked files' '
test_grep "untracked" actual
'
+test_expect_success 'reword a stash entry' '
+ git stash clear &&
+ >file-to-reword &&
+ git add file-to-reword &&
+ git stash push -m "original message" &&
+ git stash reword "new message" stash@{0} >out &&
+ test_grep "Reworded stash@{0}" out &&
+ git stash list >list &&
+ test_grep "stash@{0}: new message" list &&
+ test_grep ! "original message" list
+'
+
+test_expect_success 'reword defaults to the latest stash entry' '
+ git stash reword "default target" >out &&
+ test_grep "Reworded refs/stash@{0}" out &&
+ git stash list >list &&
+ test_grep "stash@{0}: default target" list
+'
+
+test_expect_success 'reword a deeper stash entry keeps positions and states' '
+ git stash clear &&
+ for i in 1 2 3
+ do
+ >file$i &&
+ git add file$i &&
+ git stash push -m "message $i" || return 1
+ done &&
+ git rev-parse stash@{0} stash@{1} stash@{2} >expect &&
+ git stash reword "reworded middle" stash@{1} &&
+ git rev-parse stash@{0} stash@{1} stash@{2} >actual &&
+ test_cmp expect actual &&
+ git stash list >list &&
+ test_grep "stash@{0}: On.*message 3" list &&
+ test_grep "stash@{1}: reworded middle" list &&
+ test_grep "stash@{2}: On.*message 1" list
+'
+
+test_expect_success 'reword the deepest stash entry' '
+ git rev-parse stash@{0} stash@{1} stash@{2} >expect &&
+ git stash reword "reworded deepest" stash@{2} &&
+ git rev-parse stash@{0} stash@{1} stash@{2} >actual &&
+ test_cmp expect actual &&
+ git stash list >list &&
+ test_grep "stash@{2}: reworded deepest" list
+'
+
+test_expect_success 'reword accepts a bare index and honors --quiet' '
+ git stash reword -q "quietly reworded" 1 >out &&
+ test_must_be_empty out &&
+ git stash list >list &&
+ test_grep "stash@{1}: quietly reworded" list
+'
+
+test_expect_success 'reword rejects bad arguments' '
+ test_must_fail git stash reword "no such entry" stash@{99} &&
+ test_must_fail git stash reword "" &&
+ test_must_fail git stash reword " " &&
+ test_must_fail git stash reword "not a stash" HEAD &&
+ test_must_fail git stash reword "not an index" "stash@{now}" &&
+ test_expect_code 129 git stash reword &&
+ git stash list >list &&
+ test_grep "stash@{1}: quietly reworded" list
+'
+
+test_expect_success 'reword refuses to rewrite a non-stash reflog entry' '
+ git stash clear &&
+ >real-a &&
+ git add real-a &&
+ git stash push -m "real A" &&
+ git update-ref -m junk --create-reflog refs/stash HEAD &&
+ >real-b &&
+ git add real-b &&
+ git stash push -m "real B" &&
+ git stash list >expect &&
+ test_must_fail git stash reword "reworded A" stash@{2} &&
+ git stash list >actual &&
+ test_cmp expect actual
+'
+
test_done
base-commit: 55526a18268bbc1ddaf8a6b7850c33d984eac9e9
--
gitgitgadget
^ permalink raw reply related [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-07-27 10:54 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-16 8:31 [PATCH] stash: add 'rename' subcommand Emin Özata via GitGitGadget
2026-07-16 10:08 ` Patrick Steinhardt
2026-07-16 17:46 ` Junio C Hamano
2026-07-16 21:08 ` brian m. carlson
2026-07-17 1:11 ` Junio C Hamano
2026-07-26 9:00 ` erik88
2026-07-27 5:45 ` Junio C Hamano
2026-07-27 9:18 ` Emin
2026-07-27 10:54 ` [PATCH v2] stash: add 'reword' subcommand Emin Özata via GitGitGadget
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.