linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it works
  • Уроки
  • База знаний
  • Собеседование
Часть II — Внутренности Git

$ глава 4 · 50 минут

Plumbing и porcelain

В предыдущей главе мы посмотрели на объекты, которые лежат в .git/objects/. В этой - на команды, которые с ними работают напрямую, без обёрток.

Git разделён на два слоя. Сверху - porcelain, привычные команды (add, commit, log, status, push). Снизу - plumbing, низкоуровневые операции над объектами и ссылками. Porcelain - это тонкая прослойка над plumbing.

Знать plumbing напрямую не обязательно. Через них почти никто не работает руками. Но один раз пройти через них стоит. После этого обычные команды перестают казаться магией: становится видно, какая plumbing-операция за каким add стоит, и почему git status выдаёт именно эти три раздела.

4.1 Откуда взялись названия

Plumbing - это водопровод. Porcelain - это унитаз. То, что пользователь видит, - фарфор; то, что под ним, - трубы. Метафора из самого исходника Git, она же - в документации.

Разделение появилось не сразу. В первых версиях (2005) Git был набором низкоуровневых утилит, написанных в основном на C и shell: git-update-index, git-write-tree, git-commit-tree, git-read-tree. Команд git add и git commit не было - коммиты собирали руками из этих кирпичей. Линус Торвальдс сделал сам движок, а удобную обёртку над ним пилили дальше.

Сейчас порядок такой:

  • Plumbing - команды, у которых стабильный, документированный вход и выход. Они никогда не меняются в смысле формата вывода
    • на них завязаны скрипты и сторонние инструменты (GitHub, IDE-плагины).
    • Porcelain - команды, которые удобны человеку. Их формат вывода может меняться между версиями Git: добавили цвет, изменили формулировку, переименовали колонку. Парсить их автоматически - плохая идея.

Самый простой способ отличить - git help <команда>. У plumbing первая строка часто звучит как «low-level command», у porcelain - «high-level command».

4.2 Как porcelain устроен изнутри

Поучительно посмотреть, во что разбирается одна знакомая команда. Вот git commit -m "fix typo", разложенный на plumbing:

git commit ─┬─→ git write-tree            # из индекса собрать tree
            ├─→ git rev-parse HEAD        # узнать SHA текущего коммита
            ├─→ git commit-tree           # создать commit-объект
            │       (tree, -p HEAD, -m "fix typo")
            └─→ git update-ref HEAD       # передвинуть текущую ветку

Каждая стрелка - отдельный системный вызов в исходнике Git. Если запустить эти четыре команды руками в правильном порядке, результат будет ровно такой же, как у git commit -m "fix typo". В лабе главы это и сделаем.

Аналогично:

  • git add file.txt → git hash-object -w file.txt + git update-index --add --cacheinfo
  • git checkout main → git update-ref HEAD refs/heads/main + git read-tree -m -u
  • git log → git rev-list HEAD + git cat-file -p <sha> для каждого

Porcelain - это shell-скрипты, превратившиеся со временем в C. Но идея та же: собрать вызов из мелких операций.

4.3 cat-file - швейцарский нож по объектам

git cat-file - главная команда для чтения объектов. У неё несколько флагов, и каждый отвечает за свой вопрос.

bash
git cat-file -t <sha>      # какой тип у объекта
git cat-file -s <sha>      # размер в байтах
git cat-file -p <sha>      # содержимое (pretty-print)
git cat-file -e <sha>      # есть ли такой объект (exit code)

Pretty-print подбирает форматирование под тип. Для blob - это сырые байты. Для tree - список записей. Для commit и tag - текстовое представление со всеми полями.

bash
# Тип ветки main
git cat-file -t main
# commit
# Содержимое первого коммита
git cat-file -p main
# tree 7e3f9a2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f
# parent a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# author ...
# committer ...
#
# Сообщение коммита

Префикс хэша не обязан быть полным. Если в репозитории нет другого объекта с таким же префиксом, достаточно 4–7 первых символов. Это работает и в porcelain (git show 8d0e41), но базируется на plumbing-команде git rev-parse.

См. cat-file.

4.4 rev-parse - как Git расшифровывает имена

HEAD, main, main~3, HEAD^^, :/fix typo - это всё имена, которые в итоге сводятся к одному SHA. Команда, которая делает перевод, - git rev-parse.

