Git
Как Git хранит историю и синхронизирует её с сервером, чем merge отличается от rebase, какую модель ветвления выбрать команде и как спасать «потерянные» коммиты. Git спрашивают на любом собеседовании: по ответам видно, работал ли кандидат в команде с pull request или только нажимал кнопки в IDE.
Как устроен Git и какие команды нужны каждый день?
Заголовок раздела «Как устроен Git и какие команды нужны каждый день?»Что это
Коммит — снимок всех файлов с автором, сообщением и ссылкой на предыдущий коммит. Изменение проходит три области:
- Рабочая копия — файлы на диске.
- Индекс — то, что войдёт в следующий коммит.
- Репозиторий — папка
.gitс историей.
Как пользоваться
| Команда | Что делает |
|---|---|
git clone <url> |
копирует репозиторий |
git status |
что изменено и что в индексе |
git add <файл> |
кладёт правки в индекс |
git commit -m "..." |
создаёт коммит из индекса |
git switch -c <ветка> |
создаёт ветку и переходит в неё |
git fetch |
скачивает чужие коммиты |
git pull |
fetch и слияние в текущую ветку |
git push |
отправляет коммиты на сервер |
Частая ошибка
Считать, как в SVN, что коммит уже у коллег. До git push он только у вас.
Как устроено
| Признак | SVN | Git |
|---|---|---|
| История | на центральном сервере | полная копия в каждом клоне |
| Коммит | сразу на сервере | локально, на сервер — через push |
| Ветка | копия каталога | указатель на коммит |
| Без сети | почти ничего | всё, кроме обмена с сервером |
fetch против pull
git fetchобновляет только ветки удалённого отслеживанияorigin/*. Ваши ветки и файлы не меняются.git pullделаетfetch, а затем вливаетorigin/<ветка>в текущую ветку слиянием или rebase.
Ловушки
- Неожиданный merge-коммит. Если вы и коллега закоммитили в одну ветку,
git pullсо слиянием создаст коммит слияния. История обрастает «Merge branch ‘main’ of …». - Лишнее в коммите.
git add .добавит всё подряд, включаяbin/,obj/и локальные настройки. Их исключают в.gitignore. - Отказ pull. Без настройки стратегии Git останавливается с «Need to specify how to reconcile divergent branches». Установщик Git for Windows обычно прописывает
pull.rebase false, поэтому на Windows этой ошибки можно не встретить.
Правило
Сначала git fetch и git log main..origin/main, потом осознанно merge или rebase. Стратегию pull настройте явно.
Под капотом
git cloneкопирует все объекты и создаёт ветки удалённого отслеживанияorigin/*. Локальнаяmainполучает upstream — ветку на сервере, с которой сравнивается.git status -sbпоказывает расхождение с upstream:[ahead 2, behind 1]. Это сравнение с последнимfetch, а не с сервером прямо сейчас.- Удалённых репозиториев может быть несколько:
originдля своего форка иupstreamдля исходного проекта.
Цена
- Клон содержит всю историю. Для CI хватает
git clone --depth 1(shallow clone), для больших репозиториев —--filter=blob:none(partial clone): содержимое старых файлов скачивается по требованию. - Бинарные файлы не сливаются, а блокировок, как
svn lock, в самом Git нет: конфликт в них решают только выбором одной версии.
Компромиссы
pull.rebase trueдержит локальную историю линейной, но конфликты решаются покоммитно.pull.rebase falseпроще, но засоряет историю коммитами слияния.pull.ff onlyбезопаснее всего: при расхождении Git останавливается и ждёт решения человека.
Как проверить
git remote -v— куда смотрят удалённые репозитории.git log --oneline --graph --all— граф локальных и удалённых веток.git config --show-origin --get-regexp pull— откуда взялась стратегияpull.
Итог: в Git две истории — ваша и серверная, и между ними нет ничего автоматического, кроме того, что вы явно настроили.
git clone https://github.com/acme/shop.git # копия репозитория со всей историейcd shopgit switch -c feature/discount # новая ветка от текущего коммитаgit status # что изменено и что уже в индексеgit add src/Discount.cs # положить файл в индексgit commit -m "Add discount calculation" # локальный коммит: сервер о нём не знаетgit fetch origin # обновить origin/*, свои ветки не трогатьgit log --oneline main..origin/main # какие коммиты пришли на сервер в maingit push -u origin feature/discount # опубликовать ветку и запомнить upstream- Чем
git fetchотличается отgit pull? —fetchтолько скачивает и обновляетorigin/*,pullещё и вливает в текущую ветку. - Что такое индекс и зачем он нужен? — Промежуточная область: позволяет собрать в коммит только часть изменений, например через
git add -p. - Что значит
[ahead 1, behind 2]вgit status -sb? — У вас один коммит, которого нет на сервере, и два серверных, которых нет у вас. - Как отменить
git add? —git restore --staged <файл>: файл уходит из индекса, правки в рабочей копии остаются. - Чем
git switchотличается отgit checkout? —switchиrestoreразделили две роли перегруженногоcheckout: переключение веток и восстановление файлов.
Чем merge отличается от rebase и когда что использовать?
Заголовок раздела «Чем merge отличается от rebase и когда что использовать?»Что это
Два способа перенести изменения одной ветки в другую.
- Merge соединяет ветки. Если обе ушли вперёд, появляется коммит слияния с двумя родителями.
- Rebase применяет ваши коммиты заново поверх другой ветки. Получаются новые коммиты с новыми хешами и прямая история.
Как пользоваться
- Обновить ветку:
git fetch, затемgit rebase origin/mainилиgit merge origin/main. - При конфликте Git останавливается и помечает спорные места:
<<<<<<<,=======,>>>>>>>. - Исправьте файл,
git add <файл>, затемgit rebase --continue(после merge —git commit). - Отменить —
git rebase --abortилиgit merge --abort.
Частая ошибка
Rebase ветки, которую уже забрали коллеги, и git push --force: их история разойдётся с сервером.
Как устроено
- Fast-forward. Если в целевой ветке нет новых коммитов, merge просто сдвигает её указатель вперёд. Нового коммита нет.
- Трёхстороннее слияние. Иначе Git сравнивает общего предка и две вершины и создаёт коммит слияния.
--no-ffсоздаёт его даже там, где возможен fast-forward. - Rebase. Git находит общего предка, по одному применяет ваши коммиты поверх новой базы и переносит на результат указатель ветки.
Когда что выбирать
| Ситуация | Что использовать |
|---|---|
Обновить свою ветку от main |
rebase |
| Почистить свои коммиты перед PR | rebase -i |
| Ветка общая для нескольких людей | merge |
Влить PR в main |
метод, заданный в репозитории |
Ловушки
- Rebase опубликованного. Золотое правило из Pro Git: не переписывать коммиты, которые есть вне вашего репозитория и на которых другие могли основать работу.
- Force push.
--forceперетирает чужие коммиты.--force-with-leaseоткажет, если ветка на сервере изменилась с вашего последнегоfetch. Послеfetchбез интеграции защита пропадает — для этого есть--force-if-includes(Git 2.30+). - Squash merge. После него коммиты ветки не попадают в
main. Продолжать ту же ветку нельзя — её удаляют и начинают новую.
Правило
Свою ветку — rebase и --force-with-lease, общую — только merge.
Под капотом
- Rebase — серия cherry-pick: каждый коммит применяется заново как отдельный патч. Поэтому конфликт может всплыть на каждом коммите, а не один раз.
- Старые коммиты не удаляются сразу: на них указывают
ORIG_HEADи reflog, пока их не уберёт сборка мусора. - С Git 2.34 стратегия слияния по умолчанию —
ort, заметно более быстрая заменаrecursive.
Компромиссы
| Метод слияния PR | Плюс | Минус |
|---|---|---|
| Merge commit | полная история, слияние откатывается одним revert -m 1 |
граф с параллельными ветками |
| Squash | один коммит на PR, читаемый main |
теряются промежуточные коммиты и их авторы |
| Rebase merge | линейная история с отдельными коммитами | новые хеши, подписи коммитов теряются |
Чистка своих коммитов
git commit --fixup <hash>иgit rebase -i --autosquash mainвклеивают исправление в нужный коммит.git config rerere.enabled trueзапоминает разрешённые конфликты и применяет их повторно при следующем rebase.
Как проверить в проде
git log --first-parent mainпоказывает историюmainпо одному коммиту на PR, если сливали через merge commit.git range-diffсравнивает ветку до и после rebase и показывает, что изменилось при разрешении конфликтов.
Итог: merge хранит правду о том, как шла работа, rebase — удобный рассказ о ней; переписывают только то, что ещё никто не забрал.
git switch feature/discountgit fetch origingit rebase origin/main # переносим свои коммиты поверх свежего main# CONFLICT (content): Merge conflict in price.txtgit status --short # UU price.txt — файл с конфликтом# правим price.txt и убираем маркеры <<<<<<< ======= >>>>>>>git add price.txtgit rebase --continue # применяем оставшиеся коммитыgit push --force-with-lease # откажет, если ветку на сервере кто-то изменил# До: ветки разошлись$ git log --oneline --graph --all* d331889 Add tests* 19aa729 Apply discount| * 2767ee0 Update notes|/* 2ccb685 Add price
# Вариант 1: git merge feature/discount в main$ git log --oneline --graph* 6371798 Merge branch 'feature/discount'|\| * d331889 Add tests| * 19aa729 Apply discount* | 2767ee0 Update notes|/* 2ccb685 Add price
# Вариант 2: git rebase main в ветке, затем git merge feature/discount в main$ git log --oneline --graph* 37980f0 Add tests* e677de9 Apply discount* 2767ee0 Update notes* 2ccb685 Add priceПосле rebase у коммитов ветки новые хеши (19aa729 → e677de9), а слияние в main прошло fast-forward.
- Что такое fast-forward и как его запретить? — Сдвиг указателя без нового коммита;
git merge --no-ffвсегда создаёт коммит слияния. - Чем
--force-with-leaseлучше--force? — Перезаписывает ветку, только если на сервере она такая же, какой вы её видели при последнемfetch. - Как откатить влитый merge-коммит? —
git revert -m 1 <hash>:-m 1говорит, какой родитель считать основной линией. - Почему после squash merge ветку надо удалить? — Её коммиты не попали в
main, и следующий PR из неё снова покажет старые изменения или конфликты. - Как разделить один большой коммит на несколько? —
git rebase -i, пометить коммитedit, затемgit reset HEAD~1и закоммитить частями.
Какие бывают модели ветвления и как выбрать?
Заголовок раздела «Какие бывают модели ветвления и как выбрать?»Что это
Модель ветвления отвечает на вопросы: от какой ветки создавать свою, куда её вливать и что выкладывается в прод.
Как пользоваться: GitHub Flow
- Создайте ветку от свежего
main:git switch -c feature/discount origin/main. - Делайте небольшие коммиты и отправляйте ветку на сервер.
- Откройте pull request. CI проверяет сборку и тесты, коллеги делают ревью.
- После одобрения PR вливают в
main, а ветку удаляют.
Частая ошибка
Держать ветку неделями. PR вырастает до сотен файлов, ревью превращается в формальность, а при слиянии копятся конфликты.
Как устроено
| Модель | Ветки | Кому подходит |
|---|---|---|
| Git Flow | main, develop, feature/*, release/*, hotfix/* |
продукт с версиями, редкие релизы |
| GitHub Flow | main и короткие ветки задач |
веб-сервис с частыми выкладками |
| Trunk-based | trunk (main), ветки на день-два, релизные по необходимости |
частая интеграция и CI/CD |
Когда что выбирать
- Выкладываете несколько раз в неделю и поддерживаете одну версию — GitHub Flow или trunk-based.
- Поставляете коробочные версии и чините старые — релизные ветки: trunk-based с ними или Git Flow.
- Незаконченную функцию не держат в ветке: её вливают выключенной за feature flag.
Ловушки
- Hotfix в Git Flow. Исправление вливают и в
main, и вdevelop. Забыли второе — ошибка вернётся в следующем релизе. - Флаги навсегда. Каждый флаг — две ветви кода и два набора тестов. После включения для всех флаг удаляют.
- Защита без проверок. Правило «только через PR» без обязательных проверок и ревью ничего не защищает. Какие проверки делать обязательными — в теме CI/CD и качество кода.
Правило
Чем чаще вы выкладываете, тем короче должны жить ветки.
Цена долгих веток
- Конфликты растут вместе с расхождением веток, а интеграция откладывается до слияния. Ошибки интеграции находят в конце, когда их дороже всего чинить.
- Большой PR ревьюят поверхностно: внимание рецензента ограничено, а не количество строк.
Релизные ветки
- Ветку
release/2.4отрезают отmain, когда версия готова, и помечают тегом. - Новые функции в неё не попадают — только исправления.
- Исправление делают в
mainи переносят черезcherry-pick -x: в сообщении остаётся ссылка на исходный коммит.
Обратный порядок — сначала в релиз — рискует потерять исправление в main, и ошибка вернётся в следующей версии.
Компромиссы
- Feature flags отделяют выкладку от релиза, но добавляют ветвления в коде и комбинации для тестов.
- Крупный рефакторинг без долгой ветки делают через branch by abstraction: новая реализация растёт рядом со старой за общей абстракцией.
- Когда в
mainвливают десятки PR в день, проверки на устаревшей базе пропускают поломки. Merge queue в GitHub проверяет каждый PR поверх уже стоящих в очереди.
Как проверить в проде
Метрики модели: время жизни ветки, размер PR, время от коммита до прода (lead time for changes из DORA). Растут — модель не работает, как задумано.
Итог: модель ветвления — инструмент для ритма поставки; долгие ветки откладывают интеграцию, флаги и короткие PR приближают её.
# Trunk-based: короткая ветка от свежего main, в main — только через PRgit switch -c fix/rounding origin/maingit commit -am "Fix rounding"git push -u origin fix/rounding
# Релизная ветка отрезается от main и помечается тегомgit switch -c release/2.4 maingit tag -a v2.4.0 -m "Release 2.4.0"
# Исправление сначала попало в main (коммит f2096a1), переносим его в релизgit switch release/2.4git cherry-pick -x f2096a1 # -x добавит "(cherry picked from commit ...)"git tag -a v2.4.1 -m "Hotfix 2.4.1"git push origin release/2.4 v2.4.1$ git log --oneline --graph --all --decorate* f2096a1 (main) Fix rounding* b182e6f Feature B (behind flag)| * bf02e76 (HEAD -> release/2.4, tag: v2.4.1) Fix rounding|/* 7f43956 (tag: v2.4.0) Feature A- Почему автор Git Flow сам не советует его для веб-сервисов? — Модель создавалась для версионируемого ПО; при continuous delivery он рекомендует более простой GitHub Flow.
- Как выпустить незаконченную функцию в trunk-based? — Влить её выключенной за feature flag и включить, когда готова.
- Куда сначала вносить исправление при наличии релизной ветки? — В
main, затем cherry-pick в релиз; иначе исправление легко потерять. - Что обычно включают в защиту
main? — PR с одобрением, обязательные проверки CI, запрет force push и удаления ветки. - Зачем нужен merge queue? — Проверяет PR поверх уже принятых, чтобы два по отдельности зелёных PR вместе не сломали
main.
Как Git хранит данные и как спасать ситуации?
Заголовок раздела «Как Git хранит данные и как спасать ситуации?»Что это
Коммит не исчезает, когда вы откатили ветку или удалили её. Git помнит каждое перемещение HEAD и веток в reflog.
Как пользоваться
| Ситуация | Команда |
|---|---|
| Отменить уже отправленный коммит | git revert <hash> |
| Убрать последний локальный коммит, сохранив правки | git reset --soft HEAD~1 |
Вернуть коммиты после reset --hard |
git reflog, затем git reset --hard HEAD@{1} |
| Отложить незаконченную работу | git stash, потом git stash pop |
| Перенести один коммит в другую ветку | git cherry-pick <hash> |
Частая ошибка
git reset --hard при незакоммиченных правках. Reflog хранит только коммиты, поэтому несохранённые изменения пропадут навсегда.
Как устроено
- Ветка — файл в
.git/refs/heads/с хешем коммита. HEAD обычно ссылается на ветку:ref: refs/heads/main. - Если HEAD указывает прямо на коммит, это detached HEAD. Новые коммиты в этом состоянии потеряются, если не создать ветку.
reset против revert
| Команда | Что меняет |
|---|---|
reset --soft <коммит> |
только ветку; изменения остаются в индексе |
reset --mixed <коммит> |
ветку и индекс; правки остаются в файлах |
reset --hard <коммит> |
ветку, индекс и рабочую копию |
revert <коммит> |
ничего не переписывает, добавляет обратный коммит |
Другие инструменты
- Случайно удалённая ветка.
git branch -Dпечатает хеш:(was f45de9d). Его же видно вgit reflog. Восстановление:git branch <имя> <hash>. - Stash.
git stash -uоткладывает и неотслеживаемые файлы. - Bisect.
git bisect start <плохой> <хороший>, затемgoodилиbadна каждом шаге;git bisect run <команда>проверяет автоматически.
Ловушки
revertmerge-коммита требует-m 1. Если ту же ветку потом влить снова, откатанные изменения не вернутся — сначала откатывают сам revert.- Сброшенный stash в reflog не попадает. Его ищут через
git fsck --unreachable.
Правило
Общая история — revert, своя локальная — reset.
Под капотом
- Объект адресуется хешем своего содержимого (SHA-1; поддерживается и SHA-256). Изменить коммит нельзя — только создать новый с другим хешем.
- Новые объекты лежат отдельными сжатыми файлами,
git gcупаковывает их в pack-файлы с дельта-сжатием. - Reflog локален и истекает: по умолчанию записи живут 90 дней, недостижимые — 30. После этого
git gcудаляет коммиты без ссылок.
Секрет попал в историю
- Отозвать и сменить секрет. Это главный шаг: копии истории уже есть у всех, кто сделал
fetch. - Переписать историю:
git filter-repo --sensitive-data-removalс--invert-paths --path <файл>или--replace-text. - Отправить результат:
git push --force --mirror origin. - На GitHub ссылки PR так не перезаписать — их и кеши удаляет поддержка.
- Коллеги переклонируют репозиторий. Повтор предотвращают
.gitignore, хранилище секретов и push protection.
Большие файлы
Git LFS хранит в репозитории текстовый указатель, а содержимое — на отдельном сервере. git lfs track пишет правило в .gitattributes, git lfs migrate import переносит уже закоммиченные файлы, переписывая историю. GitHub блокирует файлы больше 100 MiB.
Компромиссы
Переписывание истории меняет все хеши после первого изменённого коммита: открытые PR и чужие клоны ломаются, поэтому его делают один раз и по договорённости.
Итог: коммиты неизменяемы и долго восстановимы локально, а утёкший секрет спасает не чистка истории, а его смена.
git reset --hard HEAD~3 # «потеряли» три коммитаgit reflog -2# 7bf4219 HEAD@{0}: reset: moving to HEAD~3# e87e8fa HEAD@{1}: commit: Step 8git reset --hard HEAD@{1} # ветка снова на e87e8fa
git branch -D feature/x # Deleted branch feature/x (was f45de9d).git branch feature/x f45de9d # ветка восстановлена
# Ищем коммит, который сломал тест: v2.3.0 — хорошая версия, HEAD — плохаяgit bisect start HEAD v2.3.0git bisect run dotnet test --filter "FullyQualifiedName~DiscountTests"# f45de9dde16e396ecacc40236ccfa2d41ffdb7e6 is the first bad commitgit bisect reset # вернуться туда, где были до поиска# 0. Секрет уже отозван и заменён — без этого чистка бесполезнаgit clone --mirror https://github.com/acme/shop.gitcd shop.git# В passwords.txt строки вида: P@ssw0rd==>***REMOVED***git filter-repo --sensitive-data-removal --replace-text ../passwords.txtgit log -p --all | grep -c "P@ssw0rd" # 0 — в истории секрета больше нетgit push --force --mirror origin- Чем отличаются
reset --soft,--mixedи--hard? — Что сбрасывается вслед за веткой: ничего, индекс или индекс с рабочей копией. - Как вернуть ветку, удалённую через
git branch -D? — Найти хеш в выводе команды или вgit reflogи выполнитьgit branch <имя> <hash>. - Сколько живут записи reflog? — По умолчанию 90 дней, для недостижимых коммитов — 30; reflog есть только в локальном репозитории.
- Достаточно ли удалить файл с паролем новым коммитом? — Нет, он остаётся в истории; секрет меняют, историю чистят
git filter-repo. - Что делает
git bisect run, если команда вернула 125? — Пропускает коммит: его нельзя проверить, например он не собирается.