As several answers and a comment already noted, uncommitted work is simply uncommitted: it's not "on" any branch yet at all. The mental model that you need to have, to understand why this is and what is going on, is relatively simple, but not as simple as it could be if Git weren't Git. :-)
Keep all of these things in mind:
What's in the repository, contained in branches, are commits. Until something is committed, it's not in the repository at all.
Each commit is its own separate thing, in that you can pick one out and work with it. Each commit stores a complete snapshot of all of its files, plus more information such as who made it—the kind of stuff that shows up in git log
. In other words, Git doesn't store changes, but rather whole snapshots.
When Git shows you a commit as changes, what it's really doing is comparing the commit to the one that comes just before it–its parent commit. Git extracts both the parent and the child to a temporary area (in memory), then compares them. For all the files that are the same, Git says nothing. For each file that is different, Git computes a change-recipe: make these changes to the parent's copy of the file, and you'll get the child's copy of the file.
Git can compare any two commits—just give git diff
any two commit hash IDs, for instance—but it can also compare some other things that aren't exactly commits, as we'll see below.
Git finds commits by their hash IDs. These are the big ugly strings of letters-and-digits. Every commit has a unique one. They look random (but are actually completely deterministic). Humans are not good at working with these, which is why we use branch names in the first place.
Each commit is completely, totally, 100% read-only once it is made. Nothing can change any part of any commit. The files inside each commit are stored in a special, read-only, Git-only, compressed format that only Git can use. They are frozen for all time and continue to exist in their saved form as long as the commit itself continues to exist.
That last part is great, because it means that you can always go back to any version you saved. But it's also terrible, because it means that you can't actually get any work done with the files inside a commit.
Because of this, Git must extract a commit. That is, it has to copy all the frozen, Git-only files out of a commit and turn them into ordinary everyday files that the rest of your computer can use. So there is a Git command that means: Take the frozen files out of a commit, and make them ordinary files that I can see and work with. That command is git checkout
(and in Git 2.23 and newer, git switch
as well).
Having taken the files out of a commit, you can now see and work with them. That's where you make changes. This work area is yours to work with: it's not Git's. Git just overwrites these files with new ones (and/or removes some of these files) when you tell it to do that. So, any changes you have made are not actually stored anywhere in Git. They're just in your work area—the place Git calls your working tree or work-tree.
The way this works is that you pick a commit—by its hash ID, or by a branch name; either way you have picked one particular commit—and give Git that hash ID or name, with git checkout
or git switch
. Git then arranges things so that your work-tree has copies of the files from that commit. That commit is now your current commit. If you gave Git a branch name, that branch name is your current branch. (If you gave Git a raw hash ID, you're not on any branch now.)
Both the current branch name, and the current commit, are findable by the special name HEAD
. Git can answer two different questions about HEAD
:
- What branch name, if any, does it hold? (If it doesn't have a branch name, Git calls this a detached HEAD.)
- What commit hash ID does it name?
The git status
command generally tells you about the branch name as that's more useful to humans, but you'll see hash IDs in many places too, such as git log
or git branch -v
output.
It's important to know one other thing here. When Git first copies all the files out of some commit, it copies them—or at least, information about them and about your work-tree as well—to a Git-only-format area that Git pairs up with your work-tree. This extra copy goes in something that Git calls, variously, the index or the staging area.
The effect is that there is a third copy of each file,1 sort of in-between the committed copy and the usable copy, like this:
HEAD index work-tree
--------- --------- ---------
README.md README.md README.md
main.py main.py main.py
for instance, if you have two files in the commit. The copy of each file in HEAD
is read-only and Git-only and you can't see it directly—you have to have Git extract it. The copy in your work-tree is just a regular file and is yours to do with as you like.
The copy of each file in the index is Git-only—it's in the same format as a committed file—but it is not read-only: you can replace it any time. The git add
command effectively takes whatever you have in your work-tree now and copies that into the index. So after you've modified a file in your work-tree, you need to use git add
to replace the copy in Git's index.
Until you git add
updated files, the copies that are in the index match the copies that are in the HEAD
commit. That is, you started by having git checkout
or git switch
pick some commit. Git copied the commit's files to the index, then copied the index's files to your work-tree. So HEAD
and the index match.
When you make a new commit, Git simply freezes the index copy into a new commit. The new commit gets added to the current branch name (using the "what branch name is in HEAD
question), and then the new commit becomes the current commit (updating which hash ID is the current commit). Now that Git has made a new commit from whatever copies of files were in the index, HEAD
and the index match again.
Note how the copies of files in your work-tree don't really matter to Git. Git makes new commits from Git's index. They are not in any branch: only commits can be contained in branches.
When you use git status
, it actually runs two git diff
s for you:
First, it prints the name of the current branch, and some other useful information if appropriate.
Then it compares—i.e., uses git diff
—the HEAD
commit vs the index. For every file that is the same, it says nothing. For every file that is different, git diff
would show you a recipe for changing the left side (HEAD
) copy into the right side (index) copy; git status
trims this down to just the file's name and status (new, modified, or deleted). These are the changes staged for commit.
Then, it compares—i.e., uses git diff
—the index vs your work-tree. For every file that is the same, it says nothing. For every file that is different, git status
lists the file's name and status. These are the changes not staged for commit.
Last, since your work-tree is yours, you can create new files that aren't in the index at all. These files did not come out of the previous commit. They are not yet in Git's index, not until you use git add
to copy them into it. These are your untracked files. An untracked file is simply a file that is in your work-tree but not in your index.
If you list a particular untracked file in .gitignore
, git status
won't mention that file, and git add
won't copy that file into the index. This only works for untracked files though: if a file is already in Git's index, listing it in .gitignore
has no effect on it.
Note that the index gets loaded from a commit you git checkout
, and different commits can have different files in them, so what's in Git's index depends—at least initially—on the specific commit you chose. After that, you can use git add
and git rm
to put new files into Git's index and take files completely out of Git's index, before you make a new commit, so the set of files in the index isn't frozen either. Only the files in commits are frozen.
If you keep all of the above in mind, much more about Git will make sense.
1Technically, the index holds a reference to the file's content, rather than actually containing the file directly. This distinction only matters if you start using git ls-files --stage
and git update-index
to work directly with the index. Mostly, you should use git add
and similar commands, which hide all these details, so that you don't need to care about these fiddly little distinctions.
Conclusion
A commit can be in multiple branches. All of these branches contain that commit. It's helpful to draw the relationships between commits: some are parents of others, some are children of others, some are siblings, and so on. It is a lot like a family tree.
A commit holds a snapshot, but by comparing the snapshot in the commit's parent to the snapshot in the commit, you can see a commit as changes.
You can have commits that aren't in any branch, which is how git stash
works. We have not covered how branch names work here, but in fact they only hold the hash ID of the last commit in that branch. Everything else is in the commits themselves. The commits have relationships (parent/child), but the branch names don't.
The commits are what matter here. Branch names just help you (and Git) find commits.
Commits store snapshots, not changes. So does Git's index—the snapshot in the index acts like the proposed next commit. So, for that matter, does your work-tree: but your work-tree is yours, to do with as you will. Git just creates and removes files in it when you tell Git to do that (git checkout
, git switch
, and to remove one specific file or set of files from both index and work-tree, git rm
).
We did not cover this here, but it is possible to change the copies of files in your work-tree (and even in Git's index too), and then still switch to another branch. But it's only sometimes possible. When and why you can or cannot switch like this gets tricky, although there's a fundamental safety aspect: you can switch branches if that won't destroy your changes; you can't if it would. For the gory details, see Checkout another branch when there are uncommitted changes on the current branch.
Uncommitted work exists only in your work-tree and maybe also Git's index, if you've used git add
to copy it there. It is not in any commit, and therefore is not in any branch.