Разница между 'rebase master' и 'rebase --onto master' из ветви, полученной из ветви master

Учитывая следующую структуру ветвления:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)

Если я хочу объединить мои изменения B (и только , мои изменения B, никаких изменений A) в мастер, в чем разница между эти два набора команд?

>(B)      git rebase master
>(B)      git checkout master
>(master) git merge B

>(B)      git rebase --onto master A B
>(B)      git checkout master
>(master) git merge B

Мне в основном интересно узнать, сможет ли код из ветви A превратиться в мастер, если я буду использовать первый способ.

29
задан MrMisterMan 26 November 2015 в 16:23
поделиться

5 ответов

Потерпи меня немного, прежде чем я отвечу на вопрос в том виде, как он задан. Один из ранних ответов правильный, но есть маркировка и другие относительно незначительные (но потенциально запутанные) проблемы, поэтому я хочу начать с чертежей ветвей и меток ветвей. Кроме того, люди, пришедшие из других систем, или, может быть, просто новички в управлении ревизиями и git, часто думают о ветвях как о «линиях развития», а не как «следах истории» (git реализует их как последние, а не как первые, поэтому коммит не обязательно находится на какой-либо конкретной «линии развития»).

Во-первых, есть небольшая проблема с тем, как вы нарисовали свой график:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)

Вот тот же самый график, но с метками, нарисованными по-разному, и добавлены еще некоторые стрелки (и я нумеруются узлы коммитов для использования ниже):

0 <- 1 <- 2         <-------------------- master
           \
            3 <- 4 <- 5 <- 6      <------ A
                       \
                        7 <- 8 <- 9   <-- HEAD=B

Почему это важно, потому что git совершенно не понимает, что означает фиксация «на» некоторой ветви - или, возможно, лучше сказать, что некоторый коммит "содержится" в некотором наборе ветвей. Коммиты не могут быть перемещены или изменены, но метки ветви могут и действительно перемещаться.

В частности, имя ветви , например, master, A или B, указывает на один конкретный коммит . В этом случае master указывает на фиксацию 2, A указывает на фиксацию 6 и B указывает на фиксацию 9. Первые несколько коммитов с 0 по 2 содержатся во всех трех ветвях; коммиты 3, 4 и 5 содержатся как в A, так и в B; коммит 6 содержится только внутри A; и коммиты с 7 по 9 содержатся только в B. (Кстати, несколько имен могут указывать на один и тот же коммит, и это нормально, когда вы создаете новую ветвь.)

Прежде чем мы продолжим, позвольте мне перерисовать график еще одним способом:

0
 \
  1
   \
    2     <-- master
     \
      3 - 4 - 5
              |\
              | 6   <-- A
               \
                7
                 \
                  8
                   \
                    9   <-- HEAD=B       

Это только подчеркивает, что важна не горизонтальная линия коммитов, а отношения родитель / потомок. Метка ветви указывает на начальную фиксацию, и затем (по крайней мере, как рисуются эти графики) мы перемещаемся влево, возможно, также поднимаемся или опускаемся, чтобы найти родительские коммиты.


Когда вы перебазируете коммиты, вы фактически копируете эти коммиты.

Git никогда не может изменить какой-либо коммит.

Есть одно «истинное имя» для любого коммита (или даже любого объекта в репозитории git), которое является его SHA-1: эта строка из 40 шестнадцатеричных цифр, такая как 9f317ce..., который вы видите в git log, например. SHA-1 представляет собой криптографическую 1 контрольную сумму содержимого объекта. Содержимое содержит информацию об авторе и коммиттере (имя и адрес электронной почты), метки времени, исходное дерево и список родительских коммитов. Родителем коммита №7 всегда является коммит №5. Если вы делаете в основном точную копию коммита # 7, но устанавливаете его родительский коммит # 2 вместо коммита # 5, вы получаете другой коммит с другим идентификатором. (У меня заканчиваются однозначные цифры - обычно я использую заглавные буквы для представления идентификаторов фиксации, но с ветвями с именами A и B я подумал, что это сбивает с толку. Поэтому я назову копию # 7, # 7a, ниже.)

