Git продвинутый

Git advanced — набор продвинутых инструментов и практик Git для диагностики регрессий, автоматизации проверок, восстановления потерянных ссылок и работы со связанными репозиториями. В этой справке также рассматривается SOPS — инструмент шифрования секретов, позволяющий безопаснее хранить зашифрованные конфигурации рядом с кодом.

Git хранит историю как граф коммитов. Ветки и теги являются ссылками на объекты этого графа, а HEAD указывает на текущую позицию. Понимание этой модели помогает использовать git bisect, git reflog, hooks и другие продвинутые механизмы без лишнего риска.

A---B---C---D  main
         \
          E---F  feature

Основные инструменты из справки:

Содержание


git bisect для поиска ошибок

git bisect помогает определить коммит, в котором появилась регрессия. Вместо последовательной проверки каждого коммита Git использует бинарный поиск: выбирает коммит примерно в середине заданного диапазона, после чего пользователь или тест отмечает его как исправный либо ошибочный.

Для диапазона из 1024 коммитов обычно потребуется не более чем около 10 проверок:

2^10 = 1024

Условия использования

Для эффективного поиска нужны две известные точки:

Также необходим воспроизводимый способ определить состояние коммита: ручная проверка, unit-тест, integration-тест или отдельный диагностический скрипт.

Ручной поиск

Запуск поиска:

git bisect start

Текущий коммит можно сразу отметить как ошибочный:

git bisect bad

Известный исправный коммит задаётся хешем, тегом или другой ссылкой:

git bisect good v2.3.0

Git переключит рабочее дерево на промежуточный коммит. После проверки нужно отметить результат:

git bisect good

или:

git bisect bad

Действия повторяются, пока Git не определит первый ошибочный коммит:

<hash> is the first bad commit

После завершения необходимо вернуться к исходной ветке:

git bisect reset

Полный пример:

git bisect start
git bisect bad HEAD
git bisect good v2.3.0

# Git переключает рабочее дерево на проверяемый коммит.
./run-regression-check.sh

# Если ошибка присутствует:
git bisect bad

# Если ошибки нет:
git bisect good

# После нахождения коммита:
git bisect reset

Указание границ одной командой

Текущий коммит можно объявить ошибочным, а конкретный коммит — исправным:

git bisect start HEAD v2.3.0

Первый аргумент после start считается bad, последующие — good. Более явная последовательность команд часто лучше читается в инструкции по диагностике.

Автоматический поиск через git bisect run

Если результат можно определить скриптом или тестом, поиск автоматизируется:

git bisect start
git bisect bad HEAD
git bisect good v2.3.0
git bisect run ./run-regression-check.sh
git bisect reset

Git запускает команду на каждом выбранном коммите и интерпретирует код возврата.

Код возврата Значение для git bisect
0 коммит исправен — good
1–127, кроме 125 коммит ошибочен — bad
125 коммит невозможно проверить — skip
128 и выше выполнение прервано или произошла специальная ошибка

Пример скрипта:

#!/usr/bin/env bash
set -euo pipefail

npm ci
npm test -- --runInBand tests/regression.test.js

Если тест завершается с 0 только при отсутствии регрессии, его можно напрямую передать в git bisect run:

git bisect run npm test -- --runInBand tests/regression.test.js

Подготовка проекта перед тестом

Между коммитами могут меняться зависимости, схема базы данных или способ сборки. Поэтому автоматический скрипт должен приводить окружение к состоянию проверяемого коммита:

#!/usr/bin/env bash
set -u

rm -rf build

if ! npm ci; then
  exit 125
fi

if npm test -- --runInBand tests/regression.test.js; then
  exit 0
else
  exit 1
fi

Код 125 означает, что конкретный коммит нельзя корректно проверить. Git пропустит его, но большое количество пропусков может помешать точно определить первый ошибочный коммит.

Проверка конкретного пути

Если известно, что ошибка связана только с определённой частью репозитория, поиск можно ограничить путями:

