---
tags: [tooling]
verified: 2026-08-31
---

# Git: модель данных, rebase против merge и спасательные инструменты

## Обзор

Git легко использовать по рецептам и трудно использовать осмысленно. Рецепты («если что-то сломалось — `git reset --hard`, если ветка отстала — `git pull`») работают ровно до первой нестандартной ситуации, а дальше начинается гадание. Причина в том, что рецепты выучиваются вместо модели, а модель у git на удивление маленькая: объектная база с неизменяемыми снимками, набор указателей на них и три области, между которыми файлы перемещаются. Почти любая команда — это перестановка указателей и перенос содержимого между этими областями.

Эта заметка идёт от модели к практике. Сначала — что такое коммит, ветка и три области; потом главный сюжет, `rebase` против `merge`: что именно происходит с графом в каждом случае, почему rebase удобен на личной ветке и разрушителен на общей. Дальше — инструменты, которые спасают, когда что-то уже пошло не так: `reflog`, `bisect`, `worktree`. В конце — работа с конфликтами и разборы ситуаций, в которые попадают все.

Текст опирается на документацию git 2.55 — это последний релиз на август 2026. Там, где поведение или флаг появились в конкретной версии, версия указана: в дистрибутивах живут разные git'ы, и часть приёмов ниже на git из стабильного Debian просто не соберётся.

## Модель данных

### Коммит — снимок, а не патч

Первое расхождение с интуицией: коммит не хранит изменения. Коммит хранит указатель на дерево (`tree`) — полный снимок состояния репозитория, — плюс автора, дату, сообщение и указатели на родительские коммиты. Диффы, которые показывает `git show` и `git log -p`, вычисляются на лету сравнением двух снимков; в базе их нет. Экономия достигается не хранением патчей, а дедупликацией: одинаковые файлы в тысяче коммитов — это один объект-блоб, на который тысяча ссылок.

Идентификатор коммита — хеш его содержимого, включая хеши родителей. Отсюда всё остальное:

- Коммит неизменяем. «Изменить коммит» физически невозможно; можно только создать новый объект и переставить на него указатель, а старый оставить сиротой.
- Хеш зависит от родителей. Меняется база — меняется хеш, даже если содержимое дерева бит в бит то же самое. Именно поэтому rebase создаёт **копии** коммитов, а не переносит оригиналы.
- История — направленный ацикличный граф, а не список. Линейность — частный случай, когда у каждого коммита ровно один родитель.

Посмотреть на объекты можно напрямую:

```bash
git cat-file -p HEAD          # сам коммит: tree, parent, author, committer
git cat-file -p HEAD^{tree}   # дерево верхнего уровня
git rev-parse HEAD            # полный хеш текущего коммита
```

### Ветка — это указатель

Ветка в git — файл с одной строкой в `.git/refs/heads/` (или запись в `.git/packed-refs`), содержащий хеш коммита. Всё. Создание ветки — запись сорока с небольшим байт; поэтому ветки в git дешёвые, а в системах, где ветка была копией дерева, — нет.

`HEAD` — символическая ссылка на текущую ветку (`ref: refs/heads/main`). Когда `HEAD` указывает не на ветку, а прямо на коммит, это detached HEAD: коммиты создаются, но их никто не удерживает, и после переключения они становятся недостижимыми. Не катастрофа — `reflog` их помнит, — но и не то состояние, в котором стоит работать долго.

Коммит в этой модели — просто перестановка: создать объект, чей родитель — текущий `HEAD`, и записать его хеш в файл ветки. Слияние, откат, переключение — вариации того же самого.

```text
до commit:                 после commit:

A ─── B ─── C              A ─── B ─── C ─── D
            ↑                              ↑
          main                           main
          HEAD                           HEAD
```

### Три области

Между «файлом на диске» и «файлом в истории» у git стоит промежуточная ступень, которой нет у большинства VCS, — индекс.

| Область | Где живёт | Что это |
|---------|-----------|---------|
| worktree | файлы в каталоге проекта | то, что видит редактор и компилятор |
| index (staging area) | `.git/index` | заготовка следующего коммита: список путей с хешами блобов |
| HEAD | `.git/refs/…` через `.git/HEAD` | последний коммит текущей ветки |