Что git rebase делает

Когда вы просите git перебазировать цепочку коммитов - например, коммиты # 7-8-9 выше - он имеет скопировать их , по крайней мере, если они собираются куда-то двигаться (если они не двигаются, они могут просто оставить оригиналы на месте). По умолчанию он копирует коммиты из проверенной в данный момент ветки, поэтому git rebase требуется только две дополнительные части информации:

  • Какие коммиты следует скопировать?
  • Где должен копирует землю? То есть, каков целевой родительский идентификатор для первого скопированного коммита? (Дополнительные коммиты просто указывают на первое скопированное, второе скопированное и т. Д.)

Когда вы запускаете git rebase <upstream>, вы позволяете git вычислять обе части из одного фрагмента информации. , Когда вы используете --onto, вы получаете возможность рассказать git отдельно об обеих частях: вы все еще предоставляете upstream, но он не вычисляет цель из <upstream>, он только вычисляет коммиты для копирования из <upstream>. (Между прочим, я думаю, что <upstream> не является хорошим именем, но это то, что использует rebase, и у меня нет ничего лучше, поэтому давайте придерживаться этого здесь. Rebase вызывает target <newbase>, но Я думаю, что target - гораздо лучшее имя.)

Давайте сначала рассмотрим эти два варианта. Оба предполагают, что вы находитесь на ветке B в первую очередь:

  1. git rebase master
  2. git rebase --onto master A
  3. [11127]

    С первой командой аргументом <upstream> для rebase является master. Со вторым это A.

    Вот как git вычисляет, который выполняет копирование: он передает текущую ветвь в git rev-list , а также передает <upstream> в git rev-list, но с использованием --not - или, точнее, с эквивалентом двухточечной нотации exclude..include. Это означает, что нам нужно знать, как работает git rev-list.

    Хотя git rev-list чрезвычайно сложно - большинство команд git заканчивают тем, что используют его; это движок для git log, git bisect, rebase, filter-branch и т. д. - этот конкретный случай не слишком сложен: в двухточечной нотации rev-list перечислены все коммиты, достижимые справа. со стороны (включая этот коммит), исключая каждый коммит, достижимый с левой стороны.

    В этом случае git rev-list HEAD находит все коммиты, достижимые из HEAD, то есть почти все коммиты: коммиты 0-5 и 7-9, а git rev-list master находит все коммиты, достижимые из master, который Фиксация # 0, 1 и 2. Вычитание 0-через-2 из 0-5,7-9 оставляет 3-5,7-9. Это кандидат обязуется скопировать, как указано в git rev-list master..HEAD.

    Для нашей второй команды у нас есть A..HEAD вместо master..HEAD, поэтому коммиты для вычитания равны 0-6. Коммит № 6 не появляется в наборе HEAD, но это нормально: вычитая что-то, чего нет, оставляет его там. Таким образом, число получаемых кандидатов на копирование равно 7-9.

    Это все еще оставляет нас с определением цели перебазирования, то есть, где должен быть скопирован коммит земли? Для второй команды ответом является «фиксация, определенная аргументом --onto». Поскольку мы сказали --onto master, это означает, что целью является коммит №2.

    rebase # 1

    git rebase master

    Однако с помощью первой команды мы не указали цель напрямую, поэтому git использует фиксацию, идентифицированную <upstream>. <upstream>, которое мы дали, было master, которое указывает на коммит №2, поэтому целью является коммит №2.

    Поэтому первая команда будет начинаться с копирования коммита # 3 с любыми минимальными изменениями, необходимыми для того, чтобы его родитель был коммитом № 2. Его родителем является уже коммит # 2. Ничего не должно меняться, поэтому ничего не меняется, и rebase просто повторно использует существующий коммит # 3. Затем он должен скопировать # 4, чтобы его родитель # 3, но родитель уже # 3, поэтому он просто повторно использует # 4. Точно так же # 5 это уже хорошо. Он полностью игнорирует # 6 (это не входит в набор коммитов для копирования); он проверяет # 7-9, но все они тоже хороши, так что вся перебазировка заканчивается просто повторным использованием всех оригинальных коммитов. В любом случае вы можете принудительно копировать копии с -f, но вы этого не сделали, поэтому весь этот перебаз в конечном итоге ничего не делает.

    rebase # 2

    git rebase --onto master A

    Вторая команда rebase использовала --onto для выбора # 2 в качестве своей цели, но сказала git для копирования только что зафиксировала 7-9. Родителем коммита # 7 является коммит №5, поэтому эта копия действительно должна что-то делать. 2 Таким образом, git делает новый коммит - давайте назовем это # ​​7a - который имеет коммит № 2 в качестве родителя. Перебазировка переходит к фиксации # 8: копии теперь требуется # 7a в качестве ее родителя. Наконец, rebase переходит к коммиту # 9, которому нужен # 8a в качестве родителя. Со всеми скопированными коммитами последнее, что делает rebase, это перемещает метку (помните, метки перемещаются и меняются!). Это дает график, подобный следующему:

              7a - 8a - 9a       <-- HEAD=B
             /
    0 - 1 - 2                    <-- master
             \
              3 - 4 - 5 - 6      <-- A
                        \
                         7 - 8 - 9   [abandoned]
    

    Хорошо, но как насчет git rebase --onto master A B?

    Это почти то же самое, что и git rebase --onto master A. Разница в том, что дополнительные B в конце. К счастью, эта разница очень проста: если вы дадите git rebase один дополнительный аргумент, сначала он запускает git checkout для этого аргумента. 3

    Ваши исходные команды

    В своем первом наборе команд вы выполнили git rebase master, находясь на ветке B ]. Как отмечалось выше, это большой запрет: так как ничего не нужно перемещать, git вообще ничего не копирует (если вы не используете -f / --force, чего вы не делали). Затем вы проверили master и использовали git merge B, который - если это сказано 4 - создает новый коммит с слиянием. Поэтому Ответ Дерика , по крайней мере, на тот момент, когда я его видел, является правильным: коммит слияния имеет двух родителей, один из которых является вершиной ветви B, и эта ветвь проходит через три коммиты, которые находятся на ветке A и, следовательно, некоторые из того, что на A, в конечном итоге объединяются в master.

    Со своей второй последовательностью команд вы сначала извлекли B (вы уже были на B, так что это было избыточно, но было частью git rebase). Затем вам пришлось перебазировать копию трех коммитов, получая последний график выше, с коммитами 7a, 8a и 9a. Затем вы проверили master и сделали коммит слияния с B (снова см. Сноску 4). Опять же, ответ Дерика правильный: единственное, чего не хватает, так это того, что исходные, оставленные коммиты не затянуты, и не так очевидно, что новые объединенные коммиты являются копиями.


    1 Это имеет значение только в том смысле, что чрезвычайно сложно нацелиться на конкретную контрольную сумму. То есть, если кто-то , которому вы доверяете, скажет вам: «Я доверяю коммиту с идентификатором 1234567 ...», для кого-то еще - кого-то, кому вы не так доверяете, почти невозможно придумать коммит, который имеет тот же идентификатор, но имеет другое содержимое. Вероятность того, что это произойдет случайно, составляет 1 к 2 160 , что намного меньше вероятности того, что у вас случится сердечный приступ при ударе молнии при утоплении в цунами при похищении космическими инопланетянами. : -)

    2 Фактическая копия сделана с использованием эквивалента git cherry-pick : git сравнивает дерево коммита с деревом его родителя, чтобы получить diff, затем применяет diff к дереву нового родителя.

    3 Это на самом деле, на самом деле, в настоящий момент буквально верно: git rebase - это сценарий оболочки, который анализирует ваши параметры, а затем решает, какой тип внутренней перебазировки выполнить: неинтерактивный git-rebase--am или интерактивный git-rebase--interactive. После выяснения всех аргументов, если есть один оставшийся аргумент имени ветви, скрипт выполняет git checkout <branch-name> перед началом внутренней ребазировки.

    4 Поскольку master указывает на коммит 2, а коммит 2 является прародителем коммита 9, он обычно не делает коммит слияния в конце концов, а вместо этого делает то, что Git называет перемотка вперед . Вы можете указать Git не выполнять эти перемотки вперед, используя git merge --no-ff. Некоторые интерфейсы, такие как веб-интерфейс GitHub и, возможно, некоторые графические интерфейсы, могут разделять различные виды операций, так что их «объединение» вызывает истинное слияние, подобное этому.

    При слиянии в ускоренном режиме последний граф для первого случая выглядит следующим образом:

    0 <- 1 <- 2         [master used to be here]
               \
                3 <- 4 <- 5 <- 6      <------ A
                           \
                            7 <- 8 <- 9   <-- master, HEAD=B
    

    В любом случае коммиты с 1 по 9 теперь находятся на обеих ветвях , master и B. Разница по сравнению с истинным слиянием заключается в том, что на графике вы можете увидеть историю, которая включает слияние.

    Другими словами, преимущество ускоренного слияния состоит в том, что оно не оставляет следов того, что в противном случае является тривиальной операцией. Недостаток быстрого слияния состоит в том, что он не оставляет следов. Таким образом, вопрос о том, разрешить ли быструю перемотку вперед, действительно является вопросом о том, хотите ли вы оставить явное слияние в истории, сформированной коммитами.