bash
git rev-parse HEAD
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
git rev-parse main~2
# 7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d

Синтаксис коротко:

  • HEAD - куда сейчас смотрит указатель.
  • <branch> - последний коммит в ветке.
  • <sha> - конкретный коммит по хэшу.
  • <ref>^ - родитель. Для merge-коммита ^1 это первый родитель, ^2 - второй.
    • <ref>~N - N-й предок по первому родителю. HEAD~3 - три коммита назад.
    • <ref>^{tree}, <ref>^{commit} - переключение типа. Если ref указывает на tag, а нужен коммит - <tag>^{commit}.
    • :/pattern - последний коммит, в сообщении которого встречается pattern.

rev-parse сам не делает с объектом ничего - только переводит имя в SHA. Дальше этим SHA пользуются другие команды.

Полезный отдельный режим - --show-toplevel:

bash
git rev-parse --show-toplevel
# /home/user/projects/my-portfolio

Это путь к корню репозитория из любой подпапки. Часто используется в hooks и скриптах.

См. rev-parse.

4.5 ls-tree - заглянуть внутрь tree

Если cat-file -p показывает tree более-менее читаемо, то для машинной обработки удобнее ls-tree:

bash
git ls-tree HEAD
# 100644 blob 5f7e9c12...    README.md
# 100644 blob 8a3f2e91...    index.html
# 40000  tree e2b5a91f...    images
# 100644 blob b1d4a7e0...    style.css

Это содержимое корневого tree из текущего коммита. Если нужно зайти в поддиректорию:

bash
git ls-tree HEAD images/
# 100644 blob 9f8a7b6c...    logo.png
# 100644 blob 3e2d1c0b...    banner.jpg

Полезные флаги:

  • -r - рекурсивно, спуститься во все поддеревья.
  • --name-only - только имена файлов.
  • --full-tree - игнорировать текущую рабочую директорию, показать всё дерево из корня репозитория.

Комбинация для полного списка путей в коммите:

bash
git ls-tree -r --name-only HEAD
# README.md
# index.html
# images/logo.png
# images/banner.jpg
# style.css

Это часто нужно скриптам - пройтись по всем файлам коммита, не трогая рабочую копию.

4.6 hash-object и update-index - пишем в индекс руками

Эти две команды вместе делают то, что в porcelain называется git add. Сначала - создать blob, потом - записать запись в индекс.

bash
# 1. Создать blob из файла
git hash-object -w README.md
# 5f7e9c121234567890abcdef1234567890abcdef
# 2. Записать запись в индекс
git update-index --add --cacheinfo 100644 \
    5f7e9c121234567890abcdef1234567890abcdef \
    README.md

Что делает --cacheinfo: добавляет в индекс запись «по пути README.md лежит blob с таким-то SHA и правами 100644», даже если самого файла на диске нет. Это плумбинг - он не пытается прочитать рабочую копию.

После этого git status покажет файл как stagged. Можно коммитить.

Альтернативный режим - git update-index --add README.md - эквивалентен git add README.md: прочитает файл с диска, создаст blob, добавит запись. Но это уже шаг ближе к porcelain.

git update-index ещё умеет:

  • --remove - убрать запись из индекса.
  • --refresh - пересчитать stat'ы, отметить файлы как неизменённые, чтобы git status отработал быстрее.
    • --assume-unchanged <file> - флаг «этот файл не трогай, я знаю что делаю». Удобно для локальных правок в файле, который отслеживается, но менять его в общей истории не нужно.

4.6.1 Подводный камень: assume-unchanged ≠ skip-worktree

Есть два похожих флага у update-index, которые путают.

  • --assume-unchanged - «я обещаю, что не меняю этот файл». Нужен для производительности на больших репозиториях. Если файл всё-таки изменить - Git может это пропустить, и при git pull будут странности.
    • --skip-worktree - «считай, что файла нет в рабочей копии». Используется для локальных override (например, конфига с реальными credentials, который не должен попадать в общий репозиторий).

Оба флага локальные, в репозитории не сохраняются. Снять - --no-assume-unchanged или --no-skip-worktree.