`git add` копирует состояние из worktree в индекс, `git commit` превращает индекс в объект-дерево и заворачивает его в коммит. Индекс существует не ради церемонии: он позволяет собрать коммит из части изменений (`git add -p`), а ещё именно в нём во время конфликтного слияния лежат три версии файла одновременно.

`git status` — это отчёт о двух диффах сразу: worktree против индекса («Changes not staged») и индекс против HEAD («Changes to be committed»). Явно их можно посмотреть так:

```bash
git diff              # worktree ↔ index
git diff --staged     # index ↔ HEAD
git diff HEAD         # worktree ↔ HEAD
```

### reset: soft, mixed, hard

`git reset` — команда, которая двигает указатель ветки и по дороге может задеть индекс и worktree. Три флага отличаются ровно тем, до какой области доходит действие:

| Флаг | HEAD | index | worktree | Когда нужен |
|------|------|-------|----------|-------------|
| `--soft` | двигает | не трогает | не трогает | пересобрать последние коммиты, сохранив всё в staged |
| `--mixed` (по умолчанию) | двигает | сбрасывает по новому HEAD | не трогает | «разкоммитить» и заново выбрать, что войдёт |
| `--hard` | двигает | сбрасывает | перезаписывает | выбросить всё до указанного состояния |

Так, схлопнуть пять последних коммитов в один — это ровно то, что показано в документации `git reset`:

```bash
git reset --soft HEAD~5 && git commit
```

`--hard` — единственный из трёх, который уничтожает несохранённую работу: он перезаписывает файлы версией из целевого коммита и удаляет отслеживаемые файлы, которых в нём нет. Незакоммиченные изменения после него не восстановить ничем — они никогда не попадали в объектную базу. А вот сами коммиты, мимо которых уехал указатель, восстанавливаются через `reflog` (см. ниже): `--hard` опасен для worktree, но не для истории.

Для точечных операций с 2.23 есть команды с честными именами, и в новом коде лучше писать их: `git restore <файл>` возвращает файл из индекса, `git restore --staged <файл>` убирает файл из индекса, `git switch <ветка>` переключает ветку. `git checkout` умеет всё это одновременно, и именно поэтому по нему так трудно понять, что произойдёт.

### Как адресовать коммиты

Три синтаксиса выглядят похоже и означают совершенно разное — это источник изрядной доли путаницы:

- `HEAD~2` — второй предок по первому родителю. Идёт вглубь истории.
- `HEAD^2` — второй родитель самого `HEAD`. Осмысленно только для merge-коммитов: `^1` — ветка, в которую вливали, `^2` — которую вливали.
- `HEAD@{2}` — не история вообще, а третья сверху запись в reflog: где `HEAD` был два перемещения назад. Ось времени здесь — не граф, а журнал локальных действий.

Есть и временна́я форма: `main@{yesterday}`, `HEAD@{2.hours.ago}`. `@{upstream}` (или `@{u}`) — отслеживаемая ветка на удалённом репозитории, `@{push}` — та, куда уйдёт push, если конфигурация их развела.

## merge и rebase

Это ядро статьи. Обе команды решают одну задачу — «в моей ветке должны оказаться чужие изменения», — но делают с графом принципиально разное.

Исходная ситуация всюду ниже одна: от `B` отошла ветка `feature` с коммитами `X` и `Y`, а `main` тем временем ушла вперёд на `C` и `D`.

```text
A ─── B ─── C ─── D          main
       \
        X ─── Y              feature
```

### merge

`git merge` создаёт новый коммит с двумя родителями. Содержимое считается трёхсторонним слиянием: общий предок (`B`), наша сторона (`Y`), их сторона (`D`).

```bash
git switch feature
git merge main
```

```text
A ─── B ─── C ─── D          main
       \           \
        X ─── Y ─── M        feature
```

Существующие коммиты не тронуты: `X` и `Y` сохранили свои хеши, а `M` документирует факт слияния. История стала честной ценой ветвления в графе.

Частный случай — **fast-forward**. Если текущий коммит является предком целевого (то есть в нашей ветке нет ничего своего), нового коммита не нужно: достаточно переставить указатель.