89
ответ дан Al.G. 26 November 2015 в 16:23
поделиться

Перед любой из указанных операций ваш репозиторий выглядит следующим образом

           o---o---o---o---o  master
                \
                 x---x---x---x---x  A
                                  \
                                   o---o---o  B

После стандартной перезагрузки (без --onto master) структура будет:

           o---o---o---o---o  master
               |            \
               |             x'--x'--x'--x'--x'--o'--o'--o'  B
                \
                 x---x---x---x---x  A

... где x' - это коммиты из ветви A. (Обратите внимание, что теперь они дублируются у основания ветви B.)

Вместо этого, ребаз с --onto master создаст следующую более чистую и простую структуру:

           o---o---o---o---o  master
               |            \
               |             o'--o'--o'  B
                \
                 x---x---x---x---x  A
14
ответ дан XML 26 November 2015 в 16:23
поделиться

Различия:

Первый сет

  • (B) git rebase master

    *---*---* [master]
             \
              *---*---*---* [A]
                       \
                        *---*---* [B](HEAD)
    

Ничего не произошло. В ветке master нет новых коммитов с момента создания ветки B.

  • (B) git checkout master

    *---*---* [master](HEAD)
             \
              *---*---*---* [A]
                       \
                        *---*---* [B]
    
  • (мастер) git merge B

    *---*---*-----------------------* [Master](HEAD)
             \                     /
              *---*---*---* [A]   /
                       \         /
                        *---*---* [B]
    

