Содержание

Как Git управляет изменениями

Git - это контентно-адресуемая система хранения: все данные хранятся в виде объектов внутри каталога .git.
Каждый объект в Git имеет свой хеш, по для его идентификации.

Разберем, как знакомые команды используют структуру Git для выполнения различных операций.

Проводить эксперименты будем на репозитории httpexpect с тегом v2.16.0

Хеш коммита, на который указывает ветка находится в файле .git/refs/heads/<branch>

❯ cat .git/refs/heads/master

9be446356b6eddc852a20b420cbf19c3c53acca3

Проверим соответствие git командой:

❯ git rev-parse master

9be446356b6eddc852a20b420cbf19c3c53acca3

Хеш коммита, на который указывает тег находится в файле .git/refs/tags/<tag>
Например:

❯ cat .git/refs/tags/v2.16.0

e6879c0c3e358e8400f3fc5e9677a48ceb661740

Прверка командой git:

❯ git rev-parse v2.16.0

e6879c0c3e358e8400f3fc5e9677a48ceb661740

Для этого есть специальный файл .git/HEAD, который содержит указатель на текущую ветку или коммит
Например:

❯ cat .git/HEAD

e6879c0c3e358e8400f3fc5e9677a48ceb661740

Если переключиться на ветку master, то файл HEAD будет таким:

❯ cat .git/HEAD

ref: refs/heads/master

Все объекты в Git неизменяемые и хранятся в каталоге .git/objects.
Имя каждого объекта — это хеш, рассчитанный на основе содержимого.

Объекты сохраняются в сжатом формате <type> <size>\0<content>, где типом может быть:

  • blob - содержимое файла
  • tree - список файлов и каталогов
  • commit - снимок проекта с метаданными
  • tag - аннотированный тег с метаданными

Вычислим хеш для существующего в проекте файла formatter.go:

{
 echo -n "blob "
 echo -n $(cat formatter.go | wc -c)
 echo -ne "\0"
 cat formatter.go
} | sha1

02985f31c63a87435b23c9cdaa4837b355300446

Проверим, что такой файл существует и хеши совпадают

❯ cat .git/objects/02/985f31c63a87435b23c9cdaa4837b355300446 | perl -MCompress::Zlib -0777 -e 'print uncompress <>' | sha1

02985f31c63a87435b23c9cdaa4837b355300446

Сейчас HEAD указывает на комммит e6879c0c3e358e8400f3fc5e9677a48ceb661740, найдем объект с этим хешом и посмотрим структуру файла:
Читаем объект, распаковываем и для удобства чтения заменяем нулевые символы переводом строки:

❯ cat .git/objects/e6/879c0c3e358e8400f3fc5e9677a48ceb661740 | perl -MCompress::Zlib -0777 -e 'print uncompress <>' | tr '\0' '\n'

commit 234
tree c2635674529d78a11624302cc23480a4d00e6984
parent 420f3aeeaa0c45bfac885856ad24dd9c2569d14b
author Victor Gaydov <victor@enise.org> 1696324180 +0400
committer Victor Gaydov <victor@enise.org> 1696324220 +0400

Refine colorhttp func

Тут:

  • commit - тип объекта.
  • tree - указатель на дерево файлов, которое есть в проекте на момент создания коммита.
  • parent - предыдущий коммит, необходимый для прослеживания истории изменения файлов в проекте.
  • author - автор коммита и дата.
  • committer - тот, кто применил коммит и дата.
  • Далее идет сообщение коммита.

Проверим, что выведет команда git:

❯ git log -1 e6879c0c3e358e8400f3fc5e9677a48ceb661740

commit e6879c0c3e358e8400f3fc5e9677a48ceb661740 (HEAD, tag: v2.16.0, origin/v2)
Author: Victor Gaydov <victor@enise.org>
Date:   2023-10-03 13:09:40 +0400

    Refine colorhttp func

В git log вывод чуть красивее, но вся информация присутствует.

Для этого нужно обойти все файлы из каталога .git/refs (и файл .git/HEAD) и сравнить хеши.
Если хеши совпадают, то в названии файла/пути будет имя ветки/тега

❯ grep -rl "e6879c0c3e358e8400f3fc5e9677a48ceb661740" .git/refs .git/HEAD

.git/refs/tags/v2.16.0
.git/refs/remotes/origin/v2
.git/HEAD

Теперь посмотрим какие файлы есть в проекте на момент создания коммита e6879c0c3e358e8400f3fc5e9677a48ceb661740.
В этом коммите дерево файлов находится в объекте c2635674529d78a11624302cc23480a4d00e6984 как мы уже выяснили раньше.

Файл содержит хеши в бинарном формате, поэтому чтобы красиво отобразить их нужно преобразовать в строку:

❯ cat .git/objects/c2/635674529d78a11624302cc23480a4d00e6984 | perl -MCompress::Zlib -0777 -e '
    $_ = uncompress(<STDIN>);
    s/^tree \d+\0//;
    while (/(.*?)\0(.{20})/sg) {
      my ($header, $sha) = ($1, $2);
      $header =~ /^(\d+) (.*)$/;
      my $hex = unpack("H*", $sha);
      print "$1\t$hex\t$2\n";
    }
  '

40000	d3f3c0e53b33d211697bea88a56f7e62deb6d115	.github
100644	6bcd33c7f8945c6526bc2b3442fc591946448eff	.gitignore
100644	4f0aa540e6980fd3fe27f6921b923541a9b5f469	.golangci.yml
100644	f4e3cec654057e6b7d011f9d004fc17e412393d4	.ignore
100644	da361dcc087c3d081a5ceae48ae064f2e6df9260	.spelling
100644	c72b02ee8e98654ae8b92732a0c8429a17e1ba51	HACKING.md
100644	a022050415f901d9e2bb76880f7e14a879c70404	LICENSE
100644	6f31bfaa909a0f435076e73140b029e49750428b	Makefile
100644	6a9971e0d3ae6df648ac98e61deb32d0e8d9ebd8	README.md
40000	7ccc58aa1fb590b1f94a3279c48b1b6b706ef46d	_examples
40000	b8eeb9c418ccc558c14b1fe3da6fac0ce3cd5234	_images
100644	c1314d2f943727a232fd6eefe442132374653ccd	array.go
100644	bde4500abf7dcc8c6bc5f425aac33a8bf0a3816c	array_test.go
100644	d8dcb77078e1fa667793adb5096f180c42e21210	assertion.go
100644	17c863cbc3e9f82ae39158eb1a8c859ed54cf9f9	assertion_test.go
100644	0880e663ddaff0cd07664a080682e9f2bb07b7b2	assertion_validation.go
100644	0b0a6ddd1667bcf0b71c1bc1a8b0d233c1da744d	assertionseverity_string.go
100644	9ed2e4f0749aa8aaa54813624c41fe3b4b2022b4	assertiontype_string.go
100644	f1b3d171938aa8ba1b69141388519c1cb35763b1	binder.go
... и много других файлов

Проверить, что правильно декодировали файл можно командой git cat-file -p c2635674529d78a11624302cc23480a4d00e6984.

Для проверки списка файлов можно выполнить команду git ls-tree:

❯ git ls-tree -r e6879c0c3e358e8400f3fc5e9677a48ceb661740
100644 blob d276992856053ee54e26f7bd1fbe237ad1e08db6	.github/FUNDING.yml
100644 blob dd2452e4e151b3bd3cea38918d66153561ecf68a	.github/workflows/build.yaml
100644 blob 25e11982853085f09bc1c6161bab7b793ff48873	.github/workflows/detect_conflicts.yml
100644 blob 6bcd33c7f8945c6526bc2b3442fc591946448eff	.gitignore
100644 blob 4f0aa540e6980fd3fe27f6921b923541a9b5f469	.golangci.yml
100644 blob f4e3cec654057e6b7d011f9d004fc17e412393d4	.ignore
100644 blob da361dcc087c3d081a5ceae48ae064f2e6df9260	.spelling
100644 blob c72b02ee8e98654ae8b92732a0c8429a17e1ba51	HACKING.md
100644 blob a022050415f901d9e2bb76880f7e14a879c70404	LICENSE
100644 blob 6f31bfaa909a0f435076e73140b029e49750428b	Makefile
100644 blob 6a9971e0d3ae6df648ac98e61deb32d0e8d9ebd8	README.md
100644 blob 5812292bb2a4d0db578a4a2eb9740549585de472	_examples/.golangci.yml
100644 blob 0ce3624bfde7c2531957bbf752ed3ca0dfea6f70	_examples/doc.go
100644 blob d70ab083953bf89ae9d0314f273e63c37efd6abd	_examples/echo.go
100644 blob 548c51cfc20ac67bac5d19636eaf2beb0991c0f0	_examples/echo_test.go
100644 blob f2ab214fe185e1d93235d15852e2609d4bff34a5	_examples/fasthttp.go
100644 blob fefacee3dbb47ab5a5f74adaf9cd282d13dfe347	_examples/fasthttp_test.go
100644 blob fa15533fd7ea55ccc41717eff0ab3804e7439f6c	_examples/formatter_test.go
... и много других файлов

