Does git stash, stashes only staged files or even Unstaged and Untracked files?
By default : git stash
stashes staged files (the index) and unstaged files (tracked files, which are modified but not added). Untracked files are not stored and are left on disk.
You can use :
-
git stash -k
to tell stash to leave the staged files as they are, and only stash unstaged modifications, -
git stash -u
to tell stash to also include untracked files, -
git stash -a
to tell stash to also include untracked and ignored files.
reference : git-stash options
What git stash
does is make some commits. This means it doesn't do anything you couldn't do some other way, by just making commits.
I'm not clear whether the git stash stashes only staged files or even Unstaged and untracked files?
This isn't exactly a well-formed question, except for the last part. By default, git stash
does not stash untracked files—but there's an option to git stash
that makes it do this. You probably should not use this option. To really understand how git stash
works and what you can do, you need to be aware of a number of things.
Git is all about commits
Git isn't really about branche*. Instead, it is about commits. Branch names are useful: they help you (and Git) find commits. But it's the commits that matter.
Each commit holds a full snapshot of all of your tracked files. (We'll get into tracked in a later section, when we discuss Git's index or staging area.) These files are stored in the commit in a special, read-only, Git-only, frozen and compressed and de-duplicated form. This de-duplication means that when you make a new commit that re-uses most of the files from some previous commit, the new commit doesn't actually need another copy of the files: it just re-uses the frozen files from the earlier commit.
Every commit is numbered, but the numbers aren't simple sequential numbers like #1, #2, #3, and so on. Each commit instead has a big ugly hash ID that is unique to that one particular commit. Every Git everywhere will agree that that commit gets that hash ID, and no other commit can use that hash ID. This works internally by computing a cryptographic checksum over all of the bits (the ones and zeros) that make up the commit. Since all Gits use the same algorithm, they will all agree about as to the hash ID for that commit.
(This in turn means that it is literally impossible to change a commit. If you take a commit out of the big database of Git objects, modify some bits, and put it back, the new commit has a different hash ID. The previous commit continues to exist. Hence commits, like Git's frozen stored files, are impossible to change.)
Besides a full copy of each of your files (but de-duplicated), each commit also stores some metadata: some information about the commit itself. This information includes the name and email address of the person who made the commit, some date-and-time-stamps, and the log message in which they explain why they made that commit; but it also includes something just for Git itself: the hash ID of the previous commit. This allows Git to work backwards.
A branch name simply holds the hash ID of the last commit on the branch. We won't go into much detail here since we're more interested in stash commits, except to describe how Git normally makes a new commit.
Your work-tree
The files stored inside each commit are read-only, and moreover, are in a format that most programs on your computer literally can't read. So how can you possibly work on your files? Or, to put it another way, commits are read-only, so how can you make a new one?
Git's answer to this is to provide you with a working tree or work-tree. You start by selecting some commit—using git checkout
or git switch
, for instance—and Git takes all the frozen, compressed, de-duplicated Git-only files from the commit and expands them into normal everyday read/write files that you can read and write. These everyday-use copies of the files, taken from the commit, are in your work-tree.
This means that the files you see and work with are not actually Git's files. Git's files are hidden away in the .git
directory (and most don't have normal file names either: they're stored in .git/objects
with hash ID names and/or moved into special pack files that store multiple objects with even more compression).
This has to be the case, because Git's files are read-only and Git-only, and you need read/write files that everything can use. So Git fills in your work-tree, and then you do your work. The work-tree files are yours: Git just fills them in when you ask it to.
This would suffice: Git could just have the two copies of each file, one frozen in the current commit and one thawed-out-and-useful in your work-tree. Other version control systems do this, in fact. But Git doesn't. Git keeps a third copy of each file.1 This extra copy is in Git's frozen format, but unlike the ones in a commit, is not quite frozen: you can replace it.
1Technically, this third copy is pre-de-duplicated, so it's not actually in the index directly. Instead, the index holds each file's mode, the name of the file, a blob hash ID, a staging number, and other information that makes Git go fast. You don't normally need to know any of this, though: you can just pretend that the index holds a frozen-format copy of each file, ready to commit. You only need to know about the blob hashes if you start using the low level index manipulation commands, git ls-files --stage
and git update-index
.
The index or staging-area
The index is another hugely important thing in Git, and it's often not explained very well. Part of the problem is that it's a bit complicated: it takes on a bigger role during merges, for instance. But overall, there's a good and relatively simple description of Git's index: Git's index holds your proposed next commit.
Note: the index is either so important, or was so badly named originally, that it now has three names. It is called the index (as here), or the staging area (representing its role in making new commits), or—rarely these days—the cache. All three names refer to the same thing.2
In other words, at all times, there are three active copies of each file:
-
One is in the current or
HEAD
commit, and this copy is frozen: nothing can change it. You can make new commits that have a different copy, but the old commit continues to hold the old copy. -
A second one is ready-to-freeze and is in Git's index. It starts out matching the frozen copy.
-
The last one is in an everyday format and is yours to work with: it is in your work-tree.
When you run git commit
and make a new commit, Git packages up all the files that are in Git's index right then, and puts those into the new commit. So the new commit consists of exactly the version of each file that is in Git's index at that time.
When you run git add
on a file, you are really telling Git: Copy the work-tree version of the file into the index. Git squishes it down to the frozen format (and de-duplicates it if there's already a copy). If this is a totally new file, now it's in the index. If it was already in the index, that boots the old copy out of the index; now the index copy matches the work-tree copy.
You can also tell Git to remove a file from the index. If it was there before (copied from a commit or from your work-tree), now it's gone. The main command for doing this is git rm
, and git rm
will not only remove the index copy, but will also remove your work-tree copy. Since the work-tree copy isn't in Git at all—unless you copied it into the index, that is, or unless it came out of a commit—and you've just also removed the index copy, be careful with this operation.
To remove the index copy of a file without removing the work-tree copy, you can use git rm --cached
. Since there's still a work-tree copy, this is less dangerous, but remember that the work-tree copy is not in Git, and when you make a new commit, the new commit won't have a copy of the file.
So, while the index / staging-area starts out matching a commit, you'll generally modify things and then have Git update the index. The updates you make to the index result in "files staged for commit". If you haven't updated the index, but have updated the file in your work-tree, this results in "files not staged for commit". Note that you can do both though:
- Extract a commit: now all three copies of file F match.
- Modify the work-tree copy of F. Now
HEAD
and index F match, but work-tree doesn't: the file is "not staged for commit". - Run
git add F
NowHEAD
doesn't match the index copy of F, but the index and work-tree copies match: the file is "staged for commit". - Modify the work-tree copy of F some more. Now all three copies differ and hence file F is both "staged for commit" and "not staged for commit"!
Again, the thing to keep in mind here is that there are three copies of each file. Often, two of them, or even all three of them, match. When they match, Git just doesn't say anything about the extra copies.
The git add
and git rm
commands are the two main ones for updating Git's index. You must update Git's index before making a new commit, because git commit
uses the copies of the files that are in Git's index. That's really all there is to it here.3
2Sometimes, especially in the Git source code, the word "cache" refers to the internal data structure produced by reading the index.
3Note that git commit -a
and git commit files
work by adding any to-be-committed files to the index. This can get fairly complicated, especially if you use git commit --only
; we will not get into these details here.
Tracked vs untracked files
This brings us to the definition of an untracked file, which is also remarkably simple: An untracked file is a file that is not in Git's index. That is, if there's some file named U in your work-tree, but U is not in Git's index right now, then U is untracked.
Add file U to Git's index (while keeping it in your work-tree) and now U is tracked. Remove it from Git's index (while keeping it in your work-tree) and U is untracked again. Since git commit
will only store, in the new commit, those files that are in Git's index, an untracked file won't get committed.
Now we can finally talk sensibly about git stash
A normal git commit
command:
- gathers some metadata from you, such as your name and email address and a log message;
- writes out all of the files that are in Git's index, which are already in the frozen format, into a new commit, and uses the metadata from the above (and the current commit's hash ID) to make all of this into the new commit; and
- stores the new commit's hash ID in the current branch name, because the new commit is the new last commit.
The git stash
command starts out pretty similarly, but instead of getting a log message from you, it generates one on its own. It then makes a commit from whatever is in Git's index right now—but it doesn't put this commit on the current branch at all.
Having made a commit in almost the usual way, git stash
now runs the equivalent of git add -u
to update all the files that are in Git's index, based on the same file in your work-tree. This updats the index to match your work-tree files—but only tracked files are in the index (by definition) so only these files get updated. The stash command now makes a commit from this index, saving all the tracked files.
Git ties these two commits—the index one and the work-tree one—together,4 then runs git reset --hard
.5 This puts all the tracked files in your work-tree back into the same state they have in the current commit, and also overwrites Git's index with the frozen-format copies from the current commit. So you now have a pair of commits that save both the previous index and the previous work-tree—two full snapshots—but untracked files are not saved here at all.
Because this stash commit-pair is on no branch, you can later change to a new branch and use git stash apply
or git stash pop
or any of their variants to apply the stash to a different starting-point. The actual apply process is somewhat complicated, and if you forget to use --index
while applying, the pop
command will, if it succeeds, drop both commits even though it doesn't actually use the index commit. So I always advise anyone who uses git stash
to avoid git stash pop
: use git stash apply
first and then check to make sure you got the result you wanted before using git stash drop
to drop the stash.
4Technically, Git does this by making the work-tree commit in the form of a merge commit. Other Git tools will think this is a normal merge and won't work very well on it; you need to use git stash
on stash commits to make them behave well.
5Not all that long ago, git stash
was a fancy script, and literally used various Git commands directly, including git reset --hard
. Now it's a C program, but it still does the equivalent, just without running extra commands.
git stash
has options
When you run git stash
to create a new stash, you can give it one of two options:
-
-u
or--include-untracked
: this makes a third commit, which we'll describe in a moment. -
-a
or--all
: this also makes a third commit.
Both options tell git stash
to make this third stash commit, along with the usual two (index and work-tree). The only difference is what goes into the third commit:
- with
-a
, all files that are in your work-tree, but aren't in Git's index, go into the third commit, including files listed in.gitignore
; - with
-u
, untracked files that aren't listed in.gitignore
go into the third commit, but untracked files that are listed in.gitignore
don't.
Having made this third commit,6 the stash command then removes from your work-tree each file that went into this commit.
When you go to restore a stash, Git will check whether it's a two-commit stash or a three-commit stash. If it is a three-commit stash, Git will attempt to extract all of the files from the third commit. If any of those files are in your work-tree right now—remember, these were untracked files at the time you made the stash, so they were in your work-tree then—Git will refuse to extract this stash at all. You'll have to move any such files out of the way, or remove them entirely.
If Git can extract the third commit, it will do that, and will also extract the other two commits in the usual way. So you can stash untracked files, using one of these three-commit stashes.
6For technical reasons, this commit has to be made before the work-tree commit, so in a way it's the second commit, but it's carefully shifted around to act like it was third, for later extraction.
Keeping staged changes staged
As noted above, git stash push
or git stash save
will make a new stash with two commits in it:
- One commit saves the current index as-is.
- The other commit saves the current work-tree as-is.
Changes that are staged really means "the index copy of some file differed from the commit's copy of that file". Usually, that in turn means that the work-tree copy of that file matched the index copy.
A later git stash apply
without the --index
option just ignores the index commit. If the index commit has the same copy of each file that the work-tree commit has, and you apply only the work-tree commit, you'll get the same changes made to your work-tree: there's nothing lost by ignoring the index commit, except for forgetting which git add
commands you had used earlier.
A later git stash apply
with --index
tells Git: Before you apply the work-tree commit, try to apply the index commit to the current index. This won't always work, and if it doesn't work, git stash
will just suggest that you try not using --index
. If using --index
was very important, this is probably bad advice!
If it does all work, though, Git's index will be updated. So now some set of changes will once again be "staged for commit"—which, remember, just means "the index copy of the file differs from the current commit's copy".
In any case, with or without --index
, if the git stash apply
command now moves on to the work-tree commit, it will use Git's merge machinery to attempt to apply this commit. This can result in merge conflicts. If it does, they can be very confusing.
I prefer not to use git stash
most of the time
Most of the time, instead of git stash
, I recommend that you just make commits.
It's easy to make a commit, even one on the "wrong branch", and then "remove" it later. (The commit itself sticks around for a while in case you decide you want it back: it takes at least 30 days, by default, for Git to get around to deciding that this commit isn't any use any more, after which Git will clear it out.) The "wrong commit" made on the branch is harmless as long as you're careful not to send it out with git push
or whatever, and having made the commit, you can now switch to the right branch and use git cherry-pick
to copy it.
Having all of Git's tools—including git diff
and git show
and git cherry-pick
—that work on normal, everyday, easily found and easily seen commits is much more pleasant than having to use git stash apply
on commits that are hard to inspect because they're not everyday commits and aren't on any branch.
All that said, though, git stash
can be pretty handy sometimes. If things go awry, the git stash branch
command can turn an existing stash into a new branch, which then gives you access to all of Git's regular tools.