В конечном счете, вы захотите git rebase --onto
. Однако иногда вам не нужно делать ничего особенного.
Настройка
Давайте нарисуем вашу начальную ситуацию:
...--A--B <-- master
\
C <-- feature/A
\
D <-- feature/B
То есть, есть ряд коммитов на некоторой главной строке (я назвал это master
здесь, но это может быть develop
или что-то еще), затем один коммит на вашем feature/A
, затем один коммит на вашем feature/B
. Родителем коммита D
на вашем feature/B
является ваш коммит C
на feature/B
и feature/A
.
Несколько позже вы добавили второй коммит к вашему feature/A
, что дает:
...--A--B <-- master
\
C--E <-- feature/A
\
D <-- feature/B
В конце концов, feature/A
должно быть объединено с master
, и согласно некоторому правилу политики вы сделали новый коммит F
, который является комбинацией C
и E
, так что теперь у вас есть:
F <-- feature/A
/
...--A--B <-- master
\
C--E [abandoned]
\
D <-- feature/B
На данный момент вы хотите скопировать D
в новый коммит D'
, который выглядит точно так же, как D
с точки зрения его дифференциал по отношению к его родителю, но где родитель D'
равен F
вместо C
.
Git предлагает легкий-i sh способ получить то, что вы хотите:
git checkout feature/B
git rebase --onto feature/A something-goes-here
Проблема в части something-goes-here
. Что там происходит?
Копирование некоторых коммитов
Команда git rebase
, по сути, представляет собой просто серию git cherry-pick
команд, за которыми следует движение метки ветвления. Как вы уже обнаружили, git cherry-pick
делает то, что вы хотите: он копирует коммит. Фактически, он может копировать более одного коммита (используя то, что Git вызывает, внутренне, sequencer ).
То есть он сравнивает каждый коммит, который будет скопирован, с коммитом родитель , чтобы посмотреть, что изменилось. Затем он вносит те же изменения в текущий коммит и, если все идет хорошо, фиксирует результат.
Например, давайте начнем с этой ситуации. На данный момент я вставил новый ярлык saved-A
, чтобы запомнить фиксацию E
, и добавил имя new-B
и добавил HEAD
в скобках, чтобы показать, что текущая ветвь - это new-B
, а текущий коммит - это коммит F
:
F <-- feature/A, new-B (HEAD)
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B
Теперь мы можем запустить git cherry-pick feature/B
. Мы говорим Git: Сравните коммит D
с его родителем C
, затем внесите те же самые изменения в то место, где мы сейчас находимся в коммите F
, и передайте результат. Если все все идет хорошо, мы получаем:
D' <-- new-B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B
Все, что нам нужно сейчас сделать, это нажать имя feature/B
, чтобы указать D'
, а затем сбросить имя new-B
:
D' <-- feature/B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D [abandoned]
Опять же, первая часть этого именно то, что делает git cherry-pick
: скопировать один коммит. последняя часть этого - то, что git rebase
делает: перемещает метку ветви как feature/B
.
Ключ здесь в том, что git rebase
копий некоторые совершает. Какие из них? Ответ по умолчанию - неправильный ответ для вас!
Что делает git rebase
, в двух словах
Давайте посмотрим на немного другой рисунок:
...--A--B <-- target
\
C--D--E <-- current (HEAD)
Здесь мы находимся "на" ветке current
, то есть git status
скажет on branch current
. Коммит-коммит current
- это коммит E
: E
's ha sh ID - это га sh ID, хранящийся в имени refs/heads/current
.
Если мы теперь запустим:
git rebase target
Git будет копировать коммиты C-D-E
для новых коммитов C'-D'-E'
и помещать новые коммиты поверх target
, а затем перемещать имя ветки, например:
C'-D'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C--D--E [abandoned]
Это обычно то, что мы хотим. Но: Как git rebase
узнал, что нужно копировать C-D-E
, но не копировать A
тоже?
Ответ таков: git rebase
использует внутренний список Git некоторые операции коммитов, git rev-list
, с остановкой Документация по перебазировке утверждает, что git rebase
выполняет:
git rev-list target..HEAD
, что немного похоже на белый l ie: это достаточно близко и показательно. Точные детали хитрее, и мы немного разберемся. А пока давайте посмотрим на target..
часть target..HEAD
. Это говорит Git: не перечислять какие-либо коммиты, которые вы можете найти, начиная с цели и работая в обратном направлении.
Поскольку target
names коммит B
, это означает: не копировать коммит B
. Ну, мы уже не собирались копировать commit B
, так что ничего страшного. Но это также означает: не копировать коммит A
. Почему бы нет? Потому что commit B
указывает на коммит A
. Коммит A
находится в обеих ветках, target
и current
. Таким образом, мы скопировали бы A
, но мы этого не делаем, потому что он в списке не копирует . Есть коммиты до A
, но они все в не копируют часть, поэтому ни один из них не копируется.
Следовательно, это коммиты C-D-E
которые копируются здесь: они находятся в списке для копирования, и их нельзя остановить, если они находятся в списке «не копировать».
Итак, что делает git rebase
, в двух словах, this:
- Запомните, к какой ветви
HEAD
прикреплен. - Перечислите некоторые идентификаторы коммитов ha sh для копирования.
- Отделите
HEAD
от текущая ветвь. - Копировать перечисленные коммиты, по одному, как если бы
git cherry-pick
. - Переместить имя ветки, к которой было прикреплено
HEAD
туда, где мы сейчас находимся. - Повторное присоединение
HEAD
к перемещенной ветви.
Обратите внимание, что на шаге 4 все может go неверно. В частности, копирование commit, как если бы git cherry-pick
- независимо от того, использует ли он на самом деле git cherry-pick
- может иметь конфликт слияния . Если это так, то перебазирование останавливается посередине с отсоединенной ГОЛОВКОЙ. Вот почему знание о шаге 3 важно. Но мы оставим это для других вопросов и ответов (вместе с подробностями о том, действительно ли rebase использует вишневый кир: иногда это делает, иногда притворяется).
Правда о том, что совершается Копирование
Мы упоминали, что вышеприведенная вещь target..HEAD
была белого цвета l ie: упрощение, призванное облегчить понимание того, какие коммиты копируются. Настало время для истины.
Во-первых, git rebase
обычно пропускает слияния совершаются полностью. Любой коммит, который будет сгенерирован с помощью git rev-list
выше, удаляется, если это слияние (имеет двух или более родителей). Пока в вашем списке нет коммитов слияния, это все равно не имеет значения.
Во-вторых, git rebase
также пропускает коммиты, которые идентификатор патча, эквивалентный некоторым другим коммитам. Для этого используется программа git patch-id
. Здесь мы не будем go вдаваться в подробности, кроме как заметим, что для получения части "некоторые другие коммиты" Git фактически должен использовать git rev-list target...HEAD
с тремя точками. Это создает список симметри c разница коммитов, достижимых из HEAD
, но не цели, а также коммиты, достижимые из target
, но не HEAD
. Подробнее о достижимости см. Думайте как (а) Git. Затем команда rebase использует git patch-id
для каждого коммита в двух списках - который он генерирует для себя, поэтому он знает, какой коммит ha sh идет с каким списком - и выбивает те, которые имеют соответствующие идентификаторы патчей. Эффект этого состоит в том, что если, например, commit B
равен уже так же (по вишне), что и commit D
, вместо копирования C-D-E
, мы просто скопируем C-E
, чтобы получить:
C'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C--D--E [abandoned]
, поскольку коммиты B
и D
"делают то же самое".
Последний и самый важный для нас здесь, --onto
позволяет нам использовать другую цель .
В приведенном выше примере мы запустили:
git rebase target
и target
был нашим стоп-аргументом для git rev-list stop..HEAD
и нашей целью, для которой Git поместил копии. Но мы можем запустить:
git rebase --onto target stop
и теперь git rebase
будет использовать наш stop аргумент для stop
части git rev-list
, продолжая при этом использовать target аргумент для места копирования go.
Итак, предположим, что нам дано this сейчас:
...--A--B <-- target
\
C <-- another
\
D--E <-- current (HEAD)
и мы выполняем:
git rebase --onto target another
Теперь мы сказали Git, что аргумент stop для нашей перебазировки равен another
, который выбирает commit C
. Наша перебазировка будет использовать git rev-list
для another..HEAD
или C..E
, что означает, что список коммитов для копирования будет состоять всего из D-E
.
Этот список будет далее отфильтрован патчем. правила id и no-merges, но пока B
не совпадает с D
, мы получим:
D'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C <-- another
\
D--E [abandoned]
То есть мы скопирую только два коммита D-E
, которые достижимы с current
, опуская коммит C
, который доступен с another
.
Собираем все вместе
Вот ваши настройки в то время, когда вы хотите выполнить коммит копирование:
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B (HEAD)
Обратите внимание, что мы добавили имя saved-A
, чтобы запомнить, что не для копирования. Мы не хотим копировать коммиты C
и E
. В любом случае, мы не будем копировать E
, но это простой способ запомнить все , а не копировать.
В настоящее время мы извлекли feature/B
(commit D
). Нам не нужно создавать имя, new-B
, поэтому мы этого не делали. Теперь мы просто запустим:
git rebase --onto feature/A saved-A
Git теперь перечислит коммиты для копирования: каждый коммит, который находится в текущей ветви, feature/B
, , за исключением каждый коммит, который включен saved-A
. Итак, это коммит D
.
Git теперь отсоединяет HEAD, переходит к коммиту F
- наша --onto
цель - и копирует D
для получения D'
. Это завершает список коммитов для копирования, поэтому, успешно скопировав D
в D'
, Git принудительно перемещает имя feature/B
для указания на D'
и повторно присоединяет HEAD
, давая нам:
D' <-- feature/B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D [abandoned]
именно это мы и хотим.
Теперь мы можем удалить имя saved-A
.
Что если вы не сохранили имя?
Что, если вы уже выполнили ребазинг feature/A
, но забыли сохранить коммит ha sh Идентификатор коммита E
где-нибудь?
К счастью, у вас нет для сохранили идентификатор ha sh либо E
, либо C
. Вы можете:
- найти их, используя
git log
, или - , использовать
git reflog
, чтобы найти идентификаторы ha sh, которые feature/A
использовал для имен, или - делайте все что угодно, чтобы найти их.
Идентификаторы ha ha sh работают, поэтому вы можете просто запустить:
git rebase --onto feature/A <hash-ID-of-E-or-C>
после того, как найдете га sh ID. (Используйте метод «вырезать и вставить» или аналогичный, чтобы получить правильный идентификатор ha sh; ввод его или даже уникальный префикс вручную представляет собой рецепт ошибок.)
Имена рефлогов также работают Довольно часто вы можете сделать:
git rebase --onto feature/A feature/A@{1}
, где feature/A@{1}
- это имя reflog, которое вы увидите для ha sh ID коммита E
, когда вы запускаете git reflog feature/A
в список из предыдущих идентификаторов га sh для feature/A
. (feature/A@{2}
вероятно имена коммитов C
, так что это также сработает.)
Ключ в том, чтобы найти коммиты, которые вы хотите пропустить, и использовать их с git rebase --onto
. Установите target в зависимости от того, где копии должны go, и установите точку остановки - то, что документация git rebase
вызывает аргумент upstream - га sh удостоверение личности, которое останавливает коммиты, которые вы не не хотите скопировать.
Когда вам не нужно ничего особенного?
Если вы раздавлены коммиты имеют тот же идентификатор патча, что и исходные коммиты, git rebase
' пропускает коммиты с соответствующими идентификаторами патчей сделает всю работу за вас. Как правило, это происходит только в том случае, если у вас был один коммит, который получил squa sh, слитый с какой-либо другой ветвью.
Трюк --onto
всегда работает, поэтому вы не нужно беспокоиться об этом случае, но если это часто случается, это приятно знать.