Actually, git stash
does save the current work-tree. The problem is that the way it saves it is not directly suitable to your needs. There is also a secondary problem, which may be a very nasty problem but may instead be quite minor. See the caveat below. But in the end, you may be able to use git stash
to do what you want.
What to know
First, remember that Git does not make commits from your work-tree (your working directory) at all—and that commits are complete snapshots of all files. They are snapshots that are made from the index, not from the work-tree. The files in a new commit are those that are in the index right now.
(Remember also that the index is that thing that other parts of Git call the staging area, or sometimes the cache. It holds one copy of every file that will go into the next commit. That copy is initially the copy taken from the current commit, except for some edge cases noted in Checkout another branch when there are uncommitted changes on the current branch.)
If your work-tree differs from your index, and you want to snapshot your work-tree, you need to git add
each file, overwriting the copy in the index, before you can commit this. This, of course, destroys any careful staging you have done in your index.
But that's why git stash
really makes two commits:
- One commit saves your current index state, as a new commit that is not on any branch. Now it's safe to destroy the index state.
- The second commit saves your current work-tree, as a commit with two parents: the index commit, and the current commit. To get that commit made, Git replaces all of the files in the index with their work-tree variants (because Git makes commits from the index, not from the work-tree).1
(There's actually a third optional commit to hold untracked or untracked-plus-ignored files. If that commit exists, it is the third parent of the work-tree commit. Usually it just doesn't exist though.)
Having made these two commits, git stash
updates refs/stash
to remember the work-tree commit w
's hash ID. That commit remembers the index commit i
's hash ID, as well as the current commit's hash ID:
...--o--o--T <-- your-branch (HEAD)
\ |\
\ i-w <-- refs/stash
\
o--A <-- b
Then git stash
runs git reset --hard
, so that your index and work-tree go back to matching commit T
. I've highlighted one other commit A
, as pointed-to by some other branch b
.
1Technically, git stash
makes commit w
using a second, auxiliary / temporary index, just in case something goes wrong. It can just abandon the temporary index in that case. Making index commit i
is very easy though, as the plumbing command git write-tree
does all the work.
Making use of Git's stash commit
Remember the caveat here: git stash
essentially just does git add
on all the files that are already in the index. Any untracked files, including any untracked-and-ignored files, aren't in commit w
at all. They're just sitting in your work-tree. That's true even if, had you done git checkout A
to get to commit A
, some of those files would have been copied into your index. (Of course in this case you would generally have seen a complaint that Git needed to overwrite some untracked file, first.)
Anyway, except for this one big caveat, the stash commit w
has, in its snapshot, exactly the snapshot you say you would like to be added just past commit A
.
You can, now that this snapshot exists, tell Git to make a new commit B
that has A
as its parent and w
's tree as its snapshot. This needs one Git plumbing command:
git commit-tree -p refs/heads/b refs/stash^{tree}
That is, we use the name refs/heads/b
(branch b
, pointing to commit A
) to tell Git what the parent hash ID should be for our new commit. We use refs/stash^{tree}
to tell Git what the tree (snapshot) should be for our new commit. Git reads standard input to collect a log message—if you like, add -m <message>
or -F <file>
to supply a message, or send one to the standard input:
echo some message | git commit-tree -p refs/heads/b refs/stash^{tree}
The result is:
...--o--o--T <-- your-branch (HEAD)
\ |\
\ i-w <-- refs/stash
\
o--A <-- b
\
B
where new commit B
has the same snapshot as stash commit w
.
The git commit-tree
command prints out the hash ID of the new commit. You'll need to grab this—perhaps into a shell variable—and then most likely set some name, such as refs/heads/b
, to remember this commit. For instance:
hash=$(git commit-tree -p refs/heads/b refs/stash^{tree})
git update-ref -m "add stashed work-tree commit" refs/heads/b $hash
giving:
...--o--o--T <-- your-branch (HEAD)
\ |\
\ i-w <-- refs/stash
\
o--A--B <-- b
That is, new commit b
is now the tip of existing branch b
. The snapshot in B
is that in w
; they're automatically shared. The log message in B
is whatever you gave to git commit-tree
. The hash ID of B
is now stored in b
, and B
's parent is A
, so that this new commit is on branch b
, just as you wanted.
Now that all of that is done, you will want to restore your index and work-tree, which git stash
threw out, but first saved in those two commits. To do that, use git stash pop --index
. The --index
is important: it compares your current index to i
and uses the differences to restore your index.2 Then it compares your current work-tree to w
and uses the differences to restore your work-tree from w
. The pop
part of this then discards the i-w
commits and if there were other stashed commits, makes refs/stash
remember the correct one.
Hence, ignoring all the places where things could go wrong and all the appropriate error checking, the following command sequence might do what you want, depending on just what is is you want:
git stash push # and make sure it does something
hash=$(echo automatic commit of work-tree |
git commit-tree -p refs/heads/b refs/stash^{tree})
git update-ref -m "add stashed work-tree commit" refs/heads/b $hash
git stash pop --index
This is entirely untested (and has some bad failure modes, especially the one where git stash push
says that there is nothing to save and refuses to do anything at all).
2This is an inefficient way of just reading i
directly into the index, but it achieves the same goal. The same holds for the w
step.