git bisect start -- src/payment tests/payment

git bisect bad HEAD
git bisect good v2.3.0

После -- указываются пути, изменения которых учитываются при выборе коммитов. Такой режим может сократить диапазон поиска, но его следует применять только при уверенности, что причина находится в указанных файлах.

Просмотр состояния поиска

Текущие отметки:

git bisect log

Пример результата:

git bisect start
git bisect bad 7c9f4d1
git bisect good 19a27ab

Журнал можно сохранить:

git bisect log > bisect.log

И воспроизвести позднее:

git bisect reset
git bisect replay bisect.log

Визуализация оставшихся кандидатов:

git bisect visualize

В терминальном окружении можно явно вызвать:

git bisect visualize --oneline

Пропуск непроверяемого коммита

Если промежуточный коммит не собирается по причине, не связанной с исследуемой ошибкой:

git bisect skip

Можно пропустить несколько ревизий:

git bisect skip <commit-1> <commit-2>

Если рядом с границей находятся только пропущенные коммиты, Git может выдать несколько возможных первых ошибочных коммитов вместо одного.

Ошибка в условии good/bad

Если коммит был отмечен неправильно, можно посмотреть журнал:

git bisect log

Затем завершить поиск и воспроизвести исправленный журнал:

git bisect reset
# Отредактировать bisect.log
git bisect replay bisect.log

Для короткого поиска проще выполнить git bisect reset и начать заново.

Поиск не только «плохого» изменения

Команды good и bad подходят для регрессии, но Git поддерживает собственные термины:

git bisect start --term-old fast --term-new slow

git bisect slow HEAD
git bisect fast v2.3.0

Так можно искать первый коммит, после которого программа стала медленной, изменился формат результата или появилось другое наблюдаемое состояние.

Ограничения git bisect

git bisect особенно эффективен, если:

Поиск усложняется при нестабильных тестах, несовместимых исторических зависимостях, изменениях внешнего API, повреждённых тестовых данных или постепенном ухудшении без чёткой границы.


Продвинутые Git hooks

Git hook — исполняемый файл, который Git вызывает при определённом событии. Hooks можно использовать для форматирования, статического анализа, проверки сообщения коммита, запуска тестов, запрета опасных изменений и серверной валидации push.

По умолчанию локальные hooks находятся в каталоге:

.git/hooks/

Git создаёт примеры с суффиксом .sample. Чтобы hook выполнялся, файл обычно должен:

Пример:

chmod +x .git/hooks/pre-commit

Клиентские и серверные hooks

Клиентские hooks работают на машине разработчика.

Hook Когда запускается Типичное применение
pre-commit до создания коммита форматирование, линтинг, поиск секретов
prepare-commit-msg перед открытием редактора сообщения добавление номера задачи, шаблона
commit-msg после подготовки сообщения проверка формата сообщения
post-commit после создания коммита уведомления, локальная автоматизация
pre-rebase перед rebase запрет rebase защищённых веток
post-checkout после checkout/switch обновление зависимостей или окружения
post-merge после merge миграции локальной среды
pre-push перед отправкой объектов тесты, сборка, проверка политики

Серверные hooks выполняются в репозитории, принимающем push.

Hook Назначение
pre-receive единая проверка всего push до обновления ссылок
update отдельная проверка каждой обновляемой ссылки
post-receive действия после успешного обновления ссылок

На Git-хостингах прямой доступ к серверным hooks часто отсутствует. Их роль выполняют правила защищённых веток, required checks, CI/CD и API платформы.

Простой pre-commit

Файл .git/hooks/pre-commit:

#!/usr/bin/env bash
set -euo pipefail

npm run lint
npm test -- --runInBand

Если команда завершится с ненулевым кодом, создание коммита будет отменено.

Полный запуск тестов при каждом коммите может быть слишком медленным. Часто в pre-commit выполняют быстрые проверки изменённых файлов, а полный набор запускают в pre-push и CI.

Проверка только staged-файлов

Коммит содержит данные из индекса, а не обязательно текущее состояние рабочего дерева. Поэтому hook должен по возможности проверять именно staged-версию.

Список добавленных, скопированных и изменённых файлов:

git diff --cached --name-only --diff-filter=ACM

Пример проверки shell-скриптов:

#!/usr/bin/env bash
set -euo pipefail

mapfile -d '' files < <(
  git diff --cached --name-only --diff-filter=ACM -z -- '*.sh'
)

if ((${#files[@]} == 0)); then
  exit 0
fi

shellcheck -- "${files[@]}"

Параметр -z разделяет имена нулевым байтом и позволяет корректно обрабатывать пробелы в путях.

Важно: если рабочая копия файла отличается от staged-версии, инструмент, читающий файл с диска, может проверить не тот контент. Надёжный hook может извлекать staged-содержимое через git show :path во временный каталог или использовать инструмент, умеющий работать с индексом.

Проверка сообщения коммита через commit-msg

Hook получает путь к файлу с сообщением первым аргументом:

#!/usr/bin/env bash
set -euo pipefail

message_file="$1"
pattern='^(feat|fix|docs|style|refactor|test|build|ci|chore)(\([a-z0-9._/-]+\))?!?: .{1,72}$'

subject="$(head -n 1 "$message_file")"

if ! grep -Eq "$pattern" <<< "$subject"; then
  echo "Некорректный заголовок коммита:" >&2
  echo "  $subject" >&2
  echo "Ожидаемый пример: feat(api): add health endpoint" >&2
  exit 1
fi

Такой hook реализует упрощённую проверку соглашения Conventional Commits.

pre-push и данные из stdin

pre-push получает имя и URL remote как аргументы. Информация об отправляемых ссылках передаётся через стандартный ввод:

<local-ref> <local-oid> <remote-ref> <remote-oid>

Пример:

#!/usr/bin/env bash
set -euo pipefail

remote_name="$1"
remote_url="$2"
zero=0000000000000000000000000000000000000000

while read -r local_ref local_oid remote_ref remote_oid; do
  # Удаление remote-ссылки: local_oid состоит из нулей.
  if [[ "$local_oid" == "$zero" ]]; then
    continue
  fi

  if [[ "$remote_ref" == "refs/heads/main" ]]; then
    echo "Перед отправкой main выполняются тесты..."
    npm test -- --runInBand
  fi
done

Серверный pre-receive

pre-receive читает строки следующего формата:

<old-value> <new-value> <ref-name>

Пример запрета прямого обновления основной ветки:

#!/usr/bin/env bash
set -euo pipefail

while read -r old_oid new_oid ref_name; do
  if [[ "$ref_name" == "refs/heads/main" ]]; then
    echo "Прямые push в main запрещены" >&2
    exit 1
  fi
done

В реальном процессе такой запрет обычно реализуется защищёнными ветками на Git-платформе, поскольку они лучше интегрированы с pull/merge requests и аудитом.

Общий каталог hooks через core.hooksPath

Содержимое .git/hooks не отслеживается самим Git. Для хранения hooks в репозитории можно создать, например, каталог .githooks:

project/
├── .githooks/
│   ├── pre-commit
│   ├── commit-msg
│   └── pre-push
└── ...

Затем настроить путь:

git config core.hooksPath .githooks

Проверка:

git config --get core.hooksPath

Настройка является локальной и не активируется автоматически после clone. Её можно включать bootstrap-скриптом:

#!/usr/bin/env bash
set -euo pipefail

git config core.hooksPath .githooks
chmod +x .githooks/*
echo "Git hooks установлены"

Не следует незаметно изменять глобальную конфигурацию пользователя:

# Осторожно: влияет на все репозитории пользователя.
git config --global core.hooksPath /some/global/path

Обход hooks

Некоторые клиентские hooks можно пропустить параметром --no-verify:

git commit --no-verify
git push --no-verify

Поэтому локальный hook — удобная быстрая обратная связь, но не окончательная граница контроля. Критические проверки необходимо повторять в CI или на стороне сервера.

Безопасность hooks

Hooks являются исполняемым кодом. Перед подключением hooks из репозитория необходимо просмотреть их содержимое. Опасны сценарии, которые:

Полезные требования к hooks:


SOPS — шифрование секретов в репозитории

SOPS — инструмент для шифрования файлов конфигурации. Он поддерживает структурированные форматы, в том числе YAML и JSON, и может шифровать значения, сохраняя ключи структуры в читаемом виде. Это упрощает review изменений и интеграцию с GitOps-процессами.

Условный зашифрованный YAML может выглядеть так:

database:
  username: ENC[AES256_GCM,data:...,type:str]
  password: ENC[AES256_GCM,data:...,type:str]
sops:
  # Метаданные SOPS и сведения о получателях ключа.
  ...

SOPS шифрует данные случайным ключом файла, а этот ключ защищает одним или несколькими механизмами управления ключами. В зависимости от конфигурации могут применяться облачные KMS, PGP или age.

Что даёт SOPS

SOPS позволяет:

SOPS не делает секрет безопасным после расшифрования. Открытые значения могут попасть в логи, историю shell, временные файлы, артефакты CI, process list или состояние инфраструктурного инструмента. Необходимо защищать весь путь использования секрета.

Шифрование с age

age использует пару ключей: публичный получатель применяется для шифрования, приватный ключ — для расшифрования.

Создание ключа:

age-keygen -o keys.txt

Пример результата:

# created: 2026-09-24T12:00:00Z
# public key: age1examplepublickey...
AGE-SECRET-KEY-1EXAMPLEPRIVATEKEY...

Файл с приватным ключом нельзя добавлять в Git. Публичный получатель можно хранить в конфигурации проекта.

Шифрование файла:

sops --encrypt \
  --age age1examplepublickey... \
  secrets.yaml > secrets.enc.yaml

Расшифрование в stdout:

SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt" \
  sops --decrypt secrets.enc.yaml

Редактирование зашифрованного файла:

sops secrets.enc.yaml

SOPS расшифрует данные во временное представление, откроет редактор и зашифрует результат после сохранения.

Конфигурация .sops.yaml

Правила создания файлов удобно хранить в .sops.yaml в корне репозитория:

creation_rules:
  - path_regex: ^secrets/dev/.*\.ya?ml$
    age: >-
      age1devrecipientexample...

  - path_regex: ^secrets/prod/.*\.ya?ml$
    age: >-
      age1prodrecipientexample1...,
      age1prodrecipientexample2...

Теперь для подходящего пути достаточно выполнить:

sops --encrypt secrets/prod/app.yaml > secrets/prod/app.enc.yaml

Правила проверяются сверху вниз; обычно применяется первое подходящее правило. Регулярные выражения должны быть достаточно точными, чтобы production-файл не был случайно зашифрован ключом development-окружения.

Пример с облачным KMS

Концептуальный пример правила для AWS KMS:

creation_rules:
  - path_regex: ^environments/prod/.*\.ya?ml$
    kms: >-
      arn:aws:kms:eu-central-1:123456789012:key/11111111-2222-3333-4444-555555555555

Для расшифрования процессу нужны разрешения KMS. Доступ лучше выдавать роли workload или CI, а не долговременным пользовательским ключам.

Аналогичный подход возможен с другими поддерживаемыми системами управления ключами. Конкретные идентификаторы и параметры зависят от выбранного провайдера.

Шифрование только выбранных полей

Иногда часть конфигурации должна оставаться открытой. Например, можно шифровать значения под ключами, соответствующими регулярному выражению:

sops --encrypt \
  --encrypted-regex '^(password|token|secret)$' \
  config.yaml > config.enc.yaml

Исходный файл:

app:
  host: app.example.internal
  port: 8443
  password: change-me

После шифрования host и port могут остаться читаемыми, а password будет зашифрован. Параметр отбора необходимо тестировать: ошибочное выражение может оставить чувствительное поле открытым.

Правило можно зафиксировать в .sops.yaml:

creation_rules:
  - path_regex: ^config/.*\.ya?ml$
    encrypted_regex: '^(password|token|secret|private_key)$'
    age: age1examplepublickey...

Работа с JSON и dotenv

JSON:

sops --encrypt config.json > config.enc.json
sops --decrypt config.enc.json

Для dotenv-файлов следует явно соблюдать поддерживаемый формат и не путать зашифрованный файл с обычным .env:

sops --encrypt --input-type dotenv --output-type dotenv \
  .env > .env.enc

Расшифрование:

sops --decrypt --input-type dotenv --output-type dotenv .env.enc

Передача секрета процессу без постоянного файла

Если приложение умеет читать конфигурацию из stdin:

sops --decrypt secrets.enc.yaml | ./app --config -

Если нужен временный файл, следует ограничить права и гарантировать удаление:

#!/usr/bin/env bash
set -euo pipefail

secret_file="$(mktemp)"
chmod 600 "$secret_file"
trap 'rm -f "$secret_file"' EXIT

sops --decrypt secrets.enc.yaml > "$secret_file"
./app --config "$secret_file"

Некоторые варианты SOPS позволяют выполнять команду с расшифрованным содержимым через специальные режимы exec. Перед использованием необходимо проверить семантику конкретной версии: передаются ли данные через файл, FIFO или переменную окружения, и может ли значение стать видимым другим процессам.

Ротация получателей

Добавление или удаление KMS-ключей и age-получателей требует обновления обёртки ключа данных в существующих файлах. Типичный процесс:

# Сначала изменить .sops.yaml, затем обновить ключи файла.
sops updatekeys secrets/prod/app.enc.yaml

После удаления получателя необходимо убедиться, что у него не осталось:

Удаление получателя только из нового коммита не лишает его доступа к старым версиям, находящимся в истории Git.

Проверка, что открытый секрет не попал в Git

Пример pre-commit, запрещающего добавление незашифрованных файлов в каталог secrets:

#!/usr/bin/env bash
set -euo pipefail

failed=0

while IFS= read -r -d '' file; do
  case "$file" in
    *.enc.yaml|*.enc.yml|*.enc.json)
      ;;
    *)
      echo "Незашифрованный файл в secrets/: $file" >&2
      failed=1
      ;;
  esac
done < <(git diff --cached --name-only --diff-filter=ACM -z -- 'secrets/**')

exit "$failed"

Это только дополнительная защита. Следует также использовать secret scanning в CI или на Git-платформе.

Если секрет уже попал в историю

Одного удаления файла новым коммитом недостаточно. Нужно:

  1. Немедленно отозвать или ротировать секрет.
  2. Оценить, куда репозиторий был клонирован и какие CI-системы его обработали.
  3. При необходимости очистить историю специализированным инструментом.
  4. Согласовать принудительное обновление веток и повторное клонирование.
  5. Проверить логи, кэши, артефакты и резервные копии.

Главное действие — ротация. Переписывание Git-истории не гарантирует уничтожение всех копий секрета.

Рекомендации по SOPS


git reflog и восстановление данных

Reflog — локальный журнал изменений ссылок. Он фиксирует перемещения HEAD, веток и некоторых других ссылок: commit, merge, rebase, reset, checkout и другие операции.

Просмотр журнала HEAD:

git reflog

Пример:

7a1c4f2 HEAD@{0}: reset: moving to HEAD~2
48b61ad HEAD@{1}: commit: add payment validation
c32ab90 HEAD@{2}: commit: update API client

HEAD@{0} — текущая позиция, HEAD@{1} — предыдущая зафиксированная позиция HEAD и так далее.

Reflog конкретной ветки

git reflog show main

С датами:

git reflog --date=iso

Все доступные журналы:

git reflog show --all

Восстановление после ошибочного reset --hard

Допустим, была выполнена команда:

git reset --hard HEAD~2

Нужно найти старую позицию:

git reflog

Затем сначала создать защитную ветку:

git branch recovery-before-reset 48b61ad

Проверить содержимое:

git show --stat recovery-before-reset
git log --oneline --decorate recovery-before-reset -n 5

После проверки можно вернуть нужную ветку:

git switch main
git reset --hard recovery-before-reset

Если общая ветка уже опубликована, переписывание её истории может быть нежелательно. Безопаснее восстановить изменения новым коммитом или через git cherry-pick:

git cherry-pick 48b61ad

Восстановление удалённой ветки

Удаление ветки:

git branch -D feature/payment

Если коммит недавно был доступен через HEAD или другую ссылку, найти его можно так:

git reflog --all --date=iso

Восстановление:

git branch feature/payment <commit-hash>

Иногда полезен журнал самой ветки, если он ещё существует:

git reflog show feature/payment

Но после удаления ссылки этот журнал может быть недоступен обычным способом, поэтому --all, журнал HEAD и поиск недостижимых объектов особенно полезны.

Восстановление после неудачного rebase

Rebase перемещает ветку на новые коммиты, но прежнее положение обычно сохраняется в reflog:

git reflog

Перед восстановлением желательно сохранить текущее состояние:

git branch backup-after-rebase

Возврат к позиции до rebase:

git reset --hard HEAD@{5}

Часто Git также устанавливает ORIG_HEAD перед потенциально опасными операциями:

git show ORIG_HEAD
git reset --hard ORIG_HEAD

На ORIG_HEAD не следует полагаться как на долгосрочную резервную ссылку: последующая команда может обновить её.

Восстановление отдельного файла

Если нужно вернуть файл из найденного коммита, не меняя всю ветку:

git restore --source=<commit-hash> -- path/to/file

Чтобы сразу поместить восстановленную версию и в рабочее дерево, и в индекс:

git restore --source=<commit-hash> --staged --worktree -- path/to/file

Перед коммитом следует проверить изменения:

git diff
git diff --cached

Поиск потерянных объектов через git fsck

Если нужный коммит не удаётся найти в reflog:

git fsck --full --no-reflogs --unreachable

Или поиск dangling-объектов:

git fsck --full --no-reflogs --dangling

Пример:

dangling commit 48b61ad...

Содержимое можно изучить:

git show 48b61ad

И закрепить новой веткой:

git branch recovered-work 48b61ad

Пока объект не закреплён ссылкой, Git может удалить его при сборке мусора.

Ограничения reflog

Reflog:

Настройки сроков хранения можно посмотреть так:

git config --get gc.reflogExpire
git config --get gc.reflogExpireUnreachable

Без явных настроек Git использует собственные значения по умолчанию. Агрессивная очистка может уменьшить окно восстановления:

git gc --prune=now

Не следует запускать подобную команду, пока ведётся восстановление.

Безопасный порядок восстановления

Рекомендуемый порядок:

  1. Остановить команды, которые могут запустить очистку или переписать ссылки.
  2. Посмотреть git status, git reflog --all и граф истории.
  3. Создать новую ветку или тег на найденном коммите.
  4. Проверить файлы, diff и историю.
  5. Только после проверки изменять основную ветку.

Полезная команда для обзора:

git log --graph --oneline --decorate --all

Git submodules

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

Пример структуры:

application/
├── .gitmodules
├── src/
└── vendor/
    └── shared-library/   # отдельный Git-репозиторий

Родительский проект фиксирует точный коммит shared-library. Это обеспечивает воспроизводимость, но обновление submodule требует отдельного действия и отдельного коммита в родительском репозитории.

Добавление submodule

git submodule add https://example.com/team/shared-library.git \
  vendor/shared-library

После этого Git изменит .gitmodules и добавит gitlink:

git status
git add .gitmodules vendor/shared-library
git commit -m "build: add shared library submodule"

Пример .gitmodules:

[submodule "vendor/shared-library"]
    path = vendor/shared-library
    url = https://example.com/team/shared-library.git

Клонирование проекта с submodules

Сразу со всеми вложенными репозиториями:

git clone --recurse-submodules \
  https://example.com/team/application.git

Если обычный clone уже выполнен:

git submodule update --init --recursive

--recursive нужен, если submodule сам содержит submodules.

Просмотр состояния

git submodule status

Пример:

 8af31c2 vendor/shared-library (v1.4.0)

Символ перед хешем может указывать на особое состояние:

Обновление до коммита, записанного в родительском репозитории

После переключения ветки или pull:

git submodule update --init --recursive

Удобный вариант:

git pull --recurse-submodules
git submodule update --init --recursive

Обновление submodule на новую версию

Перейти во вложенный репозиторий и выбрать нужный коммит:

cd vendor/shared-library
git fetch --tags
git switch --detach v1.5.0
cd ../..

Родительский репозиторий увидит изменение указателя:

git diff --submodule
git add vendor/shared-library
git commit -m "build: update shared library to v1.5.0"

Вместо тега можно переключиться на ветку и получить обновления, но родительский репозиторий всё равно зафиксирует конкретный commit hash, а не плавающую ветку.

Отслеживание ветки

В .gitmodules можно указать рекомендуемую ветку:

[submodule "vendor/shared-library"]
    path = vendor/shared-library
    url = https://example.com/team/shared-library.git
    branch = main

Обновление с remote:

git submodule update --remote --merge

После этого указатель submodule в родительском проекте необходимо закоммитить. Автоматическое следование за последним коммитом ветки без фиксации нового указателя противоречило бы воспроизводимости сборки.

Выполнение команды во всех submodules

git submodule foreach 'git status --short --branch'

Рекурсивно:

git submodule foreach --recursive 'git status --short'

Изменение URL

После изменения .gitmodules локальные настройки синхронизируются:

git submodule sync --recursive
git submodule update --init --recursive

Удаление submodule

Современный безопасный порядок:

git submodule deinit -f -- vendor/shared-library
git rm -f vendor/shared-library
rm -rf .git/modules/vendor/shared-library
git commit -m "build: remove shared library submodule"

Перед удалением необходимо проверить, что внутри submodule нет незакоммиченных изменений:

 git -C vendor/shared-library status

Путь в .git/modules следует удалять осторожно и только после проверки.

Detached HEAD в submodule

После git submodule update вложенный репозиторий обычно находится в состоянии detached HEAD, поскольку родительский проект указывает на конкретный коммит.

Если нужно разрабатывать внутри submodule:

cd vendor/shared-library
git switch main
git pull --ff-only
# Внести изменения, создать commit и push.

После этого в родительском репозитории нужно зафиксировать новый указатель submodule.

Если создать commit в detached HEAD и затем переключиться, коммит можно потерять из видимой истории. До выхода следует создать ветку:

git switch -c fix/submodule-bug

При необходимости такой коммит можно найти через reflog самого submodule:

git -C vendor/shared-library reflog

Submodules в CI

CI должен явно инициализировать вложенные репозитории:

git submodule update --init --recursive

Также нужны права доступа ко всем приватным remote. Следует учитывать:

Ограничения и альтернативы

Преимущества submodules:

Недостатки:

Возможные альтернативы:

Submodule оправдан, когда действительно требуется связать независимые Git-репозитории по точному коммиту. Для обычной программной зависимости пакетный менеджер часто удобнее.


Совместное применение инструментов

Пример структуры репозитория

platform-config/
├── .githooks/
│   ├── commit-msg
│   ├── pre-commit
│   └── pre-push
├── .sops.yaml
├── .gitmodules
├── scripts/
│   ├── bootstrap.sh
│   └── regression-test.sh
├── secrets/
│   ├── dev/
│   │   └── application.enc.yaml
│   └── prod/
│       └── application.enc.yaml
├── deployment/
└── vendor/
    └── policy-library/

Bootstrap-скрипт

scripts/bootstrap.sh:

#!/usr/bin/env bash
set -euo pipefail

root="$(git rev-parse --show-toplevel)"
cd "$root"

git config core.hooksPath .githooks
chmod +x .githooks/*
git submodule update --init --recursive

echo "Локальное окружение Git подготовлено"

Hook для защиты секретов

.githooks/pre-commit:

#!/usr/bin/env bash
set -euo pipefail

errors=0

while IFS= read -r -d '' file; do
  case "$file" in
    *.enc.yaml|*.enc.yml|*.enc.json)
      # Проверка наличия метаданных SOPS в staged-содержимом.
      if ! git show ":$file" | grep -qE '(^|\")sops(:|\")'; then
        echo "Файл похож на зашифрованный, но не содержит метаданных SOPS: $file" >&2
        errors=1
      fi
      ;;
    *)
      echo "В каталоге secrets разрешены только SOPS-файлы: $file" >&2
      errors=1
      ;;
  esac
done < <(git diff --cached --name-only --diff-filter=ACM -z -- 'secrets/**')

exit "$errors"

Проверка сигнатуры не доказывает, что каждое чувствительное поле зашифровано. Необходимо дополнительно применять secret scanner и правила review.

Поиск регрессии автоматическим тестом

scripts/regression-test.sh:

#!/usr/bin/env bash
set -u

# Исторический коммит может использовать другую структуру проекта.
if [[ ! -f package-lock.json ]]; then
  exit 125
fi

if ! npm ci >/dev/null 2>&1; then
  exit 125
fi

npm test -- --runInBand tests/config-loader.test.js

Запуск:

git bisect start
git bisect bad HEAD
git bisect good v1.8.0
git bisect run ./scripts/regression-test.sh
git bisect reset

Восстановление после ошибочного изменения истории

Сначала сохранить текущее положение и найти прежний коммит:

git branch backup-current-state
git reflog --date=iso

Создать ветку восстановления:

git branch recovery <commit-hash>

Сравнить состояния:

git diff main..recovery
git log --graph --oneline --decorate --all

После проверки выбрать подходящую стратегию:

# Вернуть один коммит без переписывания общей истории:
git cherry-pick <commit-hash>

# Либо локально вернуть указатель ветки:
git reset --hard recovery

Практические рекомендации

Для git bisect

Для Git hooks

Для SOPS

Для git reflog

Для submodules


Краткая памятка

git bisect

git bisect start
git bisect bad HEAD
git bisect good <known-good-commit>
git bisect run ./test.sh
git bisect log
git bisect reset

Hooks

git config core.hooksPath .githooks
chmod +x .githooks/*
git commit --no-verify       # обход части локальных hooks
git push --no-verify         # обход pre-push

SOPS

sops --encrypt secrets.yaml > secrets.enc.yaml
sops --decrypt secrets.enc.yaml
sops secrets.enc.yaml
sops updatekeys secrets.enc.yaml

Reflog и восстановление

git reflog --date=iso
git reflog show --all
git branch recovery <commit-hash>
git restore --source=<commit-hash> -- path/to/file
git fsck --full --no-reflogs --dangling

Submodules

git clone --recurse-submodules <url>
git submodule update --init --recursive
git submodule status
git submodule update --remote --merge
git diff --submodule
git submodule foreach --recursive 'git status --short'

Продвинутые возможности Git наиболее полезны, когда сопровождаются воспроизводимыми тестами, понятными правилами команды и резервными механизмами. git bisect ускоряет локализацию регрессии, hooks дают быструю обратную связь, SOPS снижает риск хранения открытых секретов, reflog помогает вернуть потерянные ссылки, а submodules позволяют зафиксировать точную версию независимого репозитория.