02 Sep, 2025
Renaming a Git Stash
Or, what's a stash anyway?
Even though multiple work trees seem to be all the rage these days, I still work with plain old Git stashes. Most of the time I push work to the stash when some other urgent task comes up that demands my immediate attention. Other times I save my work before rebasing the current branch onto a different branch and want to apply the changes I'm about to introduce afterward in a safe manner.
The default stash message as recorded by Git looks something like this:
WIP on main: 66ca91c Add navbar
The record contains the following information: (1) the branch the commit belongs to,
(2) the hash of said commit, as well as (3) its commit message. The missing
piece here is some context about the contents of the actual stash that we created. The message itself tells us
approximately when we did some work, but not what kind. We have a few things at our disposal to help us out here.
There's the -m | --message
flag which allows us to provide an alternative message. Then there's
one of Git's less loved features: Git notes. Let's for now pretend we don't need something as fully fledged as
Git notes, but instead prefer to stick with Git's plain stash messages. What if we created a stash using either
the not-so-informative default message described above or one that we came up with ourselves, but would like to
change the message at a later point in time? The naive but obvious-seeming answer here would be simply to
recreate the stash, providing a different commit message. While this might work if we realize our mistake right
away, we might as well just do that. But what if we're in the midst of some nasty rebase or weeding out some
conflicts that appeared while merging a different branch? Simply popping the stash could just leave us with more
conflicts and is thus not a viable option. Understanding what exactly a Git stash is helps us find a way to
safely rename a stash—a feature that is miraculously still missing.
But what exactly is a stash?
Without diving into a full explanation of how Git in its entirety works, in order to understand what exactly a
stash is, it's necessary to at least grasp what might be the most extensively used data structure internally: a
Git tree (object).
An object containing a list of file names and modes along with refs to the associated blob and/or tree objects.
A tree is equivalent to a directory.
disambiguate_treeish_only
shows exactly that.
f2c66ed196
.
e36adf7122
when it was replaced by a new file written in C. The script was
kept for backwards compatibility for another year until finally being removed entirely in commit
8a2cd3f512
.
Skipping fast-forward to today, we find a file called stash.c
in builtin
. After looking
around, the function that should be of immediate interest can be found with the revealing name
do_create_stash
, which can be found inside create_stash
and do_push_stash
.
For anyone who has used stashes before, these names should sound quite familiar as they're almost the same as their
user-facing command line counterparts.
I've highlighted the three most important parts of the do_create_stash
function.
static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
int only_staged, struct stash_info *info, struct strbuf *patch,
int quiet)
{
int ret = 0;
int flags = 0;
int untracked_commit_option = 0;
const char *head_short_sha1 = NULL;
const char *branch_ref = NULL;
const char *branch_name = "(no branch)";
char *branch_name_buf = NULL;
struct commit *head_commit = NULL;
struct commit_list *parents = NULL;
struct strbuf msg = STRBUF_INIT;
struct strbuf commit_tree_label = STRBUF_INIT;
struct strbuf untracked_files = STRBUF_INIT;
prepare_fallback_ident("git stash", "git@stash");
repo_read_index_preload(the_repository, NULL, 0);
if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, 0, 0,
NULL, NULL, NULL) < 0) {
ret = error(_("could not write index"));
goto done;
}
if (repo_get_oid(the_repository, "HEAD", &info->b_commit)) {
if (!quiet)
fprintf_ln(stderr, _("You do not have "
"the initial commit yet"));
ret = -1;
goto done;
} else {
head_commit = lookup_commit(the_repository, &info->b_commit);
}
if (!check_changes(ps, include_untracked, &untracked_files)) {
ret = 1;
goto done;
}
branch_ref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
"HEAD", 0, NULL, &flags);
if (flags & REF_ISSYMREF) {
if (skip_prefix(branch_ref, "refs/heads/", &branch_name))
branch_name = branch_name_buf = xstrdup(branch_name);
}
head_short_sha1 = repo_find_unique_abbrev(the_repository,
&head_commit->object.oid,
DEFAULT_ABBREV);
strbuf_addf(&msg, "%s: %s ", branch_name, head_short_sha1);
pp_commit_easy(CMIT_FMT_ONELINE, head_commit, &msg);
strbuf_addf(&commit_tree_label, "index on %s\n", msg.buf);
commit_list_insert(head_commit, &parents);
if (write_index_as_tree(&info->i_tree, the_repository->index,
repo_get_index_file(the_repository), 0, NULL) ||
commit_tree(commit_tree_label.buf, commit_tree_label.len,
&info->i_tree, parents, &info->i_commit, NULL, NULL)) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
"index state"));
ret = -1;
goto done;
}
free_commit_list(parents);
parents = NULL;
if (include_untracked) {
if (save_untracked_files(info, &msg, untracked_files)) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save "
"the untracked files"));
ret = -1;
goto done;
}
untracked_commit_option = 1;
}
if (patch_mode) {
ret = stash_patch(info, ps, patch, quiet, add_p_opt);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
"worktree state"));
goto done;
} else if (ret > 0) {
goto done;
}
} else if (only_staged) {
ret = stash_staged(info, patch, quiet);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
"staged state"));
goto done;
} else if (ret > 0) {
goto done;
}
} else if (stash_working_tree(info, ps)) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
"worktree state"));
ret = -1;
goto done;
}
if (!stash_msg_buf->len)
strbuf_addf(stash_msg_buf, "WIP on %s", msg.buf);
else
strbuf_insertf(stash_msg_buf, 0, "On %s: ", branch_name);
if (untracked_commit_option)
commit_list_insert(lookup_commit(the_repository,
&info->u_commit),
&parents);
commit_list_insert(lookup_commit(the_repository, &info->i_commit),
&parents);
commit_list_insert(head_commit, &parents);
if (commit_tree(stash_msg_buf->buf, stash_msg_buf->len, &info->w_tree,
parents, &info->w_commit, NULL, NULL)) {
if (!quiet)
fprintf_ln(stderr, _("Cannot record "
"working tree state"));
ret = -1;
goto done;
}
done:
strbuf_release(&commit_tree_label);
strbuf_release(&msg);
strbuf_release(&untracked_files);
free_commit_list(parents);
free(branch_name_buf);
return ret;
}
The first thing happening is the creation of the current index state as a Git tree and the subsequent commit of said tree:
if (write_index_as_tree(&info->i_tree, the_repository->index,
repo_get_index_file(the_repository), 0, NULL) ||
commit_tree(commit_tree_label.buf, commit_tree_label.len,
&info->i_tree, parents, &info->i_commit, NULL, NULL)) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
"index state"));
ret = -1;
goto done;
}
Not quite as readable as the original, but the underlying logic still remains the same.
write_index_as_tree
creates a tree object from the current changes contained in the index. Then a bit
further down (if no other options were provided), the actual working directory, including the index state,
gets handled in the same manner.
The name stash_working_tree
seems confusing at first, but what it
actually does is create a tree that represents the complete working directory state (staged + unstaged changes)
relative to the base commit. It starts with the index tree as a baseline and then applies all
working directory changes on top of it. The previously created index tree is reused here together with the
actual state of the working directory to create another tree called the working tree. This two-tree approach allows
Git to preserve which changes were staged versus unstaged, enabling commands like git stash pop --index
to
restore the exact repository state.
} else if (stash_working_tree(info, ps)) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
"worktree state"));
ret = -1;
goto done;
}
Finally, another call to commit_tree
creates the actual commit object containing this tree that we work
with when we run commands like git show stash
:
if (commit_tree(stash_msg_buf->buf, stash_msg_buf->len, &info->w_tree,
parents, &info->w_commit, NULL, NULL)) {
if (!quiet)
fprintf_ln(stderr, _("Cannot record "
"working tree state"));
ret = -1;
goto done;
}
This final commit becomes the actual Git stash entry—a merge commit with multiple parents. It points to both the base commit (HEAD when the stash was created) and the index commit we created earlier, while its tree represents the complete working directory state. This structure allows Git to reconstruct both what was staged and what was unstaged when you later apply the stash.
How Are Stash Messages Displayed?
Now that we have a proper understanding of what exactly constitutes that what is commonly referred to as a stash, we're ready to move on to tackle the initial problem of reliably renaming a Git stash. The most important thing to consider here above all is the way we're actually viewing a given stash's message. First, we would have to determine if we work with Git using the command line, a GUI, or both. Some GUI clients display a list of stashes together with the messages contained in the actual stash commit. The Git command line interface, in comparison, uses the message stored in the stash reflog instead. So it's necessary to pay attention to rename both instances in order to reliably display the new message in different contexts. The actual implementation is then quite straightforward:
- Get hold of the actual stash we wish to rename
- Obtain the underlying tree and the parent commits as explained above
- Create a new commit using the same metadata (author, committer, etc.) using said tree and parents, but provide a new commit message
- Take care to update the stash reflog with the new commit as well
The end result might then look like this when using the CLI:
max@Maximilians-MacBook-Air ~/home/dev/maxh.site (main) $ git stash list
stash@{0}: Normalize source paths (redundant after commit 2fae968)
stash@{1}: WIP on main: 2fae968 Remove trailing slash from directory macro directive
stash@{2}: HTML headers (security, caching etc.)
For a full example, see the following implementation.
Last Updated on 2025-09-12