Edit: the question has changed enough to invalidate the previous response.
There's still a typo (f1.txt
listed twice) and funky non-ASCII Unicode quote marks, but we can now see what is going wrong here:
$ LATEST_TREE_HASH=$( git write-tree )
$ echo $LATEST_TREE_HASH > .git/HEAD
This is a bit of a problem. As Mark Adelsberger noted in a comment and your script says by using the word TREE
here, git write-tree
writes a tree, not a commit.
Why this is a problem
What's in .git/HEAD
is supposed to be exactly one of two things:
- a string of the form
ref: refs/heads/name
, where name
is a valid branch name, or
- the hash ID of a commit object.
In turn, a branch name—a reference of the form refs/heads/name
—must always point to a commit object, never to a blob, tree, or tag object.
This means that Git in general assumes that whatever comes out of .git/HEAD
, it refers to a commit object. By writing this tree hash into .git/HEAD
you've violated this assumption. However, to allow for "unborn branches", such as the state of an initial repository with no master
yet, HEAD
can contain the name of a branch that does not actually exist.
What happens next is, I think, not guaranteed. The git checkout
command assumes that if HEAD
contains a valid hash, it contains a commit hash, and the only other allowed possibility is that HEAD
contains the name of an orphan branch. So we run git checkout target_hash
, as in your example:
git checkout cf178d5
Case 1: moving from commit to commit
Suppose HEAD
contained a valid commit hash. Let's call this the old hash, as distinguished from the target commit hash. In this case, git checkout
would:
- Compare (recursively as needed for sub-trees) the contents of the tree of old to the contents of the tree of target.1
- For each hash that must change, including being added or removed, check whether the index and/or work-tree file version in the current index and work-tree match those in old.
- If all match, update the index hashes and copy the new files to the work-tree (or remove the files from the work-tree and remove the index entry, if appropriate).
- Otherwise (some files don't match): complain and refuse the checkout.
Obviously --force
disables the check, but this is the basic process by which both staged and unstaged modifications are carried from one checkout to another when switching branches without being in a "clean" state. The process is described in all its gory detail in the Two Tree Merge section of the git read-tree
documentation.
Case 2: moving from orphan branch to commit
The other possibility allowed by the rules is that you are currently on an orphan branch. In this case, there is no current commit. Most likely, Git simply uses the empty tree as if it were the current commit. It then follows the same rules for case 1, which is now allowed since it has a tree.
But this is, obviously, not guaranteed. If Git were to use the current (valid) tree stored in .git/HEAD
as the base tree, instead of the empty tree, and then proceed as for case 1, you'd see your two files get removed. Follow all the sub-cases outlined in git read-tree
with $H
set to your existing tree, vs $H
set to the empty tree. (I admit to not having done so, but I think this is where the behavior comes from. But see also the remark about case 3 in the read-tree documentation!)
1Git actually achieves this using a temporary index, stored in the index.lock
file. If all goes well, the temporary index is renamed to become the regular index, unlocking the index in the process. If things go poorly, Git removes the temporary index.lock
file, discarding the temporary index and unlocking the index.
Original answer (to somewhat different question)
There's another set of funky non-ASCII quote marks that made cut and paste of your instructions fail, so that when I made the normal first commit I ended up with two files:
$ git commit -m "second commit"
[master (root-commit) b9c7e4b] second commit
2 files changed, 1 insertion(+)
create mode 100644 f3.txt
create mode 100644 f3.txtcontentecho
$ ls
f3.txt f3.txtcontentecho
$ git ls-files -s
100644 5927d85c2470d49403f56ce27afd8f74b1a42589 0 f3.txt
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 f3.txtcontentecho
But note that my ls
vs ls-files -s
output differs enormously from yours at this point:
So currently the index file and the directory contains only new f3.txt file:
$ git ls-files -s
100644 [some hash] 0 f3.txt
$ ls
f1.txt f2.txt
It's not at all clear to me why you would have files f1.txt
and f2.txt
in your work-tree now; I don't.
Now we create a commit with git commit-tree
and run git checkout
:
$ INITIAL_COMMIT_HASH=$( \
> echo 'initial commit' | git commit-tree $INITIAL_TREE_HASH )
$ git checkout $INITIAL_COMMIT_HASH
but what I get is very different:
Note: checking out 'cd1bc16160c8a2814cd94bc8397230ffe5a16c22'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at cd1bc16... initial commit
and everything reads as I would expect (files f1.txt
and f2.txt
are in the work-tree and the index; neither of the f3
files are visible). Running git log --graph --all
shows the expected two (disconnected) commits (both are root commits, with no parents).