dimhold.by
← Артыкулы

Коміт, сабраны рукамі: 187 байтаў

Мой рабочы 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 хэшуе blob 12\0 hello world\n 8 байтаў загалоўка 12 байтаў змесціва 3b18e512 імя аб’екта толькі змесціва hello world\n 22596363 git яго не ўжывае
Загаловак таксама ідзе на ўваход хэша. Палічыць хэш ад аднаго змесціва можна, але імя выйдзе іншае. У гэтым рэпазіторыі такое імя не сустракаецца.

Коміт, у якім няма фарфору

Чысты рэпазіторый, далей два блобы, дрэва, коміт і спасылка. Пяць выклікаў:

$ 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, таму статус і змаўкае.

master галіна, 41 байт b0eabc99 коміт, 187 байтаў b88f66c9 дрэва, 76 байтаў 3b18e512 greeting.txt блоб, 12 байтаў 1c59427a note.txt блоб, 12 байтаў .git/index індэкс, не запісаны status друкуе D і ?? пакуль git reset яго не пабудуе
Гэта вынік пяці выклікаў пламбінга. Індэкс гэта адзінае, чаго mktree не чапае. Вывад status быў якраз пра гэта.

Што трапляе ў хэш

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.

свабодныя аб’екты, 11 блобаў ад 16828 да 16833 байтаў на дыску усе 33 свабодныя аб’екты, 187576 байтаў у пакеце, адзін файл 1530d3b5 рэвізія 10, цалкам, 13889 16490 байтаў рэвізіі з 0 па 9, дэльты ад 14 да 18 байтаў, глыбіня 1
Адзінаццаць цэлых копій супраць адной цэлай копіі і дзесяці дэльт. Дэльты паказваюць на самую новую рэвізію, таму самая старая збіраецца з апошняй.

Сорак адзін байт

Правіла пра выдаленне каталога гэта тое, што я і хацеў паправіць. Далей усё гэта ідзе на копіі рэпазіторыя, знятай да 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 не дайшоў.