Второй набор

  • (B) git rebase --onto master A B

    *---*---*-- [master]
            |\
            | *---*---*---* [A]
            |
            *---*---* [B](HEAD)
    
  • (B) git checkout master

    *---*---*-- [master](HEAD)
            |\
            | *---*---*---* [A]
            |
            *---*---* [B]
    
  • (мастер) git merge B

    *---*---*----------------------* [master](HEAD)
            |\                    /
            | *---*---*---* [A]  /
            |                   /  
            *---*--------------* [B]
    

Я хочу объединить мои изменения B (и только мои изменения B, никаких изменений A) в master

Будьте осторожны с тем, что вы понимаете под «только мои изменения B».

В первом наборе ветвь B (перед последним слиянием):

 *---*---*
          \
           *---*---*
                    \
                     *---*---* [B]

И во втором наборе ваша ветвь B:

*---*---*
        |
        |
        |
        *---*---* [B]

Если Я правильно понимаю, что вы хотите только коммитов B, которые не находятся в ветке A . Итак, второй набор - это правильный выбор для вас до слияния.

10
ответ дан Dherik 26 November 2015 в 16:23
поделиться

git log --graph --decorate --oneline A B master (или эквивалентный инструмент с графическим интерфейсом) можно использовать после каждой команды git для визуализации изменений.

Это начальное состояние хранилища, с B в качестве текущей ветви.

(B) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> B) C9
| * 2968483 C8
| * 187c9c8 C7
|/  
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0

Вот скрипт для создания хранилища в этом состоянии.

#!/bin/bash

commit () {
    for i in $(seq $1 $2); do
        echo article $i > $i
        git add $i
        git commit -m C$i
    done
}

git init
commit 0 2

git checkout -b A
commit 3 6

git checkout -b B HEAD~
commit 7 9

Первая команда rebase ничего не делает.

(B) git rebase master
Current branch B is up to date.

Проверка master и слияние B просто указывают master на тот же коммит, что и B (т.е. 9a90b7c). Новых коммитов не создано.

(B) git checkout master
Switched to branch 'master'

(master) git merge B
Updating 0aaf90b..9a90b7c
Fast-forward
<... snipped diffstat ...>

(master) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> master, B) C9
| * 2968483 C8
| * 187c9c8 C7
|/  
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0

Вторая команда rebase копирует коммиты в диапазоне A..B и указывает их на master. Три коммитов в этом диапазоне: 9a90b7c C9, 2968483 C8, and 187c9c8 C7. Копии являются новыми коммитами со своими собственными идентификаторами коммитов; 7c0e241, 40b105d и 5b0bda1. Ветви master и A неизменны.

(B) git rebase --onto master A B
First, rewinding head to replay your work on top of it...
Applying: C7
Applying: C8
Applying: C9

(B) log --graph --oneline --decorate A B master
* 7c0e241 (HEAD -> B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/  
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0

Как и прежде, проверка master и слияние B просто указывают master на тот же коммит, что и B (т.е. 7c0e241). Новых коммитов не создано.

Первоначальная цепочка коммитов, на которую указывал B, все еще существует.

git log --graph --oneline --decorate A B master 9a90b7c
* 7c0e241 (HEAD -> master, B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| | * 9a90b7c C9    <- NOTE: This is what B used to be
| | * 2968483 C8
| | * 187c9c8 C7
| |/  
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/  
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0
2
ответ дан sigjuice 26 November 2015 в 16:23
поделиться

Вы можете попробовать это сами и посмотреть. Вы можете создать локальный репозиторий git для игры:

#! /bin/bash
set -e
mkdir repo
cd repo

git init
touch file
git add file
git commit -m 'init'

echo a > file0
git add file0
git commit -m 'added a to file'

git checkout -b A
echo b >> fileA
git add fileA
git commit -m 'b added to file'
echo c >> fileA
git add fileA
git commit -m 'c added to file'

git checkout -b B
echo x >> fileB
git add fileB
git commit -m 'x added to file'
echo y >> fileB
git add fileB
git commit -m 'y added to file'
cd ..

git clone repo rebase
cd rebase
git checkout master
git checkout A
git checkout B
git rebase master
cd ..

git clone repo onto
cd onto
git checkout master
git checkout A
git checkout B
git rebase --onto master A B
cd ..

diff <(cd rebase; git log --graph --all) <(cd onto; git log --graph --all)
1
ответ дан choroba 26 November 2015 в 16:23
поделиться
Другие вопросы по тегам:

Похожие вопросы: