-1

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?

Community
  • 1
  • 1
Wakan Tanka
  • 7,542
  • 16
  • 69
  • 122

1 Answers1

0

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.

torek
  • 448,244
  • 59
  • 642
  • 775