I've seen following question here on SO, which asks how is possible to track but not stage and how to unstage but not untrack files in git. As a git beginner I'm curious in which particular scenarios would be those two cases useful?
-
Please clarify your down vote so I can improve question. Thanks. – Wakan Tanka Nov 09 '16 at 22:20
1 Answers
Technically, you can't "track but not stage" a file, nor in some sense "unstage without untracking", except that both Git and people use the word "unstage" to refer to something else. (I will describe what "unstage" winds up meaning, in a bit.)
A file is "tracked" in Git if it has an entry in what Git calls its "index", aka "staging area". Both git add
and git rm
create or replace an entry in the index—with git rm
putting in a sort of "whiteout" entry, meaning "it's here for just now, but after I commit, make it be not here at all so that it's no longer tracked." (In fact, git add
can act like git rm --cached
, and does so if the file is in the index, but missing from the work-tree.)
You can see (most of) what is in the index with git ls-files --stage
; see the example below. There are up to four index "slots" per file path name. Three of these are only used during merging. Hence, except when merging, everything in the index is always at "stage zero".
To stage a file, we just run git add path
(or git add -- path
, which is useful if the path part is something like -p
, although you can also name the file -p
as ./-p
to prevent it from looking like the patch flag). If the file is already in the index, this updates it (by replacing the existing index entry).
To "unstage" a file, we generally run git reset path
(add --
if necessary, as before). Except for one case, though, this does not remove the index entry for the path! Instead, it resets (hence the name) the index entry to match the HEAD
commit. The one case where it does remove the entry entirely is when the HEAD
commit does not have a file under that path name.
Hence, "unstaging" a file does not really un-stage the file: instead, it re-stages (or, better, resets) it back to the current (HEAD) commit, but without touching the copy in the work-tree. However, everyone uses the word "unstage" for this action, so we might as well get used to it. :-)
Getting into the weeds (to answer your other question)
The question you linked to talks about using git add -N
. This was totally broken for a long time, somewhat-fixed in Git version 2.5, and is still partly broken in Git 2.10.1 (I just tested it in mine). It effectively puts a placeholder into the index, so that the file is tracked (because it's in the index, of course) but at the same time there's no actual file there. This index entry is marked as "special", much like a whiteout entry. The behavior is actually quite similar to whiteout entries—i.e., git write-tree
does not write the file to the resulting tree—except that the intent-to-add special entry is retained after committing, while a whiteout entry is removed after committing.
As for what use this has: in my opinion, it has none. :-) Clearly, addressing the bugs (complete failure of -N
in Git versions below 2.5, minor issues in 2.5 through 2.10) has not been very important, hence probably few people, if any at all, actually use it.
One thing it does accomplish is to get git diff
to treat the file as tracked, so that a git diff
that compares the current index to the work-tree shows the file's contents as added:
$ git status --short
?? thirdfile
(this indicates that "thirdfile" is untracked)
$ git diff
(no output: git diff
does not look at the untracked file)
$ git add -N thirdfile
$ git status --short
AM thirdfile
(this indicates that the file is now tracked—in the index, and not in the HEAD
commit, hence state A
as the first letter—and also modified, because the index entry is fake but the file exists with real contents)
$ git diff
diff --git a/thirdfile b/thirdfile
index e69de29..1ecbedd 100644
--- a/thirdfile
+++ b/thirdfile
@@ -0,0 +1 @@
+data for file 3
(and here we see the real contents). If we use git ls-index --stage
to peek into the index, we see1 a stage-zero entry—a normal file—whose hash is that of the empty file:
$ git ls-files --stage
100644 e5c143f6fec374d115ef674fc036649d78c625d2 0 README
100644 6b8bd59d197cba9032b59ee3aa3f4e71cfbdc2df 0 dummyfile
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 thirdfile
$ git hash-object -t blob --stdin < /dev/null
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
If we empty out the file, the real file in the work-tree now matches the hash stored in the index. Oddly, while git diff
now shows nothing, git status
still says that the file is modified, as compared to the index version:
$ : > thirdfile
$ git status --short
AM thirdfile
$ git diff
(the last command produces no output). This—that a file whose cache entry is marked "intent to add" is always considered "modified" when compared to the work-tree—is the source of the "partly broken in Git 2.10.1" case, as this fools git commit
into allowing a commit even if the new commit's contents will match the HEAD
commit's contents.
Once you git add
(without -N
) the file, the fake "intent to add"-flagged stage-0 entry in the index becomes a real entry for a real blob (i.e., file) with a real hash pointing to a real object inside the repository. That is, the real entry overwrites the fake one. If you first remove the file from the work-tree, and then run git add
on that path, Git removes the specially-flagged index entry entirely, just as Git does if you run git rm --cached
on it. This differs from using git rm --cached
on a real (not-specially-flagged) file, where Git writes a white-out entry into the index.2
1It's not clear why git ls-files --staged
shows these intent-to-add flagged entries. The same command skips white-out entries.
2We can see whiteout entries using git status --short
. We simply create the file in the work-tree, or use git rm --cached
so that the file stays in the work-tree when the white-out entry is written to the index. At this point git ls-files --stage
does not show the file, git diff
shows the file as deleted, git status
shows the file as deleted and also as untracked, and git status --short
produces two entries for the file:
$ git rm --cached secondfile
$ git status --short
D secondfile
?? secondfile
?? thirdfile
To have the D
entry, there must be something in the index; but to have the ??
entry, Git must be pretending that there is nothing in the index. Hence there is a whiteout entry for secondfile
here, visible in some scans of the index, but none for thirdfile
, which, just a moment ago, I had in the index as one of those intent-to-add fake entries.

- 448,244
- 59
- 642
- 775