```text
до:  A ─── B ─── C ─── D        после:  A ─── B ─── C ─── D
           ↑           ↑                                  ↑
         main       origin/main                     main, origin/main
```

Это поведение по умолчанию, и именно из-за него обычный `git pull` иногда не создаёт merge-коммит, а иногда создаёт. Управляется явно:

```bash
git merge --ff-only main   # слиться только если это перемотка, иначе ошибка
git merge --no-ff feature  # всегда merge-коммит, даже если возможна перемотка
git config merge.ff false  # то же самое как политика репозитория
```

`--no-ff` — не украшательство: merge-коммит сохраняет факт, что несколько коммитов были одной задачей, и делает откат целой фичи одной командой (`git revert -m 1 <merge>`). Обратная сторона — граф, в котором каждая мелочь оставляет пузырь.

### squash

`git merge --squash feature` берёт итоговое содержимое ветки и кладёт его в индекс. Дальше вы делаете один обычный коммит — без второго родителя.

```text
A ─── B ─── C ─── D ─── S     main   (S содержит изменения X и Y)
       \
        X ─── Y               feature  (по-прежнему висит сбоку)
```

Результат: линейная история, один коммит на задачу, промежуточные шаги не попадают в `main`. Плата — git не знает, что ветка влита. `git branch --merged` её не покажет, а повторное слияние той же ветки попытается применить `X` и `Y` заново и, скорее всего, даст конфликт. Ветку после squash-слияния положено удалять и не возрождать.

### rebase

`git rebase main` находит коммиты, которые есть в `feature` и нет в `main`, и **применяет их заново** поверх `main`.

```bash
git switch feature
git rebase main
```

```text
A ─── B ─── C ─── D                  main
                   \
                    X' ─── Y'        feature
```

`X'` и `Y'` — новые объекты: то же содержимое изменений, другой родитель, другой хеш. Оригинальные `X` и `Y` никуда не делись из базы, но на них больше никто не ссылается — они станут недостижимыми и в конце концов будут собраны сборщиком мусора.

Отдельно стоит `--onto`, который умеет переносить не «всё, чего нет в upstream», а произвольный отрезок:

```bash
# перенести коммиты из feature, начиная после base, на main
git rebase --onto main base feature
```

Это рабочая команда для распутывания ветки, случайно отведённой не от того места.

### Что выбирать

Разница не в эстетике, а в наборе побочных эффектов.

**Rebase уместен**, когда ветка личная и ещё не опубликована: он даёт линейную историю, где каждый коммит стоит поверх свежей базы и, теоретически, компилируется. Такую историю приятно читать через `git log --oneline` и, что важнее, по ней хорошо работает `bisect`: меньше коммитов, у которых «сломано, потому что база старая». Перед отправкой на ревью привести ветку к текущему `main` через rebase — нормальная гигиена.

**Merge уместен**, когда ветка общая, когда важна фактическая история («эти изменения были влиты тогда-то, в таком состоянии») или когда ветка живёт долго. Ещё один практический аргумент: при слиянии конфликт разрешается один раз, при rebase — потенциально на каждом переносимом коммите. Ветку из двадцати коммитов, конфликтующую по одному и тому же файлу, rebase превращает в двадцать одинаковых разрешений (частично лечится `rerere`, см. ниже).

**Золотое правило: не переписывать историю, которую кто-то уже получил.** Механика проблемы прямо следует из модели. Коллега забрал `X` и `Y`; вы сделали rebase и запушили `X'` и `Y'`; его `git pull` видит расхождение и делает слияние — в результате в общей истории оказываются обе копии каждого коммита, а конфликты приходится разрешать дважды. Хуже: если он успел построить работу поверх `X`, его коммиты повисают на ветке, которой в upstream больше нет.

Граница «уже получил» проходит не по «запушено», а по «кто-то мог забрать». Ветка, запушенная на свой форк, где кроме вас никого, — всё ещё личная. Ветка в общем репозитории, на которую смотрит CI и коллеги, — общая, даже если ей два часа. Формулировка «не переписывай main» слишком узкая; формулировка «не переписывай ничего опубликованного» слишком широкая для практики pull-request'ов, где force-push в свою ветку — норма.