Канонически правильный способ скрыть локальные файлы - это .gitignore (если файла нет в репо) или хранить шаблон (config.example.json) и копировать руками. Эти флаги - для редких случаев, когда других вариантов нет.

4.7 write-tree и commit-tree - собираем коммит

После того как индекс заполнен, нужны две команды, чтобы из него получился коммит.

git write-tree - собрать из индекса tree-объект и записать в .git/objects/:

bash
git write-tree
# 7e3f9a2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f

Если в индексе есть подкаталоги, write-tree рекурсивно создаст все нужные tree-объекты для них. На выходе - SHA корневого tree.

git commit-tree - создать commit-объект на основе tree:

bash
echo "Первый коммит" | git commit-tree 7e3f9a2b
# b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7

Полная форма:

bash
echo "сообщение" | git commit-tree <tree-sha> \
    -p <parent-sha> \
    -p <second-parent-sha>   # для merge-коммитов

Флагов -p может быть несколько (или ни одного - это будет root commit). Автор и коммиттер берутся из user.name и user.email.

После commit-tree коммит уже создан и лежит в .git/objects/, но ни одна ветка о нём не знает. Если на него не повесить ссылку - он будет «висячим» и через какое-то время попадёт под git gc. Поэтому следующий шаг - update-ref.

4.8 update-ref - двигаем ветки и HEAD

Ветки в Git - это файлы в .git/refs/heads/. У каждого внутри 40-символьный SHA. Двигать ветку - это переписывать этот файл. Соответствующая plumbing-команда - git update-ref.

bash
git update-ref refs/heads/main b8c9d0e1

Это эквивалентно echo b8c9d0e1 > .git/refs/heads/main, но делает три полезные вещи дополнительно:

  1. Проверяет, что такой коммит существует.
  2. Записывает старое значение в reflog (через это потом работает git reflog).
  3. Атомарно обновляет файл через временный + rename, чтобы при падении Git не оставалось битого ref'а.

Двигать HEAD - отдельный случай. Если HEAD указывает на ветку (ref: refs/heads/main), то менять надо саму ветку, а не HEAD. Если HEAD в detached состоянии (указывает прямо на SHA) - то git update-ref HEAD <sha> меняет HEAD напрямую.

Принудительно переключить HEAD на другую ветку:

bash
git symbolic-ref HEAD refs/heads/feature

Эта команда меняет тип HEAD (с прямого SHA на ссылку и обратно). Она и есть основа того, что делает git checkout после того, как обновит рабочую копию.

4.9 Откуда брать список plumbing-команд

Полный список - в выводе git help -a. Команды разделены на разделы по семантике. Plumbing - в самом конце, в разделе «Low-level Commands».

Категорий несколько:

  • Manipulators - создают и меняют объекты (hash-object, write-tree, commit-tree, mktag, mktree).
    • Interrogators - читают объекты, не меняя (cat-file, ls-tree, ls-files, rev-list, rev-parse).
    • Sync - обмен с remote'ом на низком уровне (fetch-pack, send-pack, upload-pack, receive-pack).
    • Internal Helpers - служебные (check-ignore, verify-pack, fsck, gc).

Обычно из всего этого реально нужны примерно десять команд - именно их мы и разобрали в этой главе. Остальные - на случай специфических задач: реализация Git-сервера, написание pre-receive хука, miграция данных из другой VCS.

Уроки в sandbox

lab-4.1. Реализовать git log через plumbing

Цель - собрать упрощённый аналог git log --oneline из плумбинг-команд. Скрипт получает имя ветки, проходит по родителям и печатает по строчке на каждый коммит.