Разница будет только в том, что git ls-tree отображает только список файлов.
А мы вывели списко файлов и каталогов, которые находятся в корне.
Для получения всех файлов, нужно всего лишь рекурсивно пройтись по всем директориям аналогичным образом.

Структура объектов понятна, поэтому для краткости команд будем использовать git cat-file.

Для этого нужно сравнить деревья файлов проекта предыдущего коммита с деревом файлов текущего коммита.
По полю parent 420f3aeeaa0c45bfac885856ad24dd9c2569d14b получаем дерево родительского коммита - d4259cdf526369da146cf6195148e2309a9a08c6

Сравниваем деревья командой git diff и видим, что поменялось содержимое файла formatter.go:

❯ git diff --text --no-index <(git cat-file -p d4259cdf526369da146cf6195148e2309a9a08c6) <(git cat-file -p c2635674529d78a11624302cc23480a4d00e6984) | cat

diff --git a/dev/fd/13 b/dev/fd/15
--- a/dev/fd/13
+++ b/dev/fd/15
@@ -38,7 +38,7 @@
 100644 blob 585a0a1b341ddc9baebf6868f932b4c17f08fd5e	expect_test.go
-100644 blob 7b8439619f48224b440dda08c3058a1ce9bafe3d	formatter.go
+100644 blob 02985f31c63a87435b23c9cdaa4837b355300446	formatter.go
 100644 blob a78d1d2555fcb3486a95fc2e2439a750243efdde	formatter_test.go

Для этого сравниваем два блоб объекта:

❯ git diff --text --no-index <(git cat-file -p 7b8439619f48224b440dda08c3058a1ce9bafe3d) <(git cat-file -p 02985f31c63a87435b23c9cdaa4837b355300446) | cat

diff --git a/dev/fd/13 b/dev/fd/15
--- a/dev/fd/13
+++ b/dev/fd/15
@@ -1009,93 +1009,67 @@ var defaultTemplateFuncs = template.FuncMap{
 	},
-	"colorhttp": func(enable bool, colorName string, isResponse bool, input string) string {
+	"colorhttp": func(enable bool, isResponse bool, input string) string {
 		if !enable {

... и другие изменения

Чтобы себя проверить выполняем git log -p и видим, что именно такие изменения и были:

❯ git log -p | head -n 20

commit e6879c0c3e358e8400f3fc5e9677a48ceb661740
Author: Victor Gaydov <victor@enise.org>
Date:   2023-10-03 13:09:40 +0400

    Refine colorhttp func

diff --git a/formatter.go b/formatter.go
index 7b84396..02985f3 100644
--- a/formatter.go
+++ b/formatter.go
@@ -1009,93 +1009,67 @@ var defaultTemplateFuncs = template.FuncMap{
 		}
 		return color.New(colorAttr).Sprint(input)
 	},
-	"colorhttp": func(enable bool, colorName string, isResponse bool, input string) string {
+	"colorhttp": func(enable bool, isResponse bool, input string) string {
 		if !enable {
 			return input
 		}

Внесем небольшое изменение в файл chain_test.go и добавим файл в индекс.

Сравним файл индекс .git/index с предыдущим индекс файлом (который предварительно скопировали)

❯ git diff --text --no-index <(xxd /tmp/old_index) <(xxd ./.git/index) | cat

diff --git a/dev/fd/13 b/dev/fd/15
--- a/dev/fd/13
+++ b/dev/fd/15
@@ -258,10 +258,10 @@
 00001010: 21a3 f429 0000 81a4 0000 01f5 0000 0000  !..)............
 00001020: 0000 31e0 94fc 11ee 2e63 9aac 4d09 5213  ..1......c..M.R.
 00001030: f026 b4e7 44ab 4656 0008 6368 6169 6e2e  .&..D.FV..chain.
-00001040: 676f 0000 6916 3bed 26c1 a7a8 6916 3bed  go..i.;.&...i.;.
-00001050: 26c1 a7a8 0100 0010 21a4 f54d 0000 81a4  &.......!..M....
-00001060: 0000 01f5 0000 0000 0000 53a3 b767 65b3  ..........S..ge.
-00001070: 3d7f ae0f 59fc 10e2 423c e7ea 4581 16bc  =...Y...B<..E...
+00001040: 676f 0000 6916 3c31 000d 44d6 6916 3c30  go..i.<1..D.i.<0
+00001050: 3b60 3632 0100 0010 21a4 f54d 0000 81a4  ;`62....!..M....
+00001060: 0000 01f5 0000 0000 0000 53a6 d0bd f288  ..........S.....
+00001070: e37d 62c7 7e6c 2ea6 3254 8044 2b46 a941  .}b.~l..2T.D+F.A
 00001080: 000d 6368 6169 6e5f 7465 7374 2e67 6f00  ..chain_test.go.
 00001090: 0000 0000 6916 2983 3712 b6d4 6916 2983  ....i.).7...i.).
 000010a0: 3712 b6d4 0100 0010 21a3 f42a 0000 81a4  7.......!..*....

Не знаю как сделать более наглядным, но тут видно, что поменялся хеш с b76765b33d7fae0f59fc10e2423ce7ea458116bc на d0bdf288e37d62c77e6c2ea6325480442b46a941.
Сравним файлы с этими хешами:

❯ git diff --text --no-index <(git cat-file -p b76765b33d7fae0f59fc10e2423ce7ea458116bc) <(git cat-file -p d0bdf288e37d62c77e6c2ea6325480442b46a941) | cat
diff --git a/dev/fd/13 b/dev/fd/15
--- a/dev/fd/13
+++ b/dev/fd/15
@@ -822,7 +822,7 @@ func TestChain_TestingTB(t *testing.T) {
 			want: true,
 		},
 		{
-			name: "AssertReporter",
+			name: "AssertReporterNew",
 			args: args{
 				handler: &DefaultAssertionHandler{
 					Formatter: newMockFormatter(t),

Да, это то самое изменение, которые мы внесли. Его же и показывает команда git diff:

❯ git diff HEAD | cat
diff --git a/chain_test.go b/chain_test.go
index b76765b..d0bdf28 100644
--- a/chain_test.go
+++ b/chain_test.go
@@ -822,7 +822,7 @@ func TestChain_TestingTB(t *testing.T) {
 			want: true,
 		},
 		{
-			name: "AssertReporter",
+			name: "AssertReporterNew",
 			args: args{
 				handler: &DefaultAssertionHandler{
 					Formatter: newMockFormatter(t),

Обычный тег это просто указатель на коммит.
Тег v2.16.0 указывает на коммит e6879c0c3e358e8400f3fc5e9677a48ceb661740:

❯ cat .git/refs/tags/v2.16.0

e6879c0c3e358e8400f3fc5e9677a48ceb661740

Создадим аннотированный теги командой git tag -a v2.16.0-1 -m "version v2.16.0-1" и посмотрим на что он указывает:

❯ cat .git/refs/tags/v2.16.0-1

fd8a701b59285ffd3b143cf7973ae2ba67b1f9fd

Аннотированный тег - новый объект типа tag, который содержит метаданные и указатель на коммит e6879c0c3e358e8400f3fc5e9677a48ceb661740:

❯ cat .git/objects/fd/8a701b59285ffd3b143cf7973ae2ba67b1f9fd | perl -MCompress::Zlib -0777 -e 'print uncompress <>' | tr '\0' '\n'

tag 171
object e6879c0c3e358e8400f3fc5e9677a48ceb661740
type commit
tag v2.16.0-1
tagger Alexander Myasnikov <myasnikov.alexander.s@gmail.com> 1763062383 +0300

version v2.16.0-1