Есть и обратный случай — когда историю переписали не вы, а upstream. Документация `git rebase` описывает его отдельно: если после чужого rebase содержимое коммитов не изменилось, достаточно повторного `git rebase upstream` — уже применённые изменения он пропустит; если изменилось, придётся указать старую точку ветвления руками, например `git rebase --onto subsystem subsystem@{1}`, где `@{1}` берётся из reflog.

### Настройка pull

`git pull` — это `fetch` плюс интеграция, и по умолчанию интеграция — merge. Именно так в истории заводятся коммиты «Merge branch 'main' of github.com:… into main», которые не значат ничего. С git 2.27 при разошедшихся ветках без явной настройки печатается предупреждение и предлагается выбрать политику:

```bash
git config --global pull.rebase true      # тянуть с rebase
git config --global pull.ff only          # или вообще запретить неявные слияния
```

`pull.ff only` — самый предсказуемый вариант: `pull` работает, пока это перемотка, а в случае расхождения останавливается и оставляет решение за вами.

### Интерактивный rebase

`git rebase -i <база>` открывает в редакторе список коммитов (todo-лист), где каждая строка — команда. Изменение списка меняет историю: строки можно менять местами, удалять и заменять команду.

| Команда | Действие |
|---------|----------|
| `pick` | взять коммит как есть |
| `reword` | взять, но открыть редактор сообщения |
| `edit` | остановиться после применения — можно доправить файлы и `--amend` |
| `squash` | влить в предыдущий, объединив сообщения |
| `fixup` | влить в предыдущий, сообщение выбросить (`fixup -c` — наоборот, взять его) |
| `drop` | выбросить коммит |
| `exec` | выполнить shell-команду после этого коммита |
| `break` | остановиться в этом месте без применения коммита |
| `update-ref` | подвинуть ветку, указывающую на это место |

`exec` — недооценённая строчка: `git rebase -i --exec 'make test' main` прогонит тесты на каждом коммите ветки и остановится на первом сломанном. Это дешёвая проверка того, что линейная история не только красива, но и работоспособна.

Ручное редактирование todo-листа для мелких правок не нужно — есть автоматика. `git commit --fixup=<хеш>` создаёт коммит с сообщением `fixup! …`, а `git rebase -i --autosquash <база>` расставляет такие коммиты по местам и заранее помечает их как `fixup`:

```bash
git commit --fixup=a1b2c3d          # правка к конкретному коммиту
git rebase -i --autosquash main     # todo-лист уже собран правильно
git config --global rebase.autoSquash true   # чтобы не писать флаг каждый раз
```

Ещё два полезных флага: `--autostash` прячет незакоммиченные изменения на время rebase и возвращает их после (можно включить через `rebase.autoStash`), а `--update-refs` (2.38) при переносе цепочки веток, стоящих друг на друге, двигает и промежуточные ветки, а не только текущую. Без него после rebase нижние ветки стека остаются указывать на старые копии коммитов. Включается насовсем через `rebase.updateRefs`.

Проверить, что переписывание не изменило содержимого, помогает `git range-diff`: он сравнивает две последовательности коммитов попарно и показывает диффы диффов.

```bash
git range-diff main@{1} main HEAD   # что изменилось между старой и новой версией ветки
```

### Публикация переписанной ветки

После rebase обычный `git push` откажет: ветка не является потомком того, что лежит на сервере. `git push --force` сработает, но перезапишет всё, включая коммиты, которые кто-то положил туда пока вы работали.

Правильный инструмент — `--force-with-lease`: он требует, чтобы текущее значение ветки на сервере совпадало с вашей remote-tracking ссылкой, то есть чтобы вы видели актуальное состояние. У него есть известная дыра, и документация `git push` предупреждает о ней прямо: защита тривиально обходится, если что-то в фоне обновляет remote-tracking ссылки — cron с `git fetch`, IDE, файловый watcher. Тогда ваша ссылка уже знает про чужой коммит, проверка проходит, а чужая работа исчезает.

Закрывает дыру `--force-if-includes` (2.30): он проверяет, что верхушка remote-tracking ссылки действительно достижима из reflog вашей локальной ветки — то есть что чужие изменения не просто были загружены, а были вами интегрированы. Осмысленный вариант — использовать оба:

