Многие из нас создают по несколько коммитов в день с помощью GUI либо через командную строку. Например:
# 1. Modify or create a file in your working directory.
echo '# my change' > 'test.sh'
#2. Add the modification to the staging area of git.
git add test.sh
# 3. Commit the staged changes.
git commit -m "initial commit"
В примере мы используем высокоуровневые команды git, такие как git add
и git commit
. Однако также существует другая группа команд git, которые обрабатывают низкоуровневые операции.
В этой статье мы создадим git‑коммит, используя низкоуровневые операции, а не команду git commit
.
Прежде чем перейти к низкоуровневым командам и созданию git‑коммита, нам нужно понять основы. Давайте начнем с состояния файла в git.
Основы
Файлы в git могут быть в одном из трех состояний:
Modified: Файл изменен, но не был зафиксирован в базе git.
Staged: Текущая версия модифицированного файла будет включена в следующий коммит.
Committed: Данные сохранены в базу git.
Аналогично этому, git‑проект состоит из трех разделов:
Working Directory: Здесь расположены файлы, извлеченные из базы git, которые вы можете спокойно редактировать. Здесь находятся файлы в состоянии Modified.
Staging Area (Index): Файлы внутри директории .git, которые содержат информацию о том, что войдет в следующий коммит. Здесь находятся файлы в состоянии Staged.
Директория Git: В этой директории git хранит все объекты и метаданные вашего репозитория. Эта директория и есть то, что вы копируете при клонировании репозитория. Здесь находятся файлы в состоянии Committed.
Теперь, когда мы разобрались в различных разделах git‑проекта, нам необходимо понять, что же такое git‑коммит.
Объекты Git: Blob, Tree и Commit
Git‑коммит — это один из объектов git. В git есть несколько типов объектов, включая blob, tree и commit, которые можно найти в директории.git/objects.
Если вы посмотрите на содержимое этой директории, то увидите, что все хранится с использованием SHA-1 хэша объекта вместо названий файлов. Такой подход позволяет git отслеживать все изменения содержимого файла или директории и делает незамеченные изменения невозможными.
Blob
Мы можем хранить любой blob (бинарный файл) в базе данных git, получая таким образом адресно‑объектную файловую систему с встроенной системой контроля версий. Это можно легко сделать, используя одну из низкоуровневых команд — git hash-object
:
echo 'hello world' | git hash-object -w --stdin
Флаг -w
говорит git не только вернуть хэш переданного через поток ввода содержимого, но также сохранить само содержимое в качестве blob в директории .git/objects
. По сути, git записывает бинарный файл со следующим содержимым (для простоты понимания используется формат шаблонов JavaScript):
const blobFileContent = `blob ${content.bytesize}\0${content}`
const blobFileName = sha1hash(blobFileContent)
В случае «hello world» содержимое файла становится следующим: blob 11\0hello world
. Затем git рассчитывает SHA-1 хэш содержимого и сохраняет файл, используя хэш в качестве названия.
Tree
Объекты‑деревья позволяют нам хранить названия файлов. В данном случае можно думать о деревьях как о директориях, а о blob — как о содержимом файлов. По сути, дерево — это набор ссылок на blob вместе с их названиями или на другие деревья.
Содержимое объекта дерева, показанного на изображении выше, выглядит следующим образом:
100644 blob 8b137891791fe96927ad78e64b0aad7bded08bdc README
100644 blob 8b137891791fe96927ad78e64b0aad7bded08bdc package.json
040000 tree 9c422c2393ba5463772797e780e1d4c00400374c src
Commit
Коммит git — это, по сути, объект, содержащий ссылку на древо git вместе с информацией об авторе изменений, когда они были сделаны и почему (сообщение коммита). У коммита также может не быть родительского коммита (изначальный коммит), один родитель (обычный коммит) или несколько (merge‑коммит).
Пример содержимого объекта коммита (сообщение коммита отделяется от метаданных пустой строкой):
tree 5fb4d17478fc270ea606c010293c97bb76dec583
author Avestura <me@avestura.dev> 1725466118 +0330
committer Avestura <me@avestura.dev> 1725466118 +0330
initial commit
Теперь, когда у нас есть понимание, что из себя представляют объекты blob
, tree
и commit
, мы можем визуализировать их взаимосвязь. Рассмотрим следующий сценарий:
git init # initialize the .git repository
echo 'Readme' > README
echo 'License' > LICENSE
git add README LICENSE
git commit -m 'initial commit'
В данном случае в git создается 4 объекта:
Blob‑объект README
Blob‑объект LICENSE
Tree‑объект, содержащий ссылки на предыдущие 2 объекта, а также их названия
Commit‑объект, ссылающийся на tree и включающий информацию об авторе
Если мы добавим еще один коммит, новый коммит также будет включать в себя метаданные о родителе, указывая на первоначальный коммит:
Создание коммита, the hard way
Теперь, когда мы имеем представление об объектах git и их взаимосвязи, мы можем с легкостью создать коммит, используя низкоуровневые команды.
В первую очередь нам необходимо инициализировать репозиторий:
$ git init
Initialized empty Git repository in E:/Projects/git/git-playground/.git/
Теперь нам нужно создать blob‑объект. Как мы уже знаем, сделать это можно командой hash-object
:
$ echo 'This is the content of my file' | git hash-object -w --stdin
6b59acb69a04903bfa9189e3c482fb57f77393f9
Мы сохранили наш blob‑объект и знаем его хэш. Теперь нам нужно создать tree‑объект. Обычно Git использует staging‑область (индекс) для создания деревьев. Мы можем создать индекс с единственной записью (нашим созданным ранее blob‑объектом), используя команду git update-index
:
git update-index --add --cacheinfo 100644 6b59acb69a04903bfa9189e3c482fb57f77393f9 myfile.txt
Показанная выше команда выполняет следующее:
--add
добавляет файл в индекс, если его еще там нет.-
--cacheinfo <mode> <object> <path>
используется из‑за того, что наш файл находится не в самой директории, а внутри базы данных Git.Число отображает тип файла.
100644
означает обычный файл; другие типы включают исполняемые файлы и симлинки.6b59acb69a04903bfa9189e3c482fb57f77393f9
— хэш созданного blob.myfile.txt
— название файла.
После того как мы подготовили индексный файл, мы можем создать объект древа, используя write-tree
:
$ git write-tree
de53417c67393f9ef09239709759ecbbd5ebfb97
Git возвращает нам хэш созданного объекта древа. Мы можем просмотреть его содержимое командой cat-file
:
$ git cat-file -p de53417c67393f9ef09239709759ecbbd5ebfb97
100644 blob 6b59acb69a04903bfa9189e3c482fb57f77393f9 myfile.txt
Теперь, когда у нас есть объект древа и он связан с первоначальным blob, мы можем создать объект коммита, используя команду git commit-tree
:
$ echo 'My commit message' | git commit-tree de53417c67393f9ef09239709759ecbbd5ebfb97
409399744678c13717b30c103feef9451c9103bf
Скрытый текст
Для создания коммита с родителем мы можем использовать флаг -p
в команде commit-tree
. Первоначальные коммиты не имеют родителей, поэтому в примере выше он не был использован. Пример команды:
$ echo 'My commit message' | git commit-tree abcdefg -p klmnope
Итак, мы наконец создали коммит, не используя какие‑либо из высокоуровневых команд (например, git commit
). Теперь мы можем просмотреть содержимое созданного нами коммита:
$ git cat-file -p 409399744678c13717b30c103feef9451c9103bf
tree de53417c67393f9ef09239709759ecbbd5ebfb97
author Avestura <me@avestura.dev> 1725470340 +0330
committer Avestura <me@avestura.dev> 1725470340 +0330
My commit message
Мы также можем просмотреть лог коммита, используя git log
:
$ git log --stat 409399744678c13717b30c103feef9451c9103bf
commit 409399744678c13717b30c103feef9451c9103bf
Author: Avestura <me@avestura.dev>
Date: Wed Sep 4 20:49:00 2024 +0330
My commit message
myfile.txt | 1 +
1 file changed, 1 insertion(+)
Для того чтобы файл появился в рабочей директории, можно вернуть текущую ветку к созданному нами коммиту, используя git reset
:
$ git reset --hard 409399744678c13717b30c103feef9451c9103bf
HEAD is now at 4093997 My commit message
$ ls
myfile.txt
$ cat myfile.txt
This is the content of my file
? Победа! Мы создали коммит вручную и увидели его в нашей рабочей директории.
Заключение
У git есть два набора команд: высокоуровневые, такие как git add
, git commit
, git remove
и т. д., и низкоуровневые, которые используются высокоуровневыми командами для работы с объектами и ссылками git. Мы использовали эти низкоуровневые команды, чтобы создать коммит путем создания базовых объектов tree
и blob
.
vandlog
а зачем?
SergeyKiselev2001
AnSt
Чтобы лучше понимать как работает git.