Нет хорошего универсального инструмента, чтобы делать то, что вы хотите. Есть определенные c трюки, которые могут работать для вас. В частности, иногда вам понадобится git rebase --onto
, и вам придется использовать его с осторожностью.
Фон
Проблема в том, что Git ветви не вкладываются и не складываются, или любое другое слово, которое вы хотели бы использовать здесь.
Точнее, имена ветвей , такие как master
или branch1
до branch3
, просто действуют как указатели или метки. Каждый указывает на (или вставляется) один конкретный коммит. Они не имеют никакого отношения друг к другу: вы можете добавлять, удалять или перемещать любой ярлык в любом месте и в любое время. Единственное ограничение на каждую метку состоит в том, что она должна указывать на ровно один коммит.
Количество коммитов не так много на a ветви , как содержится в пределах некоторых набор ветвей . У данной пары коммитов могут быть отношения родитель / ребенок. Например, в ваших чертежах commit c1
является родителем commit c2
. Git фактически достигает этого, когда коммиты указывают на другие коммиты, подобно тому, как имена ветвей указывают на коммиты. Однако есть разница: содержимое любого коммита замораживается на все времена, включая его указатель. Это означает, что child указывает на parent . Родитель существует, когда вы делаете ребенка, но не наоборот, поэтому ребенок может указывать на родителя, но не наоборот.
(В действительности, Git работает в обратном направлении. Вы нарисовали стрелки движение вперед, что назад для Git: дети указывают назад на родителей.)
Git нужен способ найти каждый фиксированный за все время коммит. Способ заключается в их х sh идентификаторах: этих больших уродливых строках букв и цифр (что на самом деле является 160-битным значением, выраженным в шестнадцатеричном формате). Чтобы указать на коммит, что-то - имя ветки или другой коммит - просто содержит необработанный идентификатор ha sh указанного коммита. Если у вас есть идентификатор ha sh или если он есть у Git, вы можете Git найти базовый объект по этому идентификатору ha sh. 1
Git определяет ветвь name , содержащую необработанный га sh ID last commit, который должен считаться частью цепочки коммитов. Предыдущие коммиты, найденные с помощью стрелок, указывающих назад, исходящих из каждого коммита, это на или , содержащиеся в этой ветви. Итак, здесь я перейду к своей обычной записи прописных букв для каждого коммита - если у вас есть:
A <-B <-C <-D <-- master
\
E <-F <-- branch
, то коммит F
- это последний коммит branch
, но E
, D
и т. д. вплоть до A
все содержатся в branch
. Коммит D
- это последний коммит master
, но все A-B-C-D
в master
.
Обратите внимание, что при первом создании новое имя ветки, оно обычно указывает на тот же коммит, что и на существующее имя ветки:
A--B--C--D <-- master
\
E--F <-- branch1, branch2
Git вы присоединяете его HEAD
к одной из этих веток и делаете новый коммит, который получает новый га sh ID. Git записывает идентификатор new коммитов ha sh в имя ветви, к которой прикреплен HEAD
:
A--B--C--D <-- master
\
E--F <-- branch1
\
G <-- branch2 (HEAD)
, и все инварианты все еще сохраняются: branch2
содержит имя (ha sh ID) last commit в этой ветви, branch1
содержит ha sh ID его последнего коммита, master
содержит имя его последнего коммита, и скоро. Нет коммит имеет изменено (ни одна из частей коммита не может изменить), но новый коммит существует сейчас, и текущая ветвь все еще HEAD
прикреплен к нему, но был перетащен вперед.
1 Коммиты, в Git, являются одним из четырех видов внутреннего объекта типы. Остальными тремя являются blob , tree и tag объекты. Обычно единственные Git га sh идентификаторы, с которыми вы взаимодействуете каждый день, например с вырезанием и вставкой до git log
или git show
или git cherry-pick
, или в git rebase -i
инструкциях - это идентификаторы коммитов га sh. У коммитов есть специальное свойство, состоящее в том, что их содержимое всегда уникально, поэтому их идентификаторы ha sh также всегда уникальны. Git гарантирует это, добавляя отметку даты и времени к каждому коммиту. Этого, плюс тот факт, что каждый коммит имеет идентификатор ha sh своего родителя (ей), достаточен для создания необходимой уникальности.
Rebase - это копирование коммитов
Как отмечалось выше, ни одна часть любого коммита не может быть изменена. Коммиты заморожены на все времена. Самое большее, вы можете просто остановить , используя коммит. Git находит коммиты, начиная с последних - советы по ветвям - и работая в обратном направлении, и если вы перестанете использовать коммит, и настройте так, чтобы Git не смог найти это, Git в конечном итоге удалит его по-настоящему.
Однако вы можете взять коммит - любой коммит, в том числе исторический - и поработать с ним, а затем сделать новый коммит из этого. Возможно, здесь стоит отметить небольшое замечание о режиме «detached HEAD».
Допустим, у нас есть этот - тот же график, который вы нарисовали, но с использованием моего однобуквенного стиля - с теми же именами ветвей:
A--B--C--D <-- master
\
E--F <-- branch1
\
G--H <-- branch2 (HEAD)
\
I--J <-- branch3
Обычный способ работы с коммитом:
- Мы выбираем один, выбирая имя ветви.
- Git присоединяет специальное имя
HEAD
к этому имени ветви. - Это имя ветви теперь является текущей веткой , а commit теперь является текущим коммитом .
- Git копирует замороженный снимок для этого коммита в индекс Git и ваше рабочее дерево (здесь мы не будем go вдаваться в детали).
Мы можем Git извлечь коммит G
, однако, выбрав его по имени: его уникальный идентификатор ha sh. Когда мы это сделаем, мы получим detached HEAD , где HEAD
непосредственно указывает на коммит:
A--B--C--D <-- master
\
E--F <-- branch1
\
G <-- HEAD
\
H <-- branch2
\
I--J <-- branch3
Если бы мы сделали новый коммит в этом состоянии, мы бы в Факт получить один. Я назову это X
, а не K
, так как мы просто отбросим его и на мгновение забудем об этом, но давайте нарисуем такой результат:
A--B--C--D <-- master
\
E--F <-- branch1
\
G--X <-- HEAD
\
H <-- branch2
\
I--J <-- branch3
Обратите внимание, что X
является обычным во всех отношениях , за исключением , единственное имя , которое находит это HEAD
. Если бы мы дали ему имя ветки, это сделало бы коммит намного более постоянным: он продлился бы до тех пор, пока мы не удалили его имя ветви, или иначе не сделали бы коммит не доступным для поиска.
Конечно, это не совсем так. что ты делаешь. Вместо этого вы делаете новый коммит, который я назову K
(вы назвали его c11
) на branch1
обычным способом attach-HEAD:
A--B--C--D <-- master
\
E--F--K <-- branch1 (HEAD)
\
G--H <-- branch2
\
I--J <-- branch3
На этом этапе вы ' Я хотел бы скопировать коммиты G-H-I-J
в новые и улучшенные коммиты. Команда git rebase
может сделать это, так как это ее работа. Но давайте рассмотрим как выполняет свою работу.
Как работает rebase
Поскольку rebase - это копирование (некоторых) коммитов, его работа разделен на три фазы:
Фаза 1 должна решить , который обязуется копировать .
Как вы видели, коммиты часто включены много филиалов. Мы хотим скопировать те, которые находятся в нашей ветке, но также не находятся где-то еще. Например, если мы сейчас находимся на branch2
и говорим git rebase branch1
, мы хотим скопировать G-H
, но не E-F
или какой-либо из предыдущих коммитов.
Основной аргумент git rebase
это то, что в документации называется upstream
. Вот это branch1
. Копии коммитов от до доступны из нашей текущей ветки - от HEAD
или branch2
; оба выбирают один и тот же набор коммитов - минус тех коммитов, которые доступны из названия branch1
. Поэтому сначала перебазируем список всех коммитов в нашей текущей ветке , но затем выбивает из списка коммитов для копирования, все те, которые находятся на цели / upstream
. Этот список в конечном итоге содержит необработанные идентификаторы ha sh исходных коммитов.
Документация git rebase
описывает этот листинг как:
Все изменения, сделанные коммитами в текущей ветви, но не находящиеся в <upstream>
, сохраняются во временную зону. Это тот же набор коммитов, который будет показан git log <upstream>..HEAD
; или git log 'fork_point'..HEAD
, если --fork-point
активен (см. описание --fork-point
ниже); или git log HEAD
, если указан параметр --root
.
На самом деле это не полная картина, но это хорошее начало. Более полную картину мы получим в следующем разделе.
Фаза 2 о фактически копирует коммиты . Git использует git cherry-pick
или что-то в большей степени эквивалентное 2 для копирования. Мы пропустим сразу, как работает cherry-pick, за исключением того, что, как вы видели, он может получить конфликты слияния.
Здесь мы заметим, что копирование занимает переведите в режим * HEAD . Git сначала выполняет проверку стиля целевого коммита в стиле отсоединенного заголовка. Здесь, поскольку мы сказали git rebase branch1
, целью является коммит K
, поэтому копирование начинается с:
A--B--C--D <-- master
\
E--F--K <-- branch1, HEAD
\
G--H <-- branch2
\
I--J <-- branch3
с Git запоминанием имени branch2
(в файл: если вы покопаетесь в каталоге .git
во время частичной перебазировки, вы найдете каталог, полный состояния перебазировки).
Список коммитов для копирования на этом этапе - коммиты G
и H
, в этом порядке, и с использованием их реальных идентификаторов ha sh, какими бы они ни были на самом деле. Git копирует эти коммиты, по одному, в новые коммиты, чьи снимки и родители немного отличаются от оригиналов. Это дает нам этот новый набор коммитов, все еще в режиме отсоединенного HEAD:
A--B--C--D ... G'-H' <-- HEAD
\ /
E--F--K <-- branch1
\
G--H <-- branch2
\
I--J <-- branch3
Последняя фаза git rebase
заключается в переносе имени ветви.
Git выискивает сохраненное имя ветви, заставляет его указывать на текущий (HEAD
) коммит - в нашем случае H'
- и повторно присоединяет HEAD
. Итак, теперь у вас есть:
A--B--C--D ... G'-H' <-- branch2 (HEAD)
\ /
E--F--K <-- branch1
\
G--H
\
I--J <-- branch3
Обратите внимание, что на данный момент нет имени выбирается коммит H
больше. 3 Мы могли бы выправить излом на графике, но я оставил его для симметрии, и по другой причине мы увидим в следующем разделе.
2 Rebase может использовать один из нескольких «бэк-эндов». Неинтерактивный бэкэнд по умолчанию был git-rebase--am
вплоть до Git 2.26.0, но это больше не так. Серверная часть am
использует git format-patch
и git am
, отсюда и название. Он пропускает некоторые случаи переименования файлов и не может копировать коммит пустых различий, но может быть намного быстрее в некоторых относительно редких случаях перебазирования.
3 На самом деле, есть хотя бы одну reflog запись , хотя бы в настройках по умолчанию. Мы вернемся к этому позже.
Лучшее представление о том, что копирует rebase
Я упоминал выше, что в фазе 1, когда rebase перечисляет коммиты для копирования, он не ' действительно использовать метод <upstream>..HEAD
. В документации даже есть предупреждения (о режиме fork-point
), но в нем недостаточно предупреждений.
Всякий раз, когда у вас есть Git копий, - будь то, запустив git cherry-pick
самостоятельно, или любой другой метод, включая перебазирование - вы получаете коммиты, которые могут «делать то же самое» друг с другом. То есть, учитывая коммиты H
и H'
, мы могли бы запустить:
git show <hash-of-H>
, чтобы просмотреть различие между коммитом G
и коммитом H
, чтобы увидеть, что делает H
. Мы могли бы выполнить:
git show <hash-of-H'>
, чтобы просмотреть разницу между commit G'
и commit H'
, чтобы увидеть, что делает H'
.
Если мы уберем номера строк в этом списке различий, мы получим такие же изменения . 3 Git включает команду git patch-id
, которая читает список различий, удаляет номера строк - и некоторые пробелы, так что, например, конечный пробел не влияет на вещи - и хэширует результат. Это производит то, что Git называет ID патча .
В отличие от га * комита ID, который гарантированно будет уникальным для этого конкретного коммита, так что наша выбранная вишня копия - это другой коммит - патч -ID умышленно тот же , если коммит "делает то же самое". Итак:
git show <hash-of-either-H-or-H'> | git patch-id
покажет, что H
и H'
в некотором смысле являются "одним и тем же" коммитом.
Когда вы запустите git rebase
, Git на самом деле вычислить ha sh идентификаторы группы коммитов. Для тех, кто является «тем же коммитом», Git выбьет те коммиты из списка коммитов для копирования.
(По умолчанию rebase также выбивает все merge commits вне списка. В этих примерах у вас их нет, поэтому нам не нужно беспокоиться об этом здесь.)
Следовательно, если мы сейчас запустим:
git checkout branch3; git rebase branch2
Git возьмет этот график:
A--B--C--D ... G'-H' <-- branch2
\ /
E--F--K <-- branch1
\
G--H--I--J <-- branch3 (HEAD)
и внесет в список A-B-C-D-E-F-G-H-I-J
список branch3
, но затем выбьет A-B-C-D-E-F-K-G'-H'
, потому что это список branch2
, Это оставляет G-H-I-J
отправной точкой перед выполнением части с идентификатором патча. Другими словами:
branch2..HEAD
- это G-H-I-J
.
Но теперь Git вычисляет идентификатор патча для G
, H
, I
и J
. Затем он также вычисляет идентификаторы исправлений для K
, G'
и H'
. 4 Код перебазирования обнаруживает, что G
уже имеет эквивалентную фиксацию идентификатора исправления, G'
, в вверх по течению. Так что G'
выбивается из списка. Затем он обнаруживает, что H
также имеет H'
в восходящем направлении, поэтому H
выбивается из списка.
Окончательный список коммитов для копирования на этом этапе - I-J
: именно то, что вы хотели , Git теперь может отсоединить HEAD
при коммите H'
и скопировать I-J
, а затем повторно присоединить HEAD
к результату:
I'-J' <-- branch3 (HEAD)
/
A--B--C--D ... G'-H' <-- branch2
\ /
E--F--K <-- branch1
\
G--H--I--J [abandoned]
3 Точнее, мы обычно получим те же изменения. Иногда мы не получим такие же изменения, если у нас был конфликт слияния во время вишневого пика.
4 Причина этого конкретного списка в том, что это коммиты, созданные git rev-list branch2...HEAD
. Обратите внимание на три точки: это синтаксис Git для операции установки симметрия c разница . Эта симметричная c разница состоит из коммитов, доступных с HEAD
, но не branch2
, плюс коммитов, достижимых с branch2
, но не HEAD
. Один сет становится коммитом «левой стороны», а один сет становится коммитом «правой стороны». Коммит-копии копируются с левой стороны G-H-I-J
, и все получают ID-ID-патча; коммиты в восходящем потоке, которые также get patch-ID-ed, являются списком справа.
Где это идет не так
Сноска 3 (выше) является ключом к тому, где это идет не так. Если во время разрешения конфликта вы получите , изменив некоторые коммиты каким-либо существенным образом, вычисления идентификатора патча больше не будут работать, чтобы выбить некоторые коммиты.
Когда вы go перебазируете branch3
, на этот раз Git решает скопировать G
в G'
снова и / или скопировать H
в H'
снова. Каждая копия почти гарантированно столкнется (как в случае конфликта слияния) с копией, уже присутствующей при текущей сборке новых коммитов замены.
Правильное действие - опустить G
и H
в процессе копирования. Rebase сделал бы это для вас, используя трюк с идентификатором патча, за исключением того, что трюк с идентификатором патча не удался.
Использование --onto
В вашем случае вы хотите, чтобы rebase скопировал некоторые фиксируют, но не все фиксирует в диапазоне <upstream>..HEAD
, размещая копии в нужной точке. У вас есть:
A--B--C--D ... G'-H' <-- branch2
\ /
E--F--K <-- branch1
\
G--H--I--J <-- branch3 (HEAD)
и вы хотите сказать rebase: Копировать I
и J
, но не H
и, следовательно, не G
. Поместите копии после H'
на кончике branch2
.
Один аргумент не сработает, но two сделает. Предположим, вы могли бы сказать:
git rebase --dont <hash-of-H> --onto branch2 # not the actual syntax
например? К счастью, git rebase
имеет это встроенный. Фактический синтаксис:
git rebase --onto branch2 <hash-of-H>
Аргумент --onto
позволяет вам указать цель копий, освобождая аргумент upstream
, что означает что не копировать .
Rebase все равно будет выполнять ту же работу с идентификатором патча, но, запустив его со списком G-H
, он не сможет ошибиться. Конечный результат - именно то, что вы хотите.
Использование reflog или других трюков, чтобы найти H
Раздражающая часть здесь находит H
ha sh ID , С помощью этих диаграмм я могу беспечно сказать <hash-of-H>
, но в реальной ситуации, с реальными графиками и десятками коммитов, которые все выглядят одинаково, обнаружение идентификаторов ha sh является проблемой в заднице. Если бы только был простой способ получить это право.
Как оказалось, есть.
Всякий раз, когда Git движется a имя ветви, как, например, git rebase
, оставляет след предыдущих значений. Этот след идет в Git 1530 * reflogs . Существует рефлог для каждого имени ветви, плюс один для HEAD
. HEAD
очень активен и не так полезен, потому что он слишком активен, но тот для branch2
идеален.
Помните, как мы рисовали:
A--B--C--D ... G'-H' <-- branch2 (HEAD)
\ /
E--F--K <-- branch1
\
G--H
\
I--J <-- branch3
изначально. Я сказал, что оставил это для симметрии и другой причины , и теперь пришло время для причины. Мы можем использовать имя branch2@{1}
для обращения к записи reflog для "где branch2
был одним шагом / branch2
-change a go". Пока «один шаг a go» был как раз перед ребазингом, это означает «commit H
». Итак:
git checkout branch3
git rebase --onto branch2 branch2@{1}
делает свое дело.
Если вы сделали что-то в branch2
с момента вашей перебазировки - например, если вы создали, протестировали и зафиксировали - вам может понадобиться большее число чем @{1}
. Используйте git reflog branch2
, чтобы распечатать фактическое содержимое журнала, чтобы проверить.
Другой альтернативой является удаление ветви или имени тега, указывающего на фиксацию H
до , когда вы перебазируете branch2
в все. Например, если вы создадите новое имя branch2-old
или branch2.0
или что-то еще, у вас все равно будет:
A--B--C--D ... G'-H' <-- branch2
\ /
E--F--K <-- branch1
\
G--H <-- branch2-old
\
I--J <-- branch3
(независимо от того, где сейчас находится HEAD
). Вы можете пометить коммит J
как branch3-old
перед тем, как запустить его rebase.
(Reflogs удобны и обычно работают нормально. Однако имена ветвей дешевы.)
Подумайте также о том, чтобы выполнить перебазирование одним упавшим oop
Предположим, у вас есть этот график:
A--B--C--D <-- master
\
E--F--U <-- branch1
\
G--H <-- branch2
\
...
\
T <-- branch9
где U
- это новый коммит, который вы хотел бы иметь во всех branchN
предках. Если вы запустите:
git checkout branch9; git rebase branch1
, вы получите копии коммитов G-H-...--T
, все в одной операции. Теперь вы можете взять branch2
, branch3
, ..., до branch8
и просто переместить каждый из них, чтобы указать на соответствующий скопированный коммит. Совпадение оригинальных коммитов с их копиями - это работа для инструмента, но, к сожалению, этого инструмента не существует. Так что, если вы go таким образом, это своего рода руководство.
Также имейте в виду, что это не работает в некоторых случаях:
A--B--C--D <-- master
\
E--F--K <-- branch1
\
G--H--L <-- branch2
\
I--J <-- branch3
Перебазировка branch3
на branch1
копирует только G-H-I-J
, а не L
. Так что вам все еще может понадобиться случайный git rebase --onto
. (Надлежащий инструмент сделает все это.)