```bash
git push --force-with-lease --force-if-includes
```

## Спасательные инструменты

### reflog

Reflog — журнал перемещений ссылок: каждое изменение `HEAD` и каждой ветки записывается в `.git/logs/` вместе со старым и новым значением, автором и причиной («commit», «rebase (finish)», «reset: moving to…»). Это локальный журнал: он не передаётся при push, его нет в свежем клоне и он ничего не знает о том, что делали другие.

Пока коммит есть в reflog, он достижим и не будет удалён сборщиком мусора. Отсюда почти вся способность git к восстановлению.

```bash
git reflog                    # журнал HEAD
git reflog show feature       # журнал конкретной ветки
git reflog --date=iso         # с нормальными датами вместо HEAD@{5}
```

Три типовых спасения:

```bash
# 1. Промахнулись с reset --hard
git reset --hard HEAD@{1}     # вернуться туда, где HEAD был до последнего перемещения

# 2. Rebase «съел» коммиты
git reflog                    # найти запись rebase (start) и хеш до неё
git reset --hard <хеш>        # или, если rebase ещё идёт: git rebase --abort

# 3. Удалили ветку, а она была нужна
git reflog                    # найти последний коммит удалённой ветки
git branch feature <хеш>      # воссоздать указатель
```

Отдельно стоит помнить про `ORIG_HEAD`: перед потенциально разрушительными операциями (`merge`, `rebase`, `reset`) git сохраняет туда прежнюю позицию, так что `git reset --hard ORIG_HEAD` — короткий путь для отмены последнего слияния.

Reflog не вечен. По умолчанию `git gc` удаляет записи старше 90 дней (`gc.reflogExpire`) и записи, недостижимые из текущей верхушки, старше 30 дней (`gc.reflogExpireUnreachable`) — вторая цифра как раз про то, что остаётся от `commit --amend` и rebase. Для важного репозитория пороги можно поднять:

```bash
git config gc.reflogExpire "never"
git config gc.reflogExpireUnreachable "never"
```

### Когда reflog не помогает

Reflog фиксирует движение ссылок. Если объект никогда не был верхушкой ветки или `HEAD` — например, коммит из сброшенного stash или потерянный при обрыве операции, — искать надо в самой базе:

```bash
git fsck --unreachable --no-reflogs   # объекты, до которых не дотянуться
git fsck --lost-found                 # разложить их по .git/lost-found/
git show <хеш>                        # посмотреть, что это было
```

Работает, пока не прошёл `git gc`.

### bisect

`git bisect` ищет коммит, в котором сломалось, двоичным поиском по истории: за `log₂(N)` проверок вместо `N`. На тысяче коммитов это десять шагов.

```bash
git bisect start
git bisect bad                 # текущее состояние сломано
git bisect good v1.4.0         # на этом теге всё работало
# git выдаёт очередной коммит; проверяем и отвечаем:
git bisect good                # или git bisect bad
...
git bisect reset               # вернуться к исходному состоянию
```

Ручной цикл нужен редко — почти всегда есть команда, отличающая хорошее от плохого, и тогда весь поиск делает `git bisect run`. Правила кодов возврата документация формулирует так: команда должна возвращать 0, если код хорош, и значение от 1 до 127 включительно, кроме 125, если плох. 125 — «этот коммит не тестируется» (эквивалент `bisect skip`), 128 и выше прекращают поиск.

```bash
git bisect start HEAD v1.4.0   # сразу задать плохой и хороший концы
git bisect run ./check.sh
```

Скрипт обязан лежать **вне** рабочего дерева или быть устойчивым к тому, что дерево переключается: git на каждом шаге делает checkout, и файл из репозитория подменится или исчезнет вместе со своей версией. `/tmp/check.sh` надёжнее `./check.sh`. Внутри скрипт часто выглядит так:

```bash
#!/usr/bin/env bash
make -j"$(nproc)" >/dev/null 2>&1 || exit 125   # не собралось — коммит не тестируем
./run-regression-test || exit 1                 # тест упал — коммит плохой
exit 0
```

