Продвинутый Git
Базовый цикл add–commit–push покрывает 95% рабочего времени. Оставшиеся 5% — это моменты, когда всё пошло не по плану: в прод уехал баг и нужно найти коммит-виновник за десять минут, а не за день; на полпути рефакторинга прилетает срочный хотфикс; в репозиторий попал .env с паролями; рабочая директория грязная, а переключаться нужно сейчас. Эта глава — арсенал для таких ситуаций.
Здесь разберём инструменты, которые отличают опытного инженера: stash со своим стеком, worktree для параллельной работы, bisect как автоматический бинарный поиск, rebase --onto как хирургический инструмент, субмодули и LFS, хуки и подпись коммитов, защиту от утечки секретов и гигиену .git.
git stash: стек припрятанных изменений
Заголовок раздела «git stash: стек припрятанных изменений»git stash (см. git-stash(1)) прячет незакоммиченные изменения (modified + staged) и возвращает рабочую директорию в чистое состояние. Изменения не теряются — они складываются в стек:
СТЕК СТЭШЕЙ (новые сверху):
stash@{0} ← последний stash "WIP on feature-login: подсветка полей"stash@{1} "WIP on main: эксперимент с конфигом"stash@{2} ← самый старый "WIP on main: черновик документации"# что-то начал править, но прилетел хотфиксgit stash # спрятать (по умолчанию без untracked!)git stash -u # спрятать вместе с untracked-файламиgit stash list # посмотреть стек
# вернуться к правкамgit stash pop # применить stash@{0} и удалить его из стекаgit stash apply # применить stash@{0}, оставив в стеке
# навигация по стекуgit stash show -p stash@{2} # показать диф конкретногоgit stash drop stash@{1} # удалить конкретныйgit stash clear # выкинуть весь стекСценарий «прыгнул на хотфикс и вернулся»:
1. на feature-login, правки незакоммичены рабочая папка: [грязная]2. git stash рабочая папка: чистая, на feature-login3. git switch main → правка → commit → push4. git switch feature-login5. git stash pop рабочая папка: [грязная снова, правки вернулись]Важная деталь: stash — не бэкап. Он привязан к конкретному коммиту, на котором создавался, и если история этой ветки ушла вперёд, pop может дать конфликт. Правки дольше пары часов — коммить (хоть WIP-коммит), а не stash.
git worktree: несколько рабочих директорий на один репозиторий
Заголовок раздела «git worktree: несколько рабочих директорий на один репозиторий»Проблема: ты на полпути в большом рефакторинге (грязная рабочая папка), а нужно срочно посмотреть прод. Переключаться нельзя, stash неудобен, клонировать репозиторий заново — дорого (гигабайты .git).
git worktree (см. git-worktree(1)) даёт несколько рабочих директорий, ссылающихся на один .git:
git worktree add ../project-hotfix main # вторая папка на ветке maincd ../project-hotfix# работаешь в «отдельном клоне», но объекты общие
git worktree list# /home/me/project abc1234 [feature-big-refactor]# /home/me/project-hotfix def5678 [main]
git worktree remove ../project-hotfix # убрать, когда не нужноОДИН .git, ДВЕ ПАПКИ:
~/project/ ~/project-hotfix/├── .git/ ◄──────────────────┤ (файл .git-файл-указатель,│ ├── objects/ ◄── ОБЩИЕ а не каталог)│ └── refs/ ◄── ОБЩИЕ ├── .git → файл:├── src/ (refactor) │ "gitdir: /home/me/project/.git/worktrees/..."└── (грязная папка) ├── src/ (main, чистая) └── (готово к хотфиксу)Следствия: «клон» мгновенный (объекты общие), fetch делается один раз на все папки. Ограничение: одна ветка — одна рабочая папка.
git worktree add ../project-v1.2 v1.2.0 # и откат к тегу для отладки рядом с текущей работойКлассические применения: срочный хотфикс во время рефакторинга, «чистая» папка на main для быстрых проверок, сборка старого релиза из тега.
git bisect: бинарный поиск коммита-баговода
Заголовок раздела «git bisect: бинарный поиск коммита-баговода»История: вчера работало, сегодня — нет. Тысяча коммитов между «хорошим» и «плохим». Вместо линейного перебора — бинарный поиск (см. git-bisect(1)): Git сам вычисляет средний коммит, ты говоришь «хорошо/плохо», и за ~10 итераций (для тысячи коммитов!) виновник найден.
git bisect startgit bisect bad # текущий коммит — плохойgit bisect good v1.8.0 # тег v1.8.0 — хороший (где точно работало)# Git переключается на средний коммит:# Bisecting: 512 revisions left to test after this (roughly 9 steps)Дальше — итерации:
ШАГ 1 (из ~9): коммит C512 ── проверяешь ──► ПЛОХО → виновник левееШАГ 2: коммит C256 ── проверяешь ──► ХОРОШО → виновник правееШАГ 3: коммит C384 ── проверяешь ──► ПЛОХО → левееШАГ 4: коммит C320 ── проверяешь ──► ХОРОШО → правееШАГ 5: коммит C352 ── проверяешь ──► ПЛОХО → левееШАГ 6: коммит C336 ── проверяешь ──► ХОРОШО → правееШАГ 7: коммит C344 ── проверяешь ──► ПЛОХО → левееШАГ 8: коммит C340 ── проверяешь ──► ХОРОШО → правееШАГ 9: коммит C342 ── ВИНОВНИК НАЙДЕН
git bisect reset # вернуться туда, где былПроверку можно автоматизировать скриптом — bisect сам прогоняет его на каждом шаге (exit code 0 = хорошо, 1–124 = плохо, 125 = пропустить):
git bisect start HEAD v1.8.0git bisect run npm test -- --grep "checkout flow"# ... через 9 прогонов:# <коммит-хэш> is the first bad commitЕсли виновник — чужой коммит, git bisect visualize показывает контекст, и ты смотришь конкретный диф, а не тысячу.
git rebase –onto: хирургия веток
Заголовок раздела «git rebase –onto: хирургия веток»Обычный rebase переносит коммиты на новое основание. --onto нужен, когда переносится не вся ветка, а кусок: «возьми коммиты, которые есть в feature-B, но нет в feature-A, и поставь их на main».
Классическая задача: сделал ветку от неправильного места (от другой фичи вместо main):
БЫЛО (feature-search ошибочно выросла из feature-auth):
main: C1─C2 \feature-auth: A1─A2 \feature-search: S1─S2 ← надо перенести на maingit rebase --onto main feature-auth feature-search# ▲ ▲ ▲# │ │ └── какую ветку двигать# │ └── от какой точки отсчитывать «что переносить»# └── куда ставитьСТАЛО:
main: C1─C2─────────● \ \feature-auth: A1─A2 \ \feature-search: S1'─S2' ← как будто всегда росла от mainДругой частый сценарий — «отрезать хвост»: коммиты попали не в ту ветку. Тогда --onto в обратную сторону + cherry-pick:
# три коммита случайно сделали в release-1.2 вместо maingit switch maingit cherry-pick release-1.2~3..release-1.2 # перенести их в maingit switch release-1.2git reset --hard HEAD~3 # отрезать лишнее (ветка ещё не запушена!)Submodules: репозиторий внутри репозитория
Заголовок раздела «Submodules: репозиторий внутри репозитория»Субмодуль — это фиксированная ссылка на конкретный коммит другого репозитория (подробно — Pro Git, раздел «Подмодули»):
my-app/├── .git/├── src/└── shared-lib/ ← НЕ файлы, а вложенный клон ├── .git → указатель на ../.git/modules/shared-lib └── (checkout коммита abc1234).gitmodules ← файл с URL и путями субмодулейgit submodule add git@github.com:octo/shared-lib.git libs/shared-libgit commit -m "chore: add shared-lib as submodule"
# при клонировании репозитория с субмодулямиgit clone --recurse-submodules git@github.com:you/my-app.git# или позже:git submodule update --init --recursiveКлючевое свойство: родитель хранит только хэш коммита субмодуля. Новые коммиты в субмодуле родитель не видит, пока ты явно не обновишь запись. Это и сила (пинимание версий), и боль (забыл обновить запись — CI собирает старое).
Git LFS: большие бинарники
Заголовок раздела «Git LFS: большие бинарники»Git плохо хранит бинарники: каждая версия 100-МБ ассета — это новый объект на 100 МБ, и клонируешь ты все версии сразу. LFS (Large File Storage) выносит содержимое больших файлов в отдельное хранилище, а в репозитории оставляет лёгкий указатель:
БЕЗ LFS: С LFS:
.git/objects/ .git/lfs/objects/├── 100MB (v1 ассета) ├── 100MB (v1)├── 100MB (v2 ассета) └── 100MB (v2)├── 100MB (v3 ассета) clone=300MB ветки видят только указатели; clone качает текущие версииgit lfs install # один раз на машинуgit lfs track "*.psd" "*.zip" "*.mp4" # правило → .gitattributesgit add .gitattributesgit commit -m "chore: track binaries with LFS"Правило: всё, что не диффится текстом (картинки, видео, архивы, модели ML) — в LFS или в артефакт-хранилище (S3/MinIO), но не в обычные объекты Git.
Хуки: автоматика на события
Заголовок раздела «Хуки: автоматика на события»В .git/hooks/ лежат исполняемые скрипты, которые Git запускает на событиях жизненного цикла (полный список хуков — githooks(5)). Самые полезные для разработчика:
| Хук | Когда | Типичное использование |
|---|---|---|
pre-commit |
перед созданием коммита | линтер, форматтер, проверка секретов |
commit-msg |
после ввода сообщения | проверка формата conventional commits |
pre-push |
перед push | прогон тестов, запрет push в main |
post-merge |
после merge | npm install, если поменялись зависимости |
Пример pre-commit без сторонних инструментов:
#!/bin/sh# .git/hooks/pre-commit (chmod +x обязателен!)
# не дать закоммитить отладочные маркерыif git diff --cached | grep -E '^\+.*(console\.log|debugger|TODO:HACK)'; then echo "❌ Найден console.log/debugger в staged-изменениях" >&2 exit 1 # ненулевой код = отмена коммитаfiРуками хуки не масштабируются (папка .git локальна и не коммитится). Стандарт де-факто — husky + lint-staged: конфиг хуков хранится в репозитории и ставится всем через npm install:
npm install --save-dev husky lint-stagednpx husky init{ "lint-staged": { "*.{js,ts}": ["eslint --fix", "prettier --write"] }}npx lint-stagedТеперь линтер гоняется только по staged-файлам за секунды. Но хуки — дверь «на совести»: их можно обойти (--no-verify), поэтому серверная защита (CI, branch protection) остаётся обязательной.
Подпись коммитов: GPG и SSH
Заголовок раздела «Подпись коммитов: GPG и SSH»По умолчанию коммит подписывается именем и email — которые поставить может кто угодно. Подпись (GPG или SSH-ключом) доказывает, что коммит сделал именно владелец ключа:
# GPGgit config --global user.signingkey AAAA1111BBBB2222git config --global commit.gpgsign true
# или SSH-подпись (проще: ключ уже есть для GitHub)git config --global gpg.format sshgit config --global user.signingkey ~/.ssh/id_ed25519.pubобычный коммит: подписанный коммит:
author: Alice <a@corp.io> author: Alice <a@corp.io> gpg: Signature made ... «кто угодно может написать хэш коммита зашифрован Alice в поле author» приватным ключом Alice; сервер/GitHub проверяет публичным → ✓ VerifiedGitHub показывает зелёный «Verified» и позволяет требовать только подписанные коммиты в main — cheap win для цепочки поставки (supply chain).
Секреты в Git: gitleaks и trufflehog
Заголовок раздела «Секреты в Git: gitleaks и trufflehog»Правило номер один: секрет никогда не попадает в Git. Даже если удалить коммит следующим — он остался в истории, в reflog, у всех, кто успел запулить, и у ботов, сканирующих публичные пуши в реальном времени (AWS-ключи воруют за минуты после пуша).
Профилактика на уровне хука:
# pre-commit: сканер секретов до того, как коммит созданgitleaks protect --staged --verbose# exit 1, если найден похожий на секрет паттерн (AKIA..., -----BEGIN PRIVATE KEY-----, ...)# раз в неделю — полный аудит историиgitleaks detect --source . --report-path gitleaks-report.json# trufflehog — альтернатива с проверкой секретов «живые ли» через APItrufflehog git file://. --only-verifiedgit add .env ──► pre-commit (gitleaks) ──► СЕКРЕТ? ──► ДА ──► коммит ОТМЕНЁН ════════════════════════ └──► НЕТ ──► коммит созданСлучайно запушил — порядок действий: отозвать секрет (перевыпустить ключ/пароль — это первым делом!), потом уже чистить историю (git filter-repo, force-push), потом проверить логи использования. Чистка истории не спасает от кражи — секрет уже могли взять.
git blame: история конкретной строки
Заголовок раздела «git blame: история конкретной строки»git blame показывает, в каком коммите и кем была изменена каждая строка файла:
git blame -L 40,60 src/payments.js# ^abc1234 (Влад Б. 2026-03-14 13:22:11 +0300 40) const commission = 0.02;# abc12345 (Аня К. 2026-01-02 09:10:45 +0300 41) ...файл сейчас: blame показывает:
строка 40: 0.02 ← коммит abc1234, Влад, 14 мартастрока 41: ... ← коммит abc12345, Аня, 2 январястрока 42: ... ← коммит def9999, Влад, 30 декабряПолезные флаги: -L 40,60 (диапазон строк), -w (игнорировать переносы строк), -C (ловить перемещения кода между файлами), --ignore-rev (не винить за переформатирование). В расследовании бага: нашёл строку-подозреваемую → git show <коммит> → видишь диф и контекст задачи.
Sparse checkout и монорепо кратко
Заголовок раздела «Sparse checkout и монорепо кратко»Монорепо — много проектов в одном репозитории. Проблема: git clone качает всё. Решения:
Sparse checkout (см. git-sparse-checkout(1)) — чекаутишь только нужные папки:
git clone --filter=blob:none --sparse git@github.com:corp/monorepo.gitcd monorepogit sparse-checkout set apps/web libs/ui # в рабочей папке только они# --cone (режим по умолчанию в новых Git): только папки верхнего уровня,# быстрее и предсказуемее, чем паттерны-гитигнорыС partial clone (--filter=blob:none) объекты качаются лениво по мере обращения: git log и git blame работают сразу, а содержимое файлов приезжает только для тех путей, что реально чекаутятся. Для монорепо на сотни гигабайт это разница между клоном за минуту и за час.
полный clone монорепо: sparse-checkout set apps/web:
monorepo/ monorepo/├── apps/ ├── apps/│ ├── web/ ◄ нужно │ └── web/│ ├── mobile/ ✗ не нужно ├── libs/ui/├── libs/ └── (остальное НЕ скачано/не развёрнуто)│ ├── ui/ ◄ нужно│ └── core/ ✗ не нужно└── 5 ГБ объектов └── объекты качаются лениво (blob:none)Submodules (разобраны выше) — альтернатива, когда проекты всё же живут в разных репозиториях, но нужна согласованная версия. Worktree — третий вариант: по папке на проект с общими объектами. Выбор зависит от культуры команды; технически Git поддерживает все три.
Гигиена: gc, prune, clean
Заголовок раздела «Гигиена: gc, prune, clean»Git сам подчищает за собой, но знать рычаги полезно:
git gc # сборка мусора: упаковка объектов, ускорениеgit gc --aggressive # тщательнее и дольше; почти никогда не нужноgit prune # удалить недостижимые объекты (делает gc и так)git reflog expire --expire=30.days --all # подрезать reflogЧТО ГДЕ КОПИТСЯ:
объекты: все версии файлов ──► git gc упаковывает в pack-файлыreflog: история движений веток (90 дней) ──► expire подрезает stash: стек припрятанного ──► drop/clearuntracked: неотслеживаемый мусор ──► clean (ОПАСНО!)И отдельно — git clean -fd, самая опасная команда из бытовых:
git clean -n # ПРОБНЫЙ ПРОГОН: покажет, ЧТО будет удалено (обязательно сначала!)git clean -fd # удалить untracked-файлы и папки — БЕЗ корзины, навсегдаgit clean -fdx # и то, что в .gitignore (node_modules, .env — тоже!)git status говорит "nothing to commit" + untracked файлы.git clean -fd удаляет untracked НАВСЕГДА:
до: после clean -fd:src/ ✓ src/ ✓notes.md ✗ ──► notes.md УДАЛЁНdraft/ ✗ draft/ УДАЛЁН.env ✓(игнор).env ✓ (без -x не тронет)Типичные ошибки и грабли
Заголовок раздела «Типичные ошибки и грабли»-
Stash как долговременное хранилище. Через неделю не вспомнишь, что там лежало, а
popпосле ребейза ветки может дать конфликт в полусделанной работе. Правки дольше пары часов — коммить, дажеWIP. -
git clean -fdбез-n. Одна опечатка — и черновики/генерированный код/локальные фикстуры удалены без корзины. Мышечная память: всегдаclean -nпередclean -fd. -
Забыть
--recurse-submodulesпри clone. Репозиторий выглядит целым, но папки субмодулей пустые, сборка падает с загадочными ошибками. Диагноз:git submodule statusпоказывает-(не инициализирован). -
Обновить субмодуль, но не зафиксировать указатель. В субмодуле сделал
git pull, всё работает локально, запушил только родителя — CI собирает старую версию, потому что запись в родителе не изменилась. После правок в субмодуле всегда: коммит в субмодуле + коммит указателя в родителе. -
Хуки как единственная защита.
--no-verifyобходит любой хук за секунду, а хуки не настроены у того, кто клонирует впервые. Хуки — для скорости фидбека, branch protection и CI — для гарантий. -
Bisect без автоматизации на проекте, где проверка занимает 10 минут. Ручной bisect с долгой проверкой — мучение. Сначала напиши скрипт/тест, который однозначно говорит «хорошо/плохо», и прогоняй
git bisect run.
Вопросы на собеседовании
Заголовок раздела «Вопросы на собеседовании»-
Что делает
git stashи чем pop отличается от apply? Прячет незакоммиченные изменения в стек, очищая рабочую директорию.popприменяет последний стэш и удаляет его из стека;applyприменяет, оставляя в стеке. -
Зачем нужен
git worktree? Несколько рабочих директорий с общим.git: параллельная работа над разными ветками без stash и без полного повторного клона. Ограничение: ветка не может быть открыта в двух worktree одновременно. -
Как найти коммит, сломавший фичу, если их тысяча?
git bisect: помечаю плохой (текущий) и хороший (тег/коммит, где работало), Git бинарным поиском переключает на средние коммиты; лучше —git bisect run <скрипт-тест>для автоматизации. ~log₂(N) шагов. -
Объясни
git rebase --onto main A B. Переносит коммиты ветки B, начиная с точки, где она разошлась с A, на новое основание main. Используется, когда ветка выросла не оттуда или нужно перенести кусок ветки. -
Чем субмодуль отличается от простого копирования кода? Субмодуль — фиксированная ссылка на коммит внешнего репозитория с собственной историей; обновления явные и пинятся. Копия — размножённый код, который расходится с оригиналом.
-
Запушил
.envс паролем. Твои действия? Сначала отозвать/перевыпустить секрет — историю чистить поздно, ключ уже могли украсть. Потом удалить из истории (git filter-repo), force-push, уведомить команду переклонироваться, проверить логи доступа. -
Зачем husky, если есть CI? Husky даёт фидбек за секунды до создания коммита (линтер/форматтер по staged-файлам), CI — гарантию на сервере, которую нельзя обойти. Это два разных уровня защиты, а не замена друг друга.
Практика
Заголовок раздела «Практика»- Начни правку в ветке, спрять её через
git stash -u, переключись на main, сделай хотфикс-коммит, вернись и примени стэш. Критерий:git stash listпуст, правки на месте, хотфикс — отдельный коммит в main. - Создай worktree на ветке
mainрядом с текущей папкой, убедись, что объекты общие (изменение из одной папки видно вgit logдругой), удали worktree. Критерий:git worktree listпоказывает только исходную папку. - В тестовом репозитории с 20+ коммитами сломай поведение в последнем, найди виновника через
git bisect runсо скриптом-проверкой. Критерий: bisect заканчивается строкой «first bad commit» с правильным хэшом. - Создай ветку
feature-Xот другой фича-ветки вместо main и перенеси её коммиты на main черезgit rebase --onto. Критерий:git log --graphпоказывает feature-X, растущую напрямую от main. - Подключи husky + lint-staged: pre-commit прогоняет форматтер только по staged-файлам, commit-msg проверяет формат сообщения (
feat:,fix:). Критерий: коммит с сообщением «wip» отклонён, с «feat: add x» проходит. - Настрой gitleaks как pre-commit хук и попробуй закоммитить файл с фейковым ключом
«redacted:AKIA…». Критерий: коммит отклонён, в выводе виден файл и тип секрета. - Прогони
git clean -nв своём реальном проекте и проанализируй список: что бы ты потерял приclean -fdx? Вынеси ценное из untracked-зоны.