lesson ── git-labs ── ~20 мин ── 8 шагов
The goal is to see by hand that Git is a key-value object store on top of
the file system. You'll build a complete commit without git add and
git commit, using only plumbing: hash-object, write-tree,
commit-tree, update-ref. Once you see that porcelain is just a wrapper,
the fear of Git goes away.
интерактивный sandbox
Поднимется контейнер gitlab/git-base с git, bash, pre-commit. В браузере откроется терминал, можно сразу git init. Каждый шаг проверяется автоматически. Сеть air-gapped, github.com недоступен.
stack ── git · bash · 256 MB RAM · air-gapped · самоуничтожается через 30 мин простоя
cd /home/student/work
mkdir -p plumbing && cd plumbing
git init -b main # -b main = default branch is main, not master
ls -la .git/ # see the repo's internal structure
Inside .git/, look at objects/ and refs/. Right now they're almost
empty. Over the course of the lesson you'll fill them yourself.
✓ Repo initialized. objects/ is empty.
hash-object -w takes text and writes it into .git/objects/,
returning a SHA. The file itself does not appear in the working tree.
echo "Hello, Git internals" > /tmp/payload
SHA=$(git hash-object -w /tmp/payload) # -w writes the blob into the object store, returns the SHA
echo "blob: $SHA"
ls .git/objects/${SHA:0:2}/ # ${SHA:0:2} = first 2 characters of the SHA (folder name)A file appeared inside .git/objects/aa/bbcc.... That is the blob:
zlib-compressed content.
A SHA is 40 hex characters. The first 2 are the folder name, the rest are the file name.
✓ The blob is in the object store. It has no name yet.
An object has no file name, only a SHA. To read its content:
cd /home/student/work/plumbing
# the awk one-liner joins folder name + file name = the object's full SHA
SHA=$(find .git/objects -type f | head -1 | awk -F/ '{print $(NF-1)$NF}')git cat-file -t $SHA # -t = object type (blob/tree/commit/tag)
git cat-file -p $SHA # -p = pretty-print the content
-t shows the object type (blob), -p shows the content. This is the
first layer of Git: content-addressed storage.
✓ The object is a blob and holds your string.
A blob by itself is anonymous. For it to become a "file" with a name, you need an entry in the index (the future tree).
cd /home/student/work/plumbing
SHA=$(find .git/objects -type f | head -1 | awk -F/ '{print $(NF-1)$NF}')# --cacheinfo <mode> <sha> <path>: put a ready blob into the index under a name
git update-index --add --cacheinfo 100644 $SHA hello.txt
git ls-files --stage # see the contents of the index
100644 is the mode for a regular file. git ls-files --stage shows the
contents of the index: SHA + name.
✓ The index knows: hello.txt = your blob.
write-tree takes the state from the index and materializes it as a tree object.
cd /home/student/work/plumbing
TREE=$(git write-tree) # reads the index, writes a tree object, returns its SHA
echo "tree: $TREE"
git cat-file -p $TREE # see the tree's content (a list of entries)
The result is a table: 100644 blob <sha> hello.txt. That is the tree:
a list of entries.
✓ The tree is built. Inside it is the entry for hello.txt.
A commit is tree + message + author + (optionally) a parent. Since this is the first commit, there is no parent.
cd /home/student/work/plumbing
TREE=$(git write-tree)
# commit-tree <tree-sha>: create a commit, reading the message from stdin
# (no -p here because this is the first commit, there is no parent)
COMMIT=$(echo "First commit by hand" | git commit-tree $TREE)
echo "commit: $COMMIT"
git cat-file -p $COMMIT # see the commit's fields
You see the fields: tree <sha>, author ... <email>, committer ...,
a blank line, the message. That's everything in a commit.
✓ The commit object is created. It has a SHA, but no branch knows about it yet.
Right now git log is empty: HEAD points to main, but main does not exist.
Create the ref.
cd /home/student/work/plumbing
TREE=$(git write-tree)
COMMIT=$(echo "First commit by hand" | git commit-tree $TREE)
# update-ref <ref> <sha>: atomically write the SHA into the branch file
git update-ref refs/heads/main $COMMIT
git log --oneline # now log is not empty: main exists
cat .git/refs/heads/main # a branch is an ordinary text file with a SHA
git log now shows your commit. .git/refs/heads/main is an ordinary
text file with a SHA.
If the log is empty, make sure HEAD points to main: `cat .git/HEAD`.
✓ The branch is moved, the commit shows in the log. You built a commit by hand.
Right now the file hello.txt is not physically in the working tree: it's
only in objects + index. Check:
cd /home/student/work/plumbing
ls # empty: hello.txt exists only in the objects
git checkout HEAD -- hello.txt # pull the blob from the object store and put it on disk
cat hello.txt # here's the same content as in the blob
checkout pulled the blob from the object store and put it on disk.
The cycle is closed: content -> blob -> index -> tree -> commit -> ref ->
checkout -> file on disk.
✓ The file is in place. Next, in chapter 4, you'll go through this same path bottom-up, reading existing objects.
Git stores three object types: blob (content), tree (directory), commit (snapshot + metadata). Each object is addressed by the SHA-1 of its content. Plumbing commands give you direct access to all three layers.
команды
git hash-object -w filewrite a blob into the object storegit update-index --add --cacheinfo 100644 <sha> pathput a blob into the indexgit write-treebuild a tree from the current indexgit commit-tree <tree> -m '...'create a commit objectgit update-ref refs/heads/main <sha>move a branch onto a commitконцепции