3.3Rebase
Проследишь, как rebase повторно записывает коммиты поверх нового основания, меняя их hash. Сравнишь merge, rebase и squash, а также применишь abort и --onto для переноса части ветки.
- Что
git rebase mainделает с коммитами ветки и почему патч прежний, а хеш новый. - Где rebase останавливается и как вернуть ветку через
--abortиORIG_HEAD. - Как пересадить часть ветки через
--ontoи перенести стопку веток одной командой. - Как склеить, переименовать и выбросить коммиты, вклеить правку через
--fixup. - Что станет с историей,
bisectиrevertпосле merge, rebase и squash-merge.
Rebase записывает коммиты заново
У этой главы свой демо-репозиторий, Go-сервис shop. Аня ответвила retry от
коммита «Добавить хранилище заказов» и сделала в ней три коммита. Боря тем временем добавил в
main два своих, и первый из них поменял сигнатуру Store.Find:
$ git switch retry
Switched to branch 'retry'
$ git log --oneline --graph main retry
* 1ee0b4e (main) Описать запуск в README
* f178e64 Передавать контекст в хранилище
| * c357ae4 (HEAD -> retry) Искать заказ с повтором
| * 5058e4c Не ждать после последней попытки
| * e0377f6 Повторять запрос при ошибке
|/
* 3b31e8b Добавить хранилище заказов
* a6f253d Создать модуль shop
git rebase main переносит коммиты текущей ветки на вершину main:
$ git rebase main
Successfully rebased and updated refs/heads/retry.
$ git log --oneline --graph main retry
* 48b7729 (HEAD -> retry) Искать заказ с повтором
* cb6bb38 Не ждать после последней попытки
* 67bce9b Повторять запрос при ошибке
* 1ee0b4e (main) Описать запуск в README
* f178e64 Передавать контекст в хранилище
* 3b31e8b Добавить хранилище заказов
* a6f253d Создать модуль shop
Внутри четыре шага. Git составляет список коммитов из main..retry, то есть после merge-base
3b31e8b (как её ищут, сказано в главе 3.1). Отсоединяет HEAD на 1ee0b4e. По
очереди применяет изменения каждого коммита: вычисляет их из пары «коммит и его родитель» и сливает с
текущим снимком, как cherry-pick. В конце ставит retry на последний новый коммит
и возвращает на неё HEAD. Старое с новым сравнивает git range-diff, где =
значит «патч не изменился»:
$ git range-diff main ORIG_HEAD retry
1: e0377f6 = 1: 67bce9b Повторять запрос при ошибке
2: 5058e4c = 2: cb6bb38 Не ждать после последней попытки
3: c357ae4 = 3: 48b7729 Искать заказ с повтором
Патч тот же, а хеш другой, потому что у нового коммита другой родитель и другое дерево: в снимок
67bce9b уже входят коммиты Бори. Автора и дату автора rebase переносит из старого коммита,
дату коммитера ставит текущую:
$ git log --format='%h %ad %cd %s' --date=short main..retry
48b7729 2026-09-02 2026-09-04 Искать заказ с повтором
cb6bb38 2026-09-02 2026-09-04 Не ждать после последней попытки
67bce9b 2026-09-02 2026-09-04 Повторять запрос при ошибке
$ git rev-parse --short ORIG_HEAD
c357ae4
Старые коммиты целы. retry на c357ae4 больше не указывает, хеш прежней вершины
записан в ORIG_HEAD, а сами коммиты пока держит вторая ветка Ани, retry-log. Когда
сборка мусора удаляет недостижимое, сказано в главе 1.4.
1ee0b4e и получил коммиты с другими хешами. Прежние остались в репозитории, retry
на них больше не указывает, а хеш последнего хранит ORIG_HEAD.
Новые коммиты rebase создаёт не всегда. Если ветка уже стоит на вершине main, переносить
нечего:
$ git rebase main
Current branch retry is up to date.
Интерактивный rebase тоже оставляет коммит как есть, пока тот не меняется и родитель у него прежний:
хеш совпадает до знака, даже дата коммитера не обновляется. Все коммиты заново создаёт
--force-rebase. Merge-коммиты внутри ветки rebase по умолчанию выбрасывает и выпрямляет
историю, а сохраняет их --rebase-merges.
Отмена: ORIG_HEAD и --abort
Rebase прошёл без единого конфликта, а собрать ветку уже нельзя:
$ go build ./...
# shop
./orders.go:7:19: not enough arguments in call to s.Find
have (int)
want (context.Context, int)
Боря поменял сигнатуру Find и поправил все вызовы, какие были в main. Вызов в
orders.go Аня написала в своей ветке, Боря его не видел, а текстового конфликта не
случилось: правки лежат в разных файлах. Так появился коммит 48b7729, который никто ни разу не
собирал. Вернём ветку туда, где она стояла до rebase (про режимы reset есть глава 5.1):
$ git reset --hard ORIG_HEAD
HEAD is now at c357ae4 Искать заказ с повтором
$ go build ./...
$ git rev-parse --short ORIG_HEAD
48b7729
ORIG_HEAD хранит одно значение, и его перезаписывает следующий reset,
merge, rebase и даже git stash, а commit и
switch не трогают. Сам reset записал туда вершину, с которой ушёл, и повторный
вызов вернул бы ветку на 48b7729. Затёртое значение найдёт reflog (глава 5.2).
Такой коммит ловят прямо во время переноса флагом -x: после каждого перенесённого коммита
rebase запускает команду и останавливается, если она упала:
$ git rebase -x 'go build ./...' main
Executing: go build ./...
Executing: go build ./...
Executing: go build ./...
# shop
./orders.go:7:19: not enough arguments in call to s.Find
have (int)
want (context.Context, int)
warning: execution failed: go build ./...
You can fix the problem, and then run
git rebase --continue
$ git branch -v
* (no branch, rebasing retry) a33e906 Искать заказ с повтором
main 1ee0b4e Описать запуск в README
retry c357ae4 Искать заказ с повтором
retry-log e078aaa Логировать неудачные попытки
Третий коммит не собрался (хеш у него не 48b7729: перенос шёл позже, и дата коммитера
другая). git status пишет interactive rebase in progress: под капотом у
-x интерактивный rebase, хотя редактор не открывался. HEAD отсоединён и
стоит на новом коммите, а retry всё ещё на c357ae4: ветку rebase переставляет
только в конце. Выхода два. git rebase --abort возвращает ветку, индекс и рабочую копию как
было:
$ git rebase --abort
$ git log --oneline -1
c357ae4 (HEAD -> retry) Искать заказ с повтором
Или починить коммит на месте. Аня снова запускает тот же git rebase -x, на остановке
передаёт контекст в вызов в orders.go, дописывает правку в коммит и продолжает:
$ go build ./...
$ git commit -a --amend --no-edit
[detached HEAD 0bb7055] Искать заказ с повтором
Date: Wed Sep 2 12:00:00 2026 +0300
1 file changed, 13 insertions(+)
create mode 100644 orders.go
$ git rebase --continue
Successfully rebased and updated refs/heads/retry.
$ git log --oneline main..retry
0bb7055 (HEAD -> retry) Искать заказ с повтором
ce27cb2 Не ждать после последней попытки
dbe542f Повторять запрос при ошибке
На конфликте rebase останавливается так же, и к двум выходам добавляется --skip: выбросить
текущий коммит. Как разрешать конфликт и почему ours и theirs при rebase меняются местами, объясняет
глава 3.2.
--onto: пересадить только свои коммиты
Ветку retry-log Аня начала поверх старой retry. Rebase двигал
только retry, и retry-log осталась на коммитах, которых в retry
больше нет. Обычный git rebase retry возьмёт всё, что есть в ветке и чего нет в
retry, то есть четыре коммита вместе со старыми копиями:
$ git switch retry-log
Switched to branch 'retry-log'
$ git log --oneline retry..
e078aaa (HEAD -> retry-log) Логировать неудачные попытки
c357ae4 Искать заказ с повтором
5058e4c Не ждать после последней попытки
e0377f6 Повторять запрос при ошибке
$ git rebase retry # строки hint: убраны
warning: skipped previously applied commit e0377f6
warning: skipped previously applied commit 5058e4c
Auto-merging orders.go
CONFLICT (add/add): Merge conflict in orders.go
error: could not apply c357ae4... Искать заказ с повтором
Could not apply c357ae4... # Искать заказ с повтором
Первые два коммита git пропустил сам. Перед переносом он сравнивает patch-id коммитов ветки с коммитами, которые есть в retry, но не в ветке: совпал
патч, и git выбрасывает коммит. Третий коммит Аня чинила, его
патч изменился, и git попытался применить старую версию поверх новой. Отменяем и говорим прямо, что
переносить:
$ git rebase --abort
$ git rebase --onto retry c357ae4
Successfully rebased and updated refs/heads/retry-log.
$ git log --oneline main..
7a20d88 (HEAD -> retry-log) Логировать неудачные попытки
0bb7055 (retry) Искать заказ с повтором
ce27cb2 Не ждать после последней попытки
dbe542f Повторять запрос при ошибке
Полная форма: git rebase --onto <новая база> <старая база> [<ветка>]. Команда
берёт коммиты из <старая база>..<ветка> и записывает их поверх новой, а без
--onto базы совпадают. Старой базой стала прежняя вершина retry, и переехал
один коммит. Так же пересаживают ветку, начатую по ошибке не от main.
В копии репозитория Боря по ошибке начал ветку readme-env от retry, а Аня
забрала его коммит к себе через cherry-pick. Боря пересаживает ветку на main:
$ git rebase --onto main retry readme-env
warning: skipped previously applied commit 8086a14
hint: use --reapply-cherry-picks to include skipped commits
hint: Disable this message with "git config set advice.skippedCherryPicks false"
Successfully rebased and updated refs/heads/readme-env.
$ git log --oneline -1
1ee0b4e (HEAD -> readme-env, main) Описать запуск в README
Патчи ветки git сверил с коммитами retry, которых в ветке нет, нашёл там копию и выбросил
коммит, хотя в main этой правки нет. Ветка стала равна main. Видишь
skipped previously applied commit при --onto, проверь результат: откат через
ORIG_HEAD и повтор с --reapply-cherry-picks вернут коммит.
Интерактивный rebase: план в редакторе
Боря собрал в ветке metrics пять коммитов, и в таком виде показывать их на ревью не стоит:
$ git log --oneline main..metrics
63862be (HEAD -> metrics) опечатка в имени метрики
ef79a30 отладка
768088a Отдавать метрики по HTTP
96a8725 wip
f58c0cf счётчик
git rebase -i main сначала открывает редактор с планом: в строке команда и коммит, сверху
самый старый, заголовок после # (так с git 2.50) служит комментарием. Ниже идёт справка
по командам, её пересказывает таблица:
pick f58c0cf # счётчик
pick 96a8725 # wip
pick 768088a # Отдавать метрики по HTTP
pick ef79a30 # отладка
pick 63862be # опечатка в имени метрики
# Rebase 1ee0b4e..63862be onto 1ee0b4e (5 commands)
Строки выполняются сверху вниз, их можно переставлять и менять в них команду. Боря пишет так:
reword f58c0cf # счётчик
fixup 63862be # опечатка в имени метрики
pick 96a8725 # wip
squash 768088a # Отдавать метрики по HTTP
drop ef79a30 # отладка
| Команда | Что делает |
|---|---|
pick | берёт коммит как есть |
reword | берёт коммит, открывает редактор с сообщением |
edit | берёт коммит и останавливается для commit --amend или разбиения |
squash | вклеивает в предыдущий коммит, открывает редактор с обоими сообщениями |
fixup | вклеивает в предыдущий коммит, сообщение остаётся от предыдущего; -C берёт сообщение этого |
drop | выбрасывает коммит |
exec | запускает команду оболочки, -x ставит её после каждого коммита |
break | останавливает rebase здесь |
update-ref | в конце ставит сюда ветку (стопка веток ниже) |
Первым идёт reword: git открывает редактор с сообщением счётчик, и Боря пишет
вместо него «Считать запросы к заказам». fixup молча вклеивает исправление опечатки в тот же
коммит. На squash в редакторе оба сообщения, начало файла такое:
# This is a combination of 2 commits.
# This is the 1st commit message:
wip
# This is the commit message #2:
Отдавать метрики по HTTP
Строки с # git выбросит сам, Боря стирает wip. После редакторов в терминале
остаётся вот что:
$ git rebase -i main
[detached HEAD c6bea21] Считать запросы к заказам
Date: Sat Sep 5 10:00:00 2026 +0300
1 file changed, 7 insertions(+)
create mode 100644 metrics.go
[detached HEAD 04006db] Отдавать метрики по HTTP
Date: Sat Sep 5 11:00:00 2026 +0300
1 file changed, 15 insertions(+)
create mode 100644 metrics_http.go
Successfully rebased and updated refs/heads/metrics.
$ git log --oneline main..metrics
04006db (HEAD -> metrics) Отдавать метрики по HTTP
f17a06f Считать запросы к заказам
Из пяти коммитов осталось два. c6bea21 промежуточный: его создал reword, а
fixup заменил на f17a06f. Вместо drop строку можно было стереть,
git молча выбросит и такой коммит.
--fixup и --autosquash: правка старого коммита
На ревью Боре предложили считать ещё и ошибки. Новый счётчик orderErrors относится к
первому коммиту ветки, а --amend дотягивается только до последнего (глава 2.1). Коммит, в
заголовке которого записано, куда его вклеить, создаёт --fixup:
$ git commit -a --fixup=f17a06f
[metrics efc8c94] fixup! Считать запросы к заказам
1 file changed, 2 insertions(+)
$ git log --oneline main..metrics
efc8c94 (HEAD -> metrics) fixup! Считать запросы к заказам
04006db Отдавать метрики по HTTP
f17a06f Считать запросы к заказам
git rebase -i --autosquash main находит целевой коммит по заголовку после
fixup! и сам ставит исправление в план следующей строкой. Боря закрывает редактор без правок:
pick f17a06f # Считать запросы к заказам
fixup efc8c94 # fixup! Считать запросы к заказам
pick 04006db # Отдавать метрики по HTTP
$ git rebase -i --autosquash main
Successfully rebased and updated refs/heads/metrics.
$ git log --oneline main..metrics
f2eae8f (HEAD -> metrics) Отдавать метрики по HTTP
b4e4e49 Считать запросы к заказам
С git 2.44 --autosquash работает и без -i, а rebase.autoSquash=true
включает его по умолчанию только для интерактивного rebase. Сообщение правят через
--fixup=reword:<коммит>, сообщение и содержимое через --fixup=amend:<коммит>
(оба с git 2.32): они открывают редактор и создают коммит amend! …. Готовую ветку
Боря влил в main перемоткой, git merge --ff-only metrics (глава 3.1).
Стопка веток: --update-refs
retry-log стоит на retry: это стопка, вторая ветка поверх первой, у каждой свой
merge request. main опять ушёл вперёд. Обычный git rebase main из
retry-log перенёс бы все четыре коммита, но retry осталась бы на старых, и снова
понадобился бы --onto. Флаг --update-refs (git 2.38) двигает и ветки, указывающие
на переносимые коммиты. Аня переключается на retry-log и запускает
git rebase -i --update-refs main, в плане такая ветка видна строкой update-ref:
pick dbe542f # Повторять запрос при ошибке
pick ce27cb2 # Не ждать после последней попытки
pick 0bb7055 # Искать заказ с повтором
update-ref refs/heads/retry
pick 7a20d88 # Логировать неудачные попытки
$ git rebase -i --update-refs main
Successfully rebased and updated refs/heads/retry-log.
Updated the following refs with --update-refs:
refs/heads/retry
$ git log --oneline -5
adf9d5b (HEAD -> retry-log) Логировать неудачные попытки
a1de955 (retry) Искать заказ с повтором
a9378c9 Не ждать после последней попытки
f30b966 Повторять запрос при ошибке
f2eae8f (metrics, main) Отдавать метрики по HTTP
Обе ветки переехали. Флаг работает и без -i, а насовсем его включает
git config --global rebase.updateRefs true.
cherry-pick: копия одного коммита
git cherry-pick делает с одним коммитом то же, что rebase с каждым: вычисляет его патч и
записывает новый коммит поверх HEAD. Боря исправил в main печать копеек, а в ветке
release/0.1, начатой от старого коммита, та же ошибка:
$ git log --oneline -1
81a63fb (HEAD -> main) Печатать копейки двумя знаками
$ git switch -c release/0.1 3b31e8b
Switched to a new branch 'release/0.1'
$ go run .
заказ 1: 500.5 ₽ <nil>
$ git cherry-pick -x main
[release/0.1 95849ba] Печатать копейки двумя знаками
Date: Tue Sep 8 10:00:00 2026 +0300
1 file changed, 1 insertion(+), 1 deletion(-)
$ go run .
заказ 1: 500.05 ₽ <nil>
$ git log -1 --format=%B
Печатать копейки двумя знаками
(cherry picked from commit 81a63fb7cb0b0198b51a08e525169e1c22e8f60f)
-x дописал в сообщение полный хеш исходного коммита, и по нему потом выясняют, откуда пришло
исправление и во все ли релизные ветки оно попало. Документация советует -x для переноса
между опубликованными ветками: хеш из локальной ветки никому ничего не скажет.
git cherry-pick A..B переносит несколько коммитов по порядку, без самого A.
У копии тот же patch-id, поэтому rebase на ветку с копией пропустит оригинал, как пропустил два коммита
в разделе про --onto.
Опубликованную историю не переписывают
Всё выше меняло только локальный репозиторий. Когда старые коммиты уже на сервере, на них опираются чужие клоны. Обычный push переписанной ветки git отклонит как non-fast-forward, а после принудительного у коллег окажутся две истории с одинаковыми патчами. Слияние склеит обе, и каждое изменение попадёт в историю дважды (как с amend в главе 2.1). Кто строил на твоей ветке, будет пересаживать свои коммиты.
Неотправленные коммиты переписывай как угодно. Свою ветку merge request, на которой никто не строит,
тоже можно: её отправляют через git push --force-with-lease, который откажет, если на
сервере появилось чужое (глава 4.1). main, релизные и другие
общие ветки не переписывают: ошибку в них исправляют новым коммитом или git revert (глава
5.1).
merge, rebase и squash-merge на одной ветке
Вернёмся к исходному состоянию: в main новая сигнатура Find, в
retry три коммита Ани до починки. Так бывает, когда CI проверил ветку раньше, чем
main ушёл вперёд. Вольём retry в три копии репозитория по очереди: слиянием,
rebase с перемоткой и squash-merge, который кладёт всю работу ветки в main одним коммитом.
$ git merge -q retry
$ git log --oneline --graph
* 6f84f94 (HEAD -> main) Merge branch 'retry'
|\
| * c357ae4 (retry) Искать заказ с повтором
| * 5058e4c Не ждать после последней попытки
| * e0377f6 Повторять запрос при ошибке
* | 1ee0b4e Описать запуск в README
* | f178e64 Передавать контекст в хранилище
|/
* 3b31e8b Добавить хранилище заказов
* a6f253d Создать модуль shop
$ git rebase -q main retry
$ git switch -q main
$ git merge -q --ff-only retry
$ git log --oneline --graph
* 48b7729 (HEAD -> main, retry) Искать заказ с повтором
* cb6bb38 Не ждать после последней попытки
* 67bce9b Повторять запрос при ошибке
* 1ee0b4e Описать запуск в README
* f178e64 Передавать контекст в хранилище
* 3b31e8b Добавить хранилище заказов
* a6f253d Создать модуль shop
$ git merge -q --squash retry
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
$ git commit -q -m 'Повторять поиск заказа при ошибке'
$ git log --oneline --graph
* 8848d58 (HEAD -> main) Повторять поиск заказа при ошибке
* 1ee0b4e Описать запуск в README
* f178e64 Передавать контекст в хранилище
* 3b31e8b Добавить хранилище заказов
* a6f253d Создать модуль shop
Ни одна из трёх копий не собирается. Виноватый коммит ищет git bisect: хороший
3b31e8b, плохой main, проверка go build ./... (как устроен
bisect run, смотри в главе 5.3). В копии со слиянием:
$ git bisect start main 3b31e8b
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[c357ae48749e42794b8607ac570c0e8c23e4aaaa] Искать заказ с повтором
$ git bisect run go build ./...
running 'go' 'build' './...'
Bisecting: 0 revisions left to test after this (roughly 1 step)
[1ee0b4e913593137fbaf58f01293fe2c8b5aac8e] Описать запуск в README
running 'go' 'build' './...'
6f84f94f943383ad5fe8606be46a204ff5dec1fa is the first bad commit
commit 6f84f94f943383ad5fe8606be46a204ff5dec1fa
Merge: 1ee0b4e c357ae4
Author: Боря <borya@example.com>
Date: Fri Sep 4 10:00:00 2026 +0300
Merge branch 'retry'
orders.go | 11 +++++++++++
retry.go | 16 ++++++++++++++++
2 files changed, 27 insertions(+)
create mode 100644 orders.go
create mode 100644 retry.go
bisect found first bad commit
Если прогнать то же в других копиях, выйти через git bisect reset, откатить найденный коммит
и удалить ветку, выйдет так:
| merge | rebase и fast-forward | squash-merge | |
|---|---|---|---|
| Что нашёл bisect | 6f84f94, слияние; обе стороны по отдельности собираются | 48b7729, один файл и 11 строк | 8848d58, вся ветка: два файла, 27 строк |
git revert HEAD | отказ без -m; с -m 1 уходит вся ветка | уходит только orders.go, сборка снова проходит | уходит вся ветка |
git branch -d retry | удаляет | удаляет | отказ: not fully merged |
git bisect:
слияние, один небольшой коммит или вся ветка разом. От этого зависит и то, что уберёт один
git revert.
После слияния bisect указал на merge-коммит, и ответ честный: каждая сторона собирается, сломало их
сочетание, искать придётся в обоих родителях. Revert слияния с -m 1 убирает всю ветку
(глава 3.1), а повторно влить её после починки так просто не выйдет: старые коммиты Ани git считает влитыми, и
retry.go не вернётся (ловушка разобрана в главе 5.1). После rebase виноват маленький
коммит, и откатить можно его одного. Но 48b7729 создал rebase, и никто его не собирал, пока
не сломался main. Переносишь ветку, гоняй тесты на каждом коммите:
git rebase -x 'go test ./...' main.
После squash-merge история короче всех и revert убирает ветку одной командой, зато bisect не пойдёт
глубже коммита на всю ветку. Для git retry не влита: в main лежит сумма её
изменений, а не её коммиты, и git log main..retry по-прежнему выводит три коммита. Если
поменять в retry паузу в retry.go и влить ветку снова, merge-base останется
3b31e8b: правка столкнётся со squash-копией старых коммитов, и выйдет конфликт add/add в
retry.go. На GitHub три
способа называются Merge pull request, Rebase and merge и Squash and merge, а GitLab при squash и методе
Merge commit положит поверх squash-коммита ещё merge-коммит.
Вопросы
4main, история остаётся прямой.
Squash-merge кладёт в main один новый коммит с суммой изменений. От выбора зависит, насколько
точно bisect укажет виноватого и что уберёт один revert.Что остаётся в истории
После merge в main исходные коммиты ветки и merge-коммит, а --first-parent
показывает влитую ветку одним коммитом (глава 1.2). Rebase ставит коммиты в линию с новыми
хешами. После squash-merge исходные коммиты живут только в ветке, пока её не удалили.
bisect и revert
- merge: bisect указывает на merge-коммит, если виновато сочетание сторон;
revertтребует-m 1и убирает ветку целиком (ловушки в главе 5.1); - rebase: bisect доходит до маленького коммита, и откатить можно его одного;
- squash-merge: коммит на всю ветку, revert простой, но точнее bisect не скажет.
Как выбирают
Свою ветку перед вливанием переносят на свежий main и гоняют тесты на каждом коммите.
Осмысленные коммиты сохраняют: rebase с перемоткой или merge, если нужна граница ветки в истории.
Ветку из «wip» и «правок по ревью» вливают squash-merge. Обычно команда закрепляет один способ в
настройках проекта.
Rebase без -x не проверяет ни один перенесённый коммит. В демо-репозитории перенос прошёл без
конфликтов, а 48b7729 не компилируется. Кто хвалит rebase за линейную историю и молчит
про -x или CI после переноса, получает лог с несобираемыми коммитами, которые потом
мешают bisect.
edit, упавший
-x), git rebase --abort возвращает всё как было. Сразу после окончания
помогает git reset --hard ORIG_HEAD: там прежняя вершина. Старые коммиты целы.Rebase ещё идёт
На остановке HEAD отсоединён, а ветка стоит на старом коммите, её rebase переставит только в конце.
Отсюда три пути: --continue после исправления, --skip, чтобы выбросить
текущий коммит, и --abort, чтобы отменить всё.
Rebase закончен
--abort уже ответит fatal: no rebase in progress. Прежняя вершина лежит в
ORIG_HEAD, сравнить её с новой можно через git range-diff main ORIG_HEAD retry,
а откат такой:
$ git reset --hard ORIG_HEAD
HEAD is now at c357ae4 Искать заказ с повтором
Делать это нужно сразу: ORIG_HEAD перезаписывают reset, merge,
следующий rebase и git stash, а второй такой же reset вернёт
перенесённую версию. Затёртое значение хранит reflog ветки (глава 5.2).
Если на остановке сделать git reset --hard ORIG_HEAD, рабочая копия вернётся к старому
коммиту, но rebase не закончится: git status всё так же пишет
interactive rebase in progress, а следующий git rebase откажет с
fatal: It seems that there is already a rebase-merge directory. На остановке выходят
через --abort.
--fixup=<коммит> и
вклеивают через git rebase -i --autosquash <база>. Сообщение меняют командой
reword в плане или через --fixup=reword:<коммит>. Сложную правку делают
на остановке edit. Переписываются этот коммит и все после него.Правка содержимого
git commit --fixup=f17a06f создаёт коммит fixup! Считать запросы к заказам, а
--autosquash по заголовку ставит его в план за целевым коммитом с командой
fixup: изменения вклеятся, сообщение останется прежним.
Остановиться на коммите
В плане меняют pick на edit, и rebase останавливается сразу после этого
коммита:
$ git rebase -i main
Stopped at b4e4e49... # Считать запросы к заказам
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
Дальше правят файлы, делают git commit -a --amend и git rebase --continue.
Разбить коммит можно так: git reset HEAD^, несколько коммитов по частям
(add -p, глава 2.1), потом --continue.
Что поменяется
Коммиты до исправленного сохраняют хеши, исправленный и все после него получают новые. Ветки поверх
этой перенесёт --update-refs, а отправленную ветку придётся пушить с
--force-with-lease (глава 4.1).
Обычный git rebase -i main fixup-коммиты не переставляет: они остаются на своих местах с
pick и после rebase живут отдельными коммитами fixup! …. Не забывай флаг
или включи rebase.autoSquash=true, она действует на интерактивный rebase.
--force-with-lease.Что видит сервер
Обычный push проходит, только если новая вершина потомок старой, поэтому после rebase он отклонён как
non-fast-forward. После git push --force-with-lease origin retry у Бори
git fetch напечатает строку с + c357ae4...48b7729 и пометкой
(forced update): плюс значит принудительное обновление, слева старая вершина.
Что происходит у коллеги
Боря начал ветку retry-metrics поверх старой retry и поправил в ней
retry.go. Если Аня только перенесла
коммиты и патчи не поменялись, хватит обычного rebase: git узнает старые копии по patch-id и
пропустит их.
$ git rebase origin/retry # строки hint: убраны
warning: skipped previously applied commit e0377f6
warning: skipped previously applied commit 5058e4c
warning: skipped previously applied commit c357ae4
Successfully rebased and updated refs/heads/retry-metrics.
Если патчи менялись (склейка, починка, разрешённый конфликт), git rebase origin/retry старые
версии не узнает и попробует применить их второй раз. Тогда переносят только свои коммиты, указав
старую вершину из строки fetch: git rebase --onto origin/retry c357ae4.
git pull --rebase находит её сам (глава 4.1).
После fetch git status у Бори пишет, что ветка и origin/retry «have
diverged, and have 4 and 5 different commits each», и предлагает git pull.
git pull --no-rebase сольёт старые копии коммитов Ани с новыми: у Бори это кончилось
конфликтом add/add в retry.go, а в ветке без правок в тех же файлах обе копии молча легли
в историю рядом. git pull --rebase переносит только коммит Бори.
Что стоит запомнить
- Rebase воспроизводит patch каждого commit поверх новой базы и создаёт новые commit objects.
- ORIG_HEAD и reflog помогают найти исходное положение указателя после операции.
- --onto позволяет выбрать новую базу и диапазон переносимых коммитов отдельно.
Частые ошибки
- Не переписывай общую ветку, на которую уже опираются коллеги.
- Похожие patch не гарантируют одинаковый commit hash: hash включает родителей и метаданные.
Самопроверка
Сначала реши задачу самостоятельно, затем открой ответ и сравни ход рассуждений.
Чтение кода
1. Сравни hash до и после rebase
До rebase feature содержит коммит F на базе A. После переноса на B сообщение и diff остались прежними, но hash стал F2. Почему?
before: A---F
\
B
after: A---B---F2
Ответ и разбор
Rebase создаёт новый commit с родителем B. Hash вычисляется из содержимого commit object, куда входит ссылка на родителя, поэтому при новом parent hash меняется даже при прежнем сообщении и patch. Старый F может оставаться доступен через reflog или другую ссылку.
Код-ревью
2. Не force-push чужую основу
Feature уже опубликована и коллега добавил в неё commit. После локального rebase инструкция предлагает:
git push --force origin featureКак отправить переписанную историю с защитой и почему всё равно нужно согласование?
Ответ и разбор
Слепой force может затереть commit коллеги, который появился после fetch. Сначала синхронизируйся и проверь, что переписываемая ветка общая и изменение согласовано; затем используй --force-with-lease, чтобы отказать при неожиданном продвижении remote ref. Lease не заменяет координацию и проверку diff.
Применение
3. Перенеси только часть ветки
В feature есть коммиты A, B и C поверх main. Нужны только B и C, но посадить их надо на новую ветку release. Какую роль здесь играет --onto?
Ответ и разбор
Укажи git rebase --onto release A feature: A задаёт границу, а коммиты после A до feature переносятся на release. До выполнения проверь граф, что A действительно предок и что переносимый диапазон содержит нужные B и C.
Применение
4. Вернись после остановленного rebase
Rebase остановился на конфликте, и ты решил отменить всю операцию. Какая команда восстанавливает исходную ветку и что проверить после неё?
Ответ и разбор
Выполни git rebase --abort, затем проверь git status, текущую ветку и diff. Если команда уже завершилась, abort не сработает; тогда найди исходный указатель в ORIG_HEAD или reflog и сначала осмотри commit, прежде чем передвигать ветку.