Полезные детали: `git bisect terms --old=works --new=broken` переименовывает good/bad, когда ищется не поломка, а любое изменение свойства; `git bisect skip v2.5..v2.6` исключает диапазон; `git bisect log` сохраняет ход поиска, а `git bisect replay` воспроизводит его после исправления ошибочного ответа; `--first-parent` (2.29) ограничивает поиск основной линией, что резко ускоряет дело в истории, набитой merge-коммитами.

### worktree

`git worktree` даёт несколько рабочих каталогов на один репозиторий. Каждый linked worktree — обычный каталог с файлом `.git` (не каталогом), внутри которого путь к `$GIT_DIR/worktrees/<id>`; объектная база и ссылки общие. Отсюда дешевизна: второй worktree не копирует историю.

```bash
git worktree add ../proj-hotfix hotfix        # существующая ветка в новый каталог
git worktree add -b review ../proj-review origin/feature   # новая ветка от origin/feature
git worktree add --detach ../proj-test HEAD   # без ветки, для разовой проверки
git worktree list
git worktree remove ../proj-hotfix
git worktree prune                            # почистить записи о вручную удалённых каталогах
```

Ограничение по конструкции: одну ветку нельзя держать в двух worktree сразу — `add` откажется, если ветка уже где-то выписана (обходится `--force`, но это почти всегда ошибка). С 2.42 есть `--orphan` для worktree на пустой несуществующей ветке.

Практический смысл — замена `git stash` в сценарии «надо срочно посмотреть другую ветку». Stash — стек анонимных коммитов, из которого легко достать не то и в котором легко похоронить работу на неделю; worktree оставляет текущее состояние нетронутым, включая незакоммиченные правки, открытые файлы в редакторе и запущенную сборку. Отдельный выигрыш — параллельная сборка: два worktree не делят каталог артефактов и не заставляют пересобирать всё при каждом переключении.

Чего ждать не стоит: неотслеживаемые файлы (`.env`, локальные конфиги, `node_modules`) в новый worktree не попадают — их надо создавать заново; хуки и конфигурация общие на весь репозиторий; каталог нельзя просто удалить `rm -rf`, точнее можно, но потом нужен `git worktree prune`.

## Конфликты

### Чтение маркеров

Конфликт возникает, когда обе стороны изменили пересекающиеся строки и трёхстороннее слияние не может выбрать. Git оставляет в файле обе версии:

```text
<<<<<<< HEAD
timeout = 30
=======
timeout = 60
>>>>>>> feature
```

Стиль по умолчанию (`merge`) показывает только два варианта — вашу сторону и чужую, — и по нему часто непонятно, кто из двоих что менял. Стиль `zdiff3` (2.35) добавляет секцию с исходным текстом из общего предка:

```bash
git config --global merge.conflictStyle zdiff3
```

С базой перед глазами вопрос «кто поменял 30 на 60, а кто добавил строку рядом» перестаёт быть гаданием. `zdiff3` отличается от более старого `diff3` тем, что вытаскивает совпадающие строки за пределы конфликтного блока, делая его короче.

**Ловушка rebase.** Во время слияния `ours` — это ваша ветка, `theirs` — вливаемая. Во время rebase всё наоборот: git по очереди применяет ваши коммиты поверх чужой базы, поэтому `ours` означает базу (upstream), а `theirs` — ваш переносимый коммит. Об это спотыкаются регулярно; когда неуверенны, лучше не пользоваться `--ours/--theirs`, а посмотреть все три версии явно:

```bash
git diff --diff-filter=U --name-only   # какие файлы в конфликте
git show :1:path/to/file               # общий предок
git show :2:path/to/file               # ours
git show :3:path/to/file               # theirs
git checkout --conflict=zdiff3 path    # вернуть маркеры, если файл уже испорчен
```

Числа `:1:`, `:2:`, `:3:` — те самые три стадии, которые индекс держит одновременно, пока конфликт не разрешён.

### rerere

`rerere` (reuse recorded resolution) запоминает, как вы разрешили конкретный конфликт, и применяет то же решение, когда встречает тот же конфликт снова. Записи лежат в `.git/rr-cache`; включается явно:

```bash
git config --global rerere.enabled true
git config --global rerere.autoUpdate true   # сразу класть разрешённое в индекс
```

Классический сценарий из документации — долгоживущая ветка, которую многократно сливают или переносят: конфликт каждый раз один и тот же, и разрешать его вручную по десять раз бессмысленно. Для интерактивного rebase выигрыш ещё нагляднее: конфликт, повторяющийся на каждом переносимом коммите, разрешается один раз.

Обратная сторона: если решение было ошибочным, rerere будет молча повторять ошибку. Лечится точечно:

```bash
git rerere status      # какие пути отслеживаются в текущем конфликте
git rerere diff        # что именно сейчас разрешено
git rerere forget <путь>   # забыть записанное решение для файла
```

Записи сами протухают при `git gc`: неразрешённые через 15 дней, разрешённые через 60 (`gc.rerereUnresolved`, `gc.rerereResolved`).

### Стратегии слияния

Стратегия по умолчанию для слияния двух веток — `ort` («Ostensibly Recursive's Twin»). Она заменила старый `recursive`, который был умолчанием с v0.99.9k по v2.33.0; в v2.50.0 имя `recursive` окончательно превратили в синоним `ort`. Практически это значит, что на любом современном git отдельно выбирать стратегию не нужно; `ort` умеет переименования, строит виртуального общего предка при нескольких кандидатах и по умолчанию использует `diff-algorithm=histogram`.

Опции задаются через `-X`:

```bash
git merge -X ours feature       # конфликтующие куски брать из нашей версии
git merge -X theirs feature     # …из их версии
git merge -X ignore-space-change feature   # игнорировать правки пробелов
git merge -X renormalize feature           # разные правила CRLF/clean-фильтров
```

Важно не спутать `-X ours` (опция стратегии: конфликтующие куски берём у себя, неконфликтующие чужие изменения принимаем) и `-s ours` (стратегия: результат равен нашему дереву, чужие изменения выброшены целиком, но факт слияния зафиксирован). Вторая нужна редко — например, чтобы объявить старую ветку влитой и больше к ней не возвращаться.

Прочие стратегии — нишевые: `octopus` (умолчание при слиянии более двух веток, отказывается от конфликтных случаев), `resolve` (простое трёхстороннее без обработки переименований), `subtree` (когда одно дерево — поддерево другого).

## Разборы частых ситуаций

**Закоммитил не в ту ветку.** Пока не запушено, это перестановка указателей:

```bash
git branch feature          # запомнить текущую позицию под новым именем
git reset --hard HEAD~2     # вернуть main туда, где она должна быть
git switch feature          # продолжить работу
```

Если коммитов много или они вперемешку — переносить точечно через `cherry-pick`.

**Нужно переписать последний коммит.** Забыли файл, опечатка в сообщении:

```bash
git add забытый-файл
git commit --amend --no-edit    # содержимое поправили, сообщение оставили
git commit --amend              # только сообщение
```

`--amend` создаёт новый коммит и переставляет ветку — старый уходит в reflog. Если коммит уже опубликован, действуют те же правила, что и для rebase: в личную ветку — `push --force-with-lease --force-if-includes`, в общую — лучше отдельный коммит-исправление.

**Перенести один коммит из другой ветки.**

```bash
git cherry-pick a1b2c3d          # один коммит
git cherry-pick a1b2c3d^..e4f5g6 # диапазон включительно с первого
git cherry-pick -x a1b2c3d       # добавить в сообщение строку «(cherry picked from …)»
git cherry-pick -n a1b2c3d       # применить в индекс, не коммитить
```

`-x` стоит взять в привычку при переносе исправлений в релизные ветки: без него потом невозможно понять, откуда взялся коммит-двойник. Помнить, что `A..B` не включает `A`, — отсюда `^` в примере.

**Случайно запушил лишнее.** Здесь развилка. Если ветка общая — единственный корректный путь `git revert`: он создаёт новый коммит, отменяющий изменения, и ничего не переписывает.

```bash
git revert a1b2c3d
git revert -m 1 <merge>    # откатить слияние, оставив первого родителя основной линией
```

Если ветка личная (свой форк, никто не забирал) — можно переписать и запушить с `--force-with-lease --force-if-includes`.

Отдельный случай — **утёкший секрет**. `revert` здесь не решение: объект с ключом остаётся в базе, доступен по хешу, уже разошёлся по клонам, форкам и кешам хостинга. Порядок действий: сначала отозвать и перевыпустить ключ (это обязательная часть, а не запасная), затем при необходимости вычистить историю через `git filter-repo` и договориться со всеми о переклонировании. Считать секрет скомпрометированным нужно с момента push, а не с момента обнаружения.

**Потерялся stash.** `git stash drop` не спрашивает подтверждения, а сброшенный stash не значится ни в одной ветке:

```bash
git fsck --unreachable | grep commit | cut -d' ' -f3 | xargs git log --merges --no-walk
git stash apply <хеш>
```

**Собрать историю задачи перед ревью.** Типовая последовательность: подтянуть свежую базу, схлопнуть черновые коммиты, прогнать тесты на каждом шаге.

```bash
git fetch origin
git rebase --autostash origin/main
git rebase -i --autosquash origin/main
git rebase -i --exec 'make test' origin/main
git push --force-with-lease --force-if-includes
```

## Что стоит настроить один раз

Значения по умолчанию у git местами исторические, и несколько строк в `~/.gitconfig` снимают целый класс неприятностей:

```ini
[pull]
    ff = only                 ; не создавать неявных merge-коммитов
[rebase]
    autoSquash = true         ; учитывать fixup!/squash! без флага
    autoStash = true          ; не требовать чистого дерева
    updateRefs = true         ; двигать промежуточные ветки стека
[merge]
    conflictStyle = zdiff3    ; показывать общего предка в конфликте
[rerere]
    enabled = true            ; запоминать разрешения конфликтов
[diff]
    algorithm = histogram     ; более осмысленные диффы, чем myers
    colorMoved = zebra        ; отличать перемещённый код от нового
[fetch]
    prune = true              ; убирать ссылки на удалённые ветки
```

Из перечисленного спорна только `pull.ff = only` — она превращает привычный `git pull` в команду, которая иногда отказывается работать. Это и есть цель: расхождение веток должно быть замечено, а не разрешено автоматически способом, который вы не выбирали.

## Источники

- [git-rebase(1)](https://git-scm.com/docs/git-rebase) — todo-команды, `--update-refs`, `--autosquash`, раздел RECOVERING FROM UPSTREAM REBASE
- [git-merge(1)](https://git-scm.com/docs/git-merge) и [merge-strategies(7)](https://git-scm.com/docs/merge-strategies) — стратегии, история умолчаний `recursive` → `ort`
- [git-reset(1)](https://git-scm.com/docs/git-reset) — точные формулировки для `--soft`, `--mixed`, `--hard`
- [git-reflog(1)](https://git-scm.com/docs/git-reflog) и [gc.reflogExpire](https://git-scm.com/docs/git-config#Documentation/git-config.txt-gcreflogExpire) — сроки хранения записей
- [git-bisect(1)](https://git-scm.com/docs/git-bisect) — коды возврата `bisect run`, `terms`, `log`/`replay`
- [git-worktree(1)](https://git-scm.com/docs/git-worktree) — устройство linked worktree, ограничения, `--orphan`
- [git-rerere(1)](https://git-scm.com/docs/git-rerere) — механика записи разрешений и сроки их жизни
- [git-push(1)](https://git-scm.com/docs/git-push) — `--force-with-lease`, его известная слабость и `--force-if-includes`
- [Pro Git, 2-е издание](https://git-scm.com/book/en/v2) — главы 3 (ветвление) и 7 (инструменты)
- [Release Notes git 2.30, 2.35, 2.38, 2.42, 2.50](https://github.com/git/git/tree/master/Documentation/RelNotes) — привязка флагов к версиям

---

## Связанные заметки

- [Методика изучения Go-библиотек](./go-library-study-guide.md) — разбор чужого кода; `bisect` и `log -S` — часть того же инструментария
- [Neovim: руководство по конфигурации](./nvim-guide.md) — редактор, в котором разрешаются конфликты и правятся сообщения коммитов
- [CLI-инструменты поиска: ripgrep, fd, fzf, jq](./cli-search-toolkit.md) — `fzf` поверх `git log` и `git branch` для интерактивного выбора коммитов и веток
