Мой рабочы git гэта шэсць каманд і адно правіла. Каманды: add, commit, pull, push, checkout і log. Правіла: калі рэпазіторый трапіў у стан, якога я не пазнаю, выдаліць каталог і скланаваць нанова. Правіла працуе. Заадно яно значыць, што я ні разу не ведаў, што менавіта выдаляю.
Таму замест таго каб вывучыць сёмую каманду, я сабраў коміт рукамі, толькі з пламбінга. Усё ніжэй гэта git 1.7.8, сабраны з зыходнікаў, са стандартным узроўнем сціскання. Даты аўтара і коміцера прыбітыя да адной секунды, каб кожны хэш тут паўтараўся.
Спачатку імёны, у рэпазіторыі, у якім няма нічога:
$ printf 'hello world\n' > a.txt
$ git hash-object a.txt
3b18e512dba79e4c8300dd08aeb37f8e728b8dad
$ printf 'blob 12\0hello world\n' | sha1sum
3b18e512dba79e4c8300dd08aeb37f8e728b8dad -
Імя аб’екта гэта sha1 ад загалоўка і змесціва. Загаловак гэта тып, прабел, даўжыня ў байтах і нулявы байт. Калі палічыць хэш ад адных дванаццаці байтаў, выйдзе 22596363b3de40b06f981fb85d82312e8c0ed511. У гэтым рэпазіторыі такое імя не сустракаецца.
Потым я скапіяваў a.txt у b.txt і запісаў абодва:
$ git hash-object -w a.txt
3b18e512dba79e4c8300dd08aeb37f8e728b8dad
$ git hash-object -w b.txt
3b18e512dba79e4c8300dd08aeb37f8e728b8dad
$ find .git/objects -type f
.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
Два файлы, адно імя, адзін аб’ект. Імя гэта функцыя ад гэтых байтаў разам з загалоўкам, таму адно і тое ж змесціва пад двума шляхамі не можа лягчы ў два аб’екты. На дыску аб’ект займае 28 байтаў. На ўваход deflate ідуць дваццаць байтаў (дванаццаць змесціва і восем загалоўка), на выхадзе 28. Сцісканне дадало тут восем байтаў.
Коміт, у якім няма фарфору
Чысты рэпазіторый, далей два блобы, дрэва, коміт і спасылка. Пяць выклікаў:
$ git hash-object -w greeting.txt
3b18e512dba79e4c8300dd08aeb37f8e728b8dad
$ git hash-object -w note.txt
1c59427adc4b205a270d8f810310394962e79a8b
$ printf '100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad\tgreeting.txt
100644 blob 1c59427adc4b205a270d8f810310394962e79a8b\tnote.txt
' | git mktree
b88f66c9fea440e76158a5d4dec51ecaf7e3f53d
$ echo handmade | git commit-tree b88f66c9fea440e76158a5d4dec51ecaf7e3f53d
b0eabc99e5bbf3ffdc8127c50e8ea05c0de3dfba
$ git update-ref refs/heads/master b0eabc99e5bbf3ffdc8127c50e8ea05c0de3dfba
У коміце 187 байтаў змесціва і гэта ўсё, што ў ім ёсць:
$ git cat-file -p b0eabc99e5bbf3ffdc8127c50e8ea05c0de3dfba
tree b88f66c9fea440e76158a5d4dec51ecaf7e3f53d
author Dmitriy Semenkevich <dimhold@gmail.com> 1323253380 +0100
committer Dmitriy Semenkevich <dimhold@gmail.com> 1323253380 +0100
handmade
Дрэва гэта 76 байтаў. Галіна гэта файл, у якім сорак шаснаццатковых лічбаў і знак новага радка:
$ wc -c .git/refs/heads/master
41 .git/refs/heads/master
$ cat .git/HEAD
ref: refs/heads/master
git log --stat гэта чытае і друкуе мае два файлы як дададзеныя, што я і хацеў убачыць. Чытачу накшталт log патрэбныя аб’екты і адна спасылка. І тое і другое я напісаў рукамі.
Далей вось гэта:
$ git status --short
D greeting.txt
D note.txt
?? greeting.txt
?? note.txt
Абодва файлы адначасова выдаленыя і неадсочваныя. Файла .git/index на дыску не было зусім: дрэва я сабраў праз mktree і праз індэкс не праходзіў. Git параўноўвае індэкс з HEAD, бачыць два файлы ў HEAD і нічога ў індэксе, далей паведамляе пра два выдаленні. Спіс неадсочваных бярэцца з абходу каталога: застаецца тое, пра што індэкс не ведае, а гэта тыя ж два файлы. git reset без аргументаў піша індэкс на 184 байты з HEAD, статус змаўкае, спасылка застаецца на месцы. Значыць, індэкс гэта трэцяя сутнасць на дыску, побач з аб’ектамі і спасылкамі. Ён трымае спіс файлаў, якія git пакладзе ў наступны коміт. Пасля git reset без аргументаў гэты спіс гэта копія дрэва з HEAD, таму статус і змаўкае.
Што трапляе ў хэш
commit-tree на тым жа дрэве з той жа датай і тым жа паведамленнем зноў дае b0eabc99. Праз секунду ён дае 47e87c9eaa1871559a5ee5c44ca3170cfc73f0a2. Дрэва абодва разы хэшуецца ў b88f66c9.
Дрэва гэта функцыя ад змесціва. Коміт нясе яшчэ час і аўтара. Рэбэйз я тут не запускаў, але новыя імёны комітаў пасля рэбэйзу бяруцца адсюль: радок коміцера пішацца нанова ў кожнага коміта, які пераехаў.
Што я думаў пра змесціва коміта
Я лічыў, што ў коміце ляжыць дыф, бо дыф гэта тое, што друкуе git show. Таму я ўзяў read-cache.c з зыходнікаў git, 46456 байтаў на 1703 радкі. Далей адзінаццаць комітаў: файл як ён ёсць, потым яшчэ дзесяць, у кожным перапісаны адзін радок у сярэдзіне:
$ git diff --stat HEAD~1 HEAD
read-cache.c | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
count-objects -v кажа count: 33. Трыццаць тры аб’екты на адзінаццаць комітаў, значыць адзінаццаць блобаў і адзінаццаць дрэў пад імі. Кожны блоб гэта цэлая копія файла, ад 16828 да 16833 байтаў на дыску ў залежнасці ад рэвізіі. Свабодныя аб’екты ў суме даюць 187576 байтаў. Адзін перапісаны радок каментара каштуе цэлага новага блоба.
Далей git gc:
$ git count-objects -v
count: 0
size: 0
in-pack: 33
packs: 1
size-pack: 18
prune-packable: 0
garbage: 0
$ wc -c .git/objects/pack/*.pack
16490 .git/objects/pack/pack-d208f07049583dc92008db070bb8c6daa90890fa.pack
$ git verify-pack -v .git/objects/pack/pack-*.idx | grep ' blob '
1530d3b547cf0a52ec2f6f4ed20053f5229581ea blob 46468 13889 1783
4723c6ab5406c51c12595742b8770ebd36062d5b blob 16 29 15722 1 1530d3b547cf0a52ec2f6f4ed20053f5229581ea
7765d864e63e91df92c92c73404d6d857be734d3 blob 16 29 15802 1 1530d3b547cf0a52ec2f6f4ed20053f5229581ea
5790a91044e4fdf5b2eec515051a66c110e0daa4 blob 18 31 16439 1 1530d3b547cf0a52ec2f6f4ed20053f5229581ea
187576 байтаў ператварыліся ў 16490, гэта ў 11,4 раза менш. Вышэй чатыры з адзінаццаці радкоў з блобамі. З сямі, якіх я не прывёў, шэсць гэта дэльты па 16 байтаў, а адна 14.
База 1530d3b5 гэта самая новая версія файла. Дзесяць старых рэвізій ляжаць дэльтамі ад яе, глыбіня 1, памер ад 14 да 18 байтаў. Рэвізія, якую я закоміціў першай, захоўваецца як дэльта ў 18 байтаў ад апошняй. Калонку size я перачытаў двойчы, перш чым паверыў: у дэльта-аб’екта ў ёй памер дэльты.
$ git cat-file -s 4723c6ab5406c51c12595742b8770ebd36062d5b
46467
Дыфы існуюць, але ляжаць слоем ніжэй коміта. Лічыць іх repack, а запускае яго gc. Тут ён лічыць назад, ад самай новай версіі. У коміце запісана імя дрэва, у дрэве імя блоба, а блоб на дыску гэта па-ранейшаму цэлая копія read-cache.c.
Сорак адзін байт
Правіла пра выдаленне каталога гэта тое, што я і хацеў паправіць. Далей усё гэта ідзе на копіі рэпазіторыя, знятай да gc:
$ git rev-parse HEAD
c4608054b7ae3474916ed487338c7d3a5c72ec20
$ git reset --hard HEAD~3
HEAD is now at 972ff27 revision 7
$ sed -n '853p' read-cache.c
* Trivial optimization, revision 7: if we find an entry that
$ git cat-file -t c4608054b7ae3474916ed487338c7d3a5c72ec20
commit
$ git reflog
972ff27 HEAD@{0}: reset: moving to HEAD~3
c460805 HEAD@{1}: commit: revision 10
Гэта першыя два запісы рэфлога з дванаццаці. Рабочае дрэва паехала на тры рэвізіі назад. Коміт, з якога я пайшоў, па-ранейшаму аб’ект і рэфлог яго па-ранейшаму помніць. Вярнуць яго каштавала аднаго update-ref. Наступны reset --hard перапісаў індэкс і 46 КБ рабочага файла з аб’ектаў, якія нікуды не падзеліся. count-objects паказвае 33 і да і пасля ўсяго гэтага.
git branch invoice-fix піша спасылку на 41 байт і не дадае ні аднаго аб’екта. Заадно ён піша рэфлог новай галіны на 167 байтаў, значыць галіна абышлася тут у 208 байтаў. “Галіны ў git танныя” гэта фраза, якую я паўтараў, не маючы за ёй ліку.
Чаго я не праверыў
Як repack выбірае базу дэльты і ці трымаецца глыбіня 1 на сапраўднай гісторыі. Стандартна тут акно 10 і глыбіня да 50. Адзінаццаць рэвізій аднаго тэкставага файла гэта лёгкі выпадак для акна ў 10.
Бінарны файл, у якім дэльце няма чаго паўторна выкарыстоўваць. Тут толькі тэкст.
Калі недасягальны коміт сапраўды знікае. Я памераў, што ён перажывае reset, а да prune не дайшоў.