Многие из нас создают по несколько коммитов в день с помощью 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 могут быть в одном из трех состояний:

  1. Modified: Файл изменен, но не был зафиксирован в базе git.

  2. Staged: Текущая версия модифицированного файла будет включена в следующий коммит.

  3. Committed: Данные сохранены в базу git.

Аналогично этому, git‑проект состоит из трех разделов:

  1. Working Directory: Здесь расположены файлы, извлеченные из базы git, которые вы можете спокойно редактировать. Здесь находятся файлы в состоянии Modified.

  2. Staging Area (Index): Файлы внутри директории .git, которые содержат информацию о том, что войдет в следующий коммит. Здесь находятся файлы в состоянии Staged.

  3. Директория 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.

Комментарии (4)


  1. vandlog
    10.09.2024 13:56
    +2

    а зачем?


    1. SergeyKiselev2001
      10.09.2024 13:56
      +1


    1. AnSt
      10.09.2024 13:56
      +2

      Чтобы лучше понимать как работает git.


  1. General_Manjago
    10.09.2024 13:56

    Спасибо за статью, жду продолжение про annotated tags :-)