TL;DR
Basically, git status
is Git's best attempt to tell you everything at once.
If a file isn't showing up, and you think it might be ignored, git check-ignore -v
is useful: it will tell you why the file is ignored, if it is ignored, or say nothing if it's not ignored.
There is one thing that's missing, which is a quick and easy way to see the special flags that you can set in the index.
git ls-files
can show you what's in the index, but it's not very user friendly.
Long
In Git, tracked simply means is present in the index. At the same time, only untracked files can be ignored. An untracked file is, by definition, a file that is not in the index, but is in your work-tree—the area in which you do your everyday work.
The index is also perhaps best described as what will go into the next commit. (It's more complicated than that—because the index has a few additional roles—but that's the short version.) A new git commit
simply packages up the files that are in the index to make the snapshot for the new commit. Everything else flows from this.
Remember that at all times, there are, in effect, three active copies of a tracked file, though two of them can be missing:
There is the HEAD
(current commit) copy, which is where the file came from initially. If there is no HEAD
copy, the file is new: it will be an added file in the next commit, as compared to the current commit.
The HEAD
copy of the file, like any committed copy of any file, is read-only. Nothing you can do can change it. It's in a special Git-only format and is quite tricky to find: you have to have Git extract it into a usable form.
There is the index copy. Technically this is really a reference to a copy somewhere else. It too is in the special internal Git format for files (a "blob object", in Git terms) but for your own purposes you can just think of it as a whole separate physical copy, because you can, at any time, overwrite it with a new copy, or remove it.
Last, there is the work-tree copy. This is the only version of the file that you can see with your ordinary computer tools. It's in your computer's ordinary format, so you can do anything you like with this copy.
If the work-tree copy of a tracked file is missing, it looks deleted, as compared to the (not-deleted) index copy.
When you first git checkout
some commit—each commit is uniquely specified by its big ugly hash ID, so that you can check out old ones if you like—Git reads the commit's files into the index, so that all the committed files are there and no other files are there.1 So now HEAD
, which is the commit you just checked out, and the index match. Meanwhile, this same git checkout
also removes from your work-tree all the tracked files from the commit you had out a moment ago, and replaces them with the files from the index that are from the new HEAD
commit. So now all the index copies match all the work-tree copies. None of your untracked files are touched here, only the tracked ones.
The effect is that right after git checkout
, all three copies of every tracked file match. There's the unchangeable copy in the HEAD
commit, which has been copied into the index, which has been copied into your work-tree.
From here on, you can:
- modify (or create or remove) a work-tree file however you like;
- use
git add
to copy a work-tree file into the index: now the two match, and if the file was not in the index at all before, now it is in the index;
- use
git rm
to remove a file from both the index and the work-tree: now it's gone from both;
- use (one form of)
git reset
to copy a file from the HEAD
to the index without touching the work-tree; and/or
- use various other Git commands, such as
git rm --cached
or special forms of git checkout
that are now easier in Git 2.23+ via the new git restore
, to
achieve other manipulations of the index and/or work-tree.
Whatever you do, the HEAD
copy of the file never changes. The index copy is either there, so that the file is tracked, or not there, so that the file is untracked. The work-tree copy is there or not: Git does not really care, because Git will use the index copy for the next commit.
To see what's different, you must have Git compare:
HEAD
vs index: anything that is the same is boring; git status
says nothing about that. Anything that is different is interesting: git status
tells you staged for commit, because if you ran git commit
right now, the new commit you would make would be different from the current commit.
index vs work-tree: anything that is the same is boring and git status
says nothing. Anything that is different is interesting: git status
tells you not staged for commit, because you could update the index copy of the file (or remove it) to match the work-tree, after which it might not match the current commit any more.
Last—well, almost last—given any untracked file—which by definition isn't in the index—git status
will complain about it being untracked, unless you use an "ignore" file to make it shut up. The ones you make Git shut up about are your ignored files.
Note that a tracked file can be different in all three copies. In this case, both of the two comparisons—HEAD
-vs-index, and index-vs-work-tree—that git status
runs, result in "interesting" results. You will see the file as both staged for commit and not staged for commit. Sometimes, the result of git status --short
, which puts both results into the first two columns of output from one-line-per-file output with multiple columns, is useful here.
1This is a slight oversimplification. There is a lot of gory detail about when uncommitted changes can be carried across a checkout in Checkout another branch when there are uncommitted changes on the current branch.
Assume-unchanged and skip-worktree
You mention the --assume-unchanged
flag. There is also a --skip-worktree
flag. These flags are slightly different internally, but for the most part, achieve the same result. They can only be set on a file that is in the index, so they only apply to tracked files. Also, only you (or some command you run) can set them: a new clone never has any of these flags set.
Their effect is to tell Git—especially git status
, which is doing a lot of file comparing—that it should not bother to look at the work-tree copy of the file. Instead, when Git would have checked the work-tree copy, Git can just assume that the work-tree copy matches the index copy.
These flags are a little bit hard to view, because they're stored in the index, which itself is a little bit hard to view. The git ls-files
command can show them, along with all the entries in the index—which tends to be thousands of lines in many projects, and hence not all that useful in practice. I wrote a little Python script that I call git-flagged
so that I can run git flagged
to see which files have these flags set, and/or reset the flags, without seeing a lot of other irrelevant stuff.2
Note that operations like git checkout
and git merge
will look past these flags and warn you if a work-tree file would be overwritten. That is, you might make some local change to some file, flag the index copy so that Git normally stops showing you changes not staged for commit, and work for a while, including making new commits, which of course use the index copy of the file so they keep matching the previous commit. But then, having done this for a while, you forget you have these local changes you're deliberately hiding.
At some point, someone else, in some other Git repository from which you regularly obtain commits, changes the file. If you git checkout
their commit, or git merge
with their commit, Git would have to write their version of the file into your work-tree. Fortunately, Git doesn't just do that without warning. Unfortunately, you may have forgotten that you had flagged some files, and you might want to get a full list of all flagged files.
2You can also use grep
or similar, but when I am using the flags a lot, it's nice to have something more convenient.