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). From now on referred to as tree. Quoting from the gitglossary man page, 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. Interestingly, besides the plain term tree, another one often encountered and used throughout the man pages of various Git commands is the notion of a tree-ish object. The Git kernel documentation defines this basically as anything that can be dereferenced to a Git object. And indeed, looking at disambiguate_treeish_only shows exactly that. While the man page for the stash command serves as a great first clue, it's usually best to inspect the source code to gain a full understanding of how things actually work under the hood. Similar to the rebase command, the stash command evolved from a simple Bash script that was introduced with commit f2c66ed196 . Quite surprisingly though, the stash functionality was contained in said shell script for almost 15 years until commit 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:

  1. Get hold of the actual stash we wish to rename
  2. Obtain the underlying tree and the parent commits as explained above
  3. Create a new commit using the same metadata (author, committer, etc.) using said tree and parents, but provide a new commit message
  4. 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