Это упражнение убирает магию из git log: видно, как обход графа коммитов превращается в цепочку cat-file.

  1. В существующем репозитории (можно использовать тот, что сделали в лабе главы 2) узнай SHA вершины ветки: git rev-parse HEAD. Запиши его в переменную: cur=$(git rev-parse HEAD).

  2. Прочитай commit-объект для этого SHA: git cat-file -p $cur. В выводе видно строку parent <sha> (или несколько для merge-коммитов) и сообщение коммита.

  3. Извлеки первое сообщение (первая строка после пустой строки): subject=$(git cat-file -p $cur | sed -n '/^$/,$p' | sed '1d' | head -1). Запиши строку лога: echo "${cur:0:7} $subject".

  4. Найди родителя: parent=$(git cat-file -p $cur | grep '^parent ' | head -1 | cut -d' ' -f2). Если переменная пустая - дошли до корневого коммита, цикл закончился.

  5. Заверни всё в while-цикл, в каждой итерации обновляй cur=$parent. На выходе получишь полный лог.

  6. Сверь свой вывод с git log --oneline. Должно совпадать построчно (различаться может только длина SHA - у тебя 7 символов, у git log тоже 7 по умолчанию).

  7. Прокомментируй каждую plumbing-команду в скрипте: что она делает, какой объект читает или меняет. Это и есть полный конспект главы.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Plumbing - низкоуровневые команды со стабильным выводом. Porcelain - высокоуровневые обёртки для человека, формат вывода может меняться.
  • Каждая порцелайн-команда разбирается на цепочку plumbing-вызовов. `git commit` = write-tree + commit-tree + update-ref.
  • `cat-file` читает любые объекты по SHA. `-t` показывает тип, `-p` - содержимое, `-s` - размер.
  • `rev-parse` переводит имена (HEAD, main~2, :/typo) в SHA. Все porcelain-команды внутри используют его.
  • `ls-tree` показывает содержимое tree-объекта, удобно для машинной обработки.
  • `hash-object` создаёт blob, `update-index` записывает запись в индекс. Вместе - это `git add`.
  • `write-tree` собирает tree из индекса, `commit-tree` создаёт commit-объект, `update-ref` передвигает ветку. Вместе - это `git commit`.
  • Для сторонних инструментов и скриптов всегда использовать plumbing - у него стабильный API.

Контрольные вопросы

  1. В чём разница между plumbing и porcelain с точки зрения скрипта, который автоматизирует работу с Git?

    Показать ответ

    У plumbing формат вывода зафиксирован - он не меняется между версиями Git, его можно безопасно парсить. У porcelain формат рассчитан на человека: могут добавлять цвет, переименовывать колонки, менять формулировки. Скрипт, который парсит git status, рискует сломаться при обновлении Git. Скрипт, который использует git ls-files или git rev-parse, - не сломается. У части porcelain-команд (в частности git status, git push, git blame, git worktree list) есть флаг --porcelain (да, именно так названный) - он переключает их в стабильный формат, пригодный для машин. Общим интерфейсом он не является: у большинства porcelain-команд такого флага нет, и для них надёжнее звать соответствующий plumbing.

  2. Что произойдёт, если запустить `git commit-tree`, но не вызвать `git update-ref` после?

    Показать ответ

    Коммит-объект будет создан и записан в .git/objects/, но ни одна ветка о нём не узнает. Это так называемый «висячий» (dangling) коммит. Найти его можно через git fsck --lost-found. Если в течение времени жизни reflog (по умолчанию 30 дней) этот SHA не попадёт ни на одну ссылку, его удалит git gc. Если же успеть повесить ссылку - например, git branch rescue <sha> - коммит «оживёт» и станет частью истории.

  3. Когда `git rev-parse HEAD~3` даст ошибку?

    Показать ответ

    Если у текущей ветки меньше трёх коммитов в истории по первому родителю. Например, если репозиторий только-только создан и в нём один коммит - HEAD~1 уже даст «unknown revision». Это нормальный exit code (128), и rev-parse выведет ошибку в stderr.

  4. Почему `git update-ref` лучше, чем `echo <sha> > .git/refs/heads/main`?

    Показать ответ

    Три причины. Первая - update-ref проверяет, что коммит с таким SHA существует; прямая запись может оставить ссылку, которая указывает в никуда. Вторая - update-ref пишет в reflog старое значение, без этого git reflog не сможет восстановить прошлое состояние. Третья - атомарность: update-ref пишет во временный файл и атомарно его переименовывает, поэтому при падении в середине операции ref не окажется в полу-записанном состоянии.

  5. Как через plumbing проверить, существует ли в репозитории объект с конкретным SHA, не выводя его содержимого?

    Показать ответ

    git cat-file -e <sha>. Эта команда не печатает ничего, только возвращает exit code: 0 - объект есть, ненулевой - нет. Используется в скриптах: if git cat-file -e <sha> 2>/dev/null; then ... fi.

← Предыдущая03-object-modelСледующая →05-three-areas
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки