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

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

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

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

>(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

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

Ответы

Ответ 1

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

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

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

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

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

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

В частности, имя ветки типа master, A или B указывает на одно конкретное коммит. В этом случае master указывает на фиксацию 2, A указывает на фиксацию 6, а B указывает на фиксацию 9. Первые несколько коммитов с 0 по 2 содержатся во всех трех ветвях; commits 3, 4 и 5 содержатся как внутри A, так и B; commit 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 является криптографической контрольной суммой содержимого объекта. Содержимое - автор и коммиттер (имя и адрес электронной почты), метки времени, исходное дерево и список родительских коммитов. Родитель commit # 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 calls target <newbase>, но я думаю, что цель - лучшее имя.)

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

  • git rebase master
  • git rebase --onto master A

При первой команде аргумент <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, что commit #s 0, 1 и 2. Вычитание 0-через-2 из 0-5,7-9 оставляет 3-5,7-9. Это кандидат обязуется копировать, как указано в git rev-list master..HEAD.

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

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

rebase # 1

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

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

rebase # 2

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

          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. Как отмечалось выше, это большой нет-op: поскольку ничего не нужно перемещать, git ничего не копирует (если вы не используете -f/--force, которого вы не сделали). Затем вы проверили master и использовали git merge B, который, если ему сообщается 4 создает новый коммит с объединением. Поэтому ответ Dherik, с того момента, как я его видел, по крайней мере, здесь верен: объединение слияния имеет двух родителей, одним из которых является кончик ветки B, и эта ветвь возвращается через три коммиты, которые находятся на ветке A, и поэтому некоторые из того, что на A завершается, сливаются в master.

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


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

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

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

4 Так как master указывает на фиксацию 2 и commit 2 является предком commit 9, это, как правило, не будет совершать коммит в конце, но вместо этого сделает то, что git вызывает быстрый -forward работа. Вы можете указать 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. Разница, по сравнению с истинным слиянием, состоит в том, что из графика вы можете увидеть историю, включающую слияние.

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

Ответ 2

Различия:

Первый набор

(B) git мастер перезаписи

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

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

(B) git мастер проверки

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

(мастер) git merge B

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

Второй набор

(B) git rebase --onto master A B

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

(B) git мастер проверки

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

(мастер) git merge B

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

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

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

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

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

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

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

Если я правильно понимаю, то, что вы хотите, только , бит B не соответствует ветки A. Таким образом, второй набор является правильным выбором для вас перед слиянием.

Ответ 3

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

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

После стандартного rebase (без --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.)

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

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

Ответ 4

git log --graph --decorate --oneline A B master (или эквивалентный инструмент GUI) можно использовать после каждой команды 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

Вот script, чтобы создать репозиторий в этом состоянии.

#!/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

Ответ 5

Вы можете попробовать сами и посмотреть. Вы можете создать локальный репозиторий 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)