I sometimes need to make a snapshot of the current (possibly dirty) working directory. git stash save -u
is very similar to what I need but there are two problems:
git stash apply
because I first need to find the state before git stash
.I currently have the following sequence of commands that works for me but I would like to know if there is a more elegant way of doing it.
# on branch master
git add .
git commit -m "Temporary commit on the original branch"
git checkout -b snapshot_of_master_yyyy-mm-dd-hh-mm-ss HEAD~
git merge master
git checkout master
git reset HEAD^
Thank you everyone for the answers and explanations! I am going to do something like this mostly based on @XavierGuihot's answer
git stash -u # -u to include untracked files
git checkout -b snapshot_of_master_yyyy-mm-dd-hh-mm-ss
git stash apply
git add --all
git commit -m "Snapshot"
git checkout master
git stash pop --index # --index to recover the state of indexed files
You can stash your changes, create a new branch, apply your changes, perform the commit only on the new branch and then checkout master again before finally applying your changes again.
git stash
git checkout -b snapshot_of_master_yyyy-mm-dd-hh-mm-ss
git stash apply
git add --all
git commit -m "Snapshot"
git checkout master
git stash pop
Most efficient with no worktree churn is to do it directly with core commands, assuming you don't have any inflight merge conflicts or intent-to-add markers in the index it's
statenow=`git write-tree`
stashedindex=`git commit-tree -p @ -m "index snap" $statenow`
git add -f .
git tag snap-`date +%Y-%m-%dT%H.%M.%S` \
$(git commit-tree -p @ -p $stashedindex \
-m "worktree snap" \
$(git write-tree)
)
git read-tree $statenow
but the simplest way if you don't care about ignored files or no-effect worktree churn will be
git stash -a -u
git tag snap-`date +%Y-%m-%dT%H.%M.%S stash
git stash pop
then for either, to recover the state you do e.g.
git clean -dfx
git checkout snap-2018-02-18T14.19.15 # to move HEAD + worktree there
# or
git read-tree -um @ snap-2018-02-18T14.19.15 # just the worktree
git read-tree @ snap-2018-02-18T14.19.15^2 # then restore the index
Use jthill's answer, which he wrote while I was away from the keyboard between stages of writing the long answer below.
Before you settle on any one answer to this question (see jthill's answer for the set I think are in some sense "the best"), consider this: Whenever you have a state that isn't just "everything tracked in the work-tree exactly matches HEAD
", there are three versions of each file to worry about.
That is, when you first run:
git clone <url>
or, from a totally clean (no untracked files, no modified files, etc.) state do:
git checkout <somebranch>
you start out with three copies of each file, such as README
and Makefile
and so on, available right now:
One in HEAD
(the tip commit of whichever branch you have checked out): this one is read-only and of course matches the one in HEAD
, because it is the one in HEAD
. This HEAD:README
file is stored in the special Git-only format that Git uses. (Use git show HEAD:README
to see it.)
One in the index. The index is where you will build your next commit, but right now, it just contains a copy of everything that is in the current commit. So :0:README
—you can use git show :0:README
to see this copy—exactly matches HEAD:README
. This extra copy is also stored in the special Git-only format, which means it takes essentially no space at all. The difference between :0:README
and HEAD:README
is that you can overwrite this one: git add README
copies the work-tree README
into :0:README
, for instance. (The copy you make here will take some space, but will then be what goes into the next git commit
, after which they'll share that frozen / read-only version, until you copy yet another one in.)
The last copy of each file, such as README
, is in the work-tree. This file is in its normal everyday format, so that all programs can read and write it. There's no need to view it with git show
because it's just a normal file!
Initially, all three versions match, and there are no untracked files.
So:
I sometimes need to make a snapshot of the current (possibly dirty) working directory.
for every file with path P
except for untracked files, we have:
P
that is in the HEAD commit: you never need to save this one, as it's already saved, permanently, by being in a commit;P
that is in the index: do you want to save this one?; andP
that is in the work-tree: you no doubt do want to save this one.That also leaves the question of what to do with the untracked files.
Let's note here that an untracked file is simply a file in the work-tree that does not exist in the index. (This includes files that are in HEAD
but that you carefully removed from the index—they're currently untracked, as long as the work-tree version of that file exists.)
git stash save -u
is very similar to what I need ...
That is a good clue, because git stash save -u
saves:
-a
instead of -u
here.... but there are two problems: [1.
git stash -u
removes the untracked files after committing them, and 2.git stash -u
makes it rather hard to un-stash again later]
Note that git stash -u
simply runs git clean
to remove the untracked files. You would have to do a git reset --hard && git clean -df
to get back to the state to unstash (but see the problem case I mention below).
Now, the main problem with making a commit—any commit—is that you do it by copying files into the index. But we just noted that there could be index versions of various files, like :0:README
and :0:path/to/important.data
, that differ from their HEAD:
counterparts and from their work-tree counterparts. If you do anything to save the work-tree counterparts, you must do it by overwriting the index copy!
If this is okay, you could possibly have a path forward that's less complicated than using git stash
or equivalent. But you still have a problem with untracked files! If it's not OK, you must save the index first, just like git stash
does, in which case you might want to just use git stash
.
We already noted, above, that an untracked file is a file that's in the work-tree—has some path U—that is not also in the index: there's no :0:U
. This creates something of a problem: to save those files, we have to copy them into the index. This, of course, destroys (overwrites) anything we carefully staged that's different from both HEAD
and work-tree version. This is where the all the complications come from.
If you do want to preserve the index state for any reason, and the index state records which files should be tracked and untracked later, then we have our solution (which is jthill's solution as well), which is much like git stash
, but slightly modified:
git write-tree
.git commit-tree
. This commit can have the current commit (HEAD
or @
) as its parent, although that's not actually required.git add -A
, or git add -f -A
, etc., depending on what precisely you want in this second commit.git stash
does, he has the second commit store both commits as parents, with the index commit as the second parent of the second commit. That forces us to use the suffix ^2
notation later, which has the advantage that it works with the git stash
script.)Having written all of these out, we must immediately put the index back into its original state—the one we saved as the first step. Otherwise all the previously-untracked files are now tracked files!
To restore one of these things, we have the same problem you have already with git stash save -u
: the files in the second commit we made (to hold the untracked files) are going to become, at least temporarily, tracked files. If there are files with those same names in the work-tree right now, Git will be very reluctant to overwrite them—so we need a git clean -df
or git clean -dfx
to destroy them. There's a bit of a problem here because this will also remove files that aren't in that second commit: for instance, suppose when you saved everything, there was an untracked file named important-1
but nothing named important-2
. Now there's an important-2
.
If you just naively run git clean -df
or git clean -dfx
right now, Git will remove both of these untracked important-*
files. Then we'll instruct Git to extract, from the second commit, all the files, including the previously-untracked important-1
. Git will copy the file into both the index and the work-tree. Since there is no saved important-2
, Git won't copy that file.
This is the rather large defect to using:
git clean -dfx
git checkout snap-2018-02-18T14.19.15
and is why:
git read-tree -um @ snap-2018-02-18T14.19.15
git read-tree @ snap-2018-02-18T14.19.15^2
is better. The first step, git read-tree -um @ snap-...
, does a merge-and-update to bring in the second commit we made (holding all the work-tree state) into the index and update the work-tree. That way important-2
won't be destroyed.
The second step is needed to fix up the index afterward, because reading all those untracked files from the second commit causes them to become tracked files. We'd like to restore the index to the way it was when we made the snapshot, or at least, pull out of the index any files that are in it now that should not be in it.
We have exactly that index state: it's in the first commit we made, which is snap-...^2
(the second parent of the snapshot or the stash). We can either read that directly into the index:
git read-tree snap-2018-02-18T14.19.15^2
(note the lack of @
/ HEAD
here), or do a two-tree merge to leave modifications we've made to the index in place where possible:
git read-tree @ snap-2018-02-18T14.19.15^2
Note that we could, instead, just reset the index to match the commit you are currently on:
git reset HEAD
or perhaps reset it to the parent of the saved first commit:
git read-tree snap-2018-02-18T14.19.15^1
if you didn't really want to save the index state. Either way, the untracked files are once again untracked, now that they're no longer in the index.
Another clean and simple approach. Even no branch changing nor stash
needed:
git commit
git branch name-of-my-snapshot-branch
git reset HEAD^
With the commit
just commit everything you want to snapshot, in whichever way you prefer to do this.
The reset
points your branch to the former commit and so you are back to where you started.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With