null

Немного о porcelain и plumbing командах, а также о типах объектов в git

Каждому разработчику практически ежедневно приходится сталкиваться с системой контроля версий git, выполняя набор привычных операций вроде git add, git fetch, git commit, git log и им подобных. Обычно для работы над прикладным проектом знания базового набора команд git оказывается вполне достаточно, однако важно понимать, что большинство всем известных команд представляют собой высокоуровневый инструментарий git (т.н. "porcelain" команды), которым возможности git не исчерпываются.

Porcelain - это команды, которые предназначены в первую очередь для удобства конечного пользователя (программиста), у них обычно довольно подробный вывод информации в консоль, который представлен в виде, удобном для чтения. Кроме того, вывод таких команд не регламентирован и теоретически может незначительно меняться от версии к версии. Подобные команды делают работу с git быстрее и доступнее, освобождая программиста от лишней работы, осуществляя множество операций "под капотом". У такого подхода есть очевидный недостаток - отсутствие гибкости и возможности контролировать каждый отдельный этап процесса выполнения команд. Впрочем, для большинства повседневных задач это и не нужно. Однако проблемы могут возникнуть, например, при необходимости автоматизировать какой-либо процесс, связанный с git. Если мы пишем скрипт, который полагается на вывод porcelain команд, то с выходом новой версии git код может сломаться (не говоря уже о необходимости в принципе парсить текстовый вывод). Примерами porcelain команд можно обозначить add, commit, status, push, pull, fetch, branch, checkout, merge, rebase, log, diff, stash, show, reset и др.

Другой категорией команд выступает низкроуровневый инструментарий - plumbing команды. В повседневной работе они редко нужны, однако позволяют работать с внутренностями git напрямую, обеспечивая максимальную гибкость. Иногда такие команды экономят время в повседневной разработке, заменяя длинные цепочки визуальных действий или помогая «вылечить» репозиторий, когда обычные команды выдают ошибки. Также они могут оказаться крайне полезны для использования в скриптах, поскольку они стабильны, их вывод не меняется и содержит минимум необходимых данных в хорошо структурированном формате, упрощая автоматическую обработку. По сути каждая porcelain команда, написанная программистом, самостоятельно исполняет набор более мелких низкоуровневых plumbing команд, манипулирующих различными объектами git.

Для общего понимания ситуации отметим, что в файловой системе git в принципе есть 4 типа объектов, о которых мы часто не задумываемся, но к работе с которыми сводится процесс исполнения любой команды:

  • blob - просто голое содержимое файла; в нем нет имени файла или даты, только сами данные (код, текст, картинка); если создать несколько файлов с идентичным содержанием, то все они будут ссылаться на один и тот же объект;

  • tree - своего рода директория, объект-дерево, содержащее список имен файлов (блобов) и других директорий (деревьев), а также права доступа к ним;

  • commit - это "снимок" проекта, который ссылается на конкретное дерево (корень проекта в этот момент) и содержит метаданные: автора, дату, сообщение и ссылку на предыдущий коммит (родителя);

  • tag - ссылка на конкретный коммит, содержащая имя тега, автора и сообщение.

Именно с ними работают plumbing команды как "атомарные" операции. В качестве примера приблизительно покажем, какие plumbing команды нужно вызвать, чтобы добиться результата аналогичного работе высокоуровневой обертке git commit:

  1. git hash-object -w file.txt - взять содержимое файла и превратить его в объект blob, высчитать хеш и сохранить в директорию .git/objects
  2. git update-index --add file.txt - привязать имя к хешу в индексе,  связать содержимое (блоб) с именем файла
  3. git write-tree - создать из текущего состояния индекса новый объект типа tree, вернуть хеш этого дерева
  4. echo "commit message" | git commit-tree <хеш_дерева> -p <хеш_предыдущего_коммита> - создать объект commit из созданного ранее дерева; идет указание на tree как на внутреннее состояние репозитория и на parent как на прошлый коммит (нужно, чтобы связать в историю все коммиты); на выходе получаем хеш самого коммита
  5. git update-ref refs/heads/main <хеш_коммита> - передвинуть указатель ветки на новый хеш, чтобы HEAD теперь указывал на новый созданный нами коммит

На примере выше мы показали, как plumbing команды позволяют лучше понимать внутреннее устройство git и при необходимости гибко им управлять, в том числе и внутри скриптов. Сам список таких команд весьма обширен и может быть найден в документации. Сейчас же, в дополнение к plumbling командам уже указанным выше, отдельно выделим еще несколько, заслуживающих внимание:

  • git rev-parse - одна из самых полезных команд в принципе, которая принимает строку (название ветки, тег, ссылка в стиле HEAD~4) и отдает хеш соответсвтующего коммита (либо последнего на указанной ветке); оказывается весьма нужно в скриптах;
  • git cat-file <хеш> - показывает сырое содержимое объекта по его хешу, позволяя узнать тип, размер любого из типов объектов;
  • git ls-tree - вывод содержимого дерева, показывается как список файлов и папок внутри конкретного коммита;
  • git ls-files - вывод информации о содержимом индекса.

Итак, подводя итог, еще раз отметим, что даже если использовать большинство из plumbing команд оказывается избыточно в случае повседневной разработки, все равно знать об их существовании достаточно важно. В этом небольшом обзоре, который не является исчерпывающим, я постарался сформулировать азы, которые позволят понимать git чуть глубже и расширить свой арсенал команд.

 

Вперед

Коротко о себе:

 

Работаю Java\Kotlin Backend Developer в компании Tune-it. На работе занимаюсь проектами, связанными с Liferay, NiFi, Spring Framework, а вне работы - философской антропологией