Вопрос относительно git rebase против git слияния в master - PullRequest
0 голосов
/ 20 января 2020

Вот простой рабочий процесс, который я использовал в своей ветви функций. У меня есть только 1 коммит, который я хотел бы аккуратно объединить с мастером (используя ускоренное слияние).

Работа над функцией

git checkout -b feature

работа, работа, работа

git add .

git commit -m "finished feature"

Перебазировать ветку объектов поверх обновленного мастера

git checkout master

git pull

git checkout feature

git rebase master

Объединить ветвь объектов на главном мастере

git checkout master

Теперь я должен сделать git rebase feature ИЛИ git merge feature? Какая будет разница в этом случае? Какая лучшая практика?

git push

Ответы [ 2 ]

2 голосов
/ 20 января 2020

Если вы используете ускоренное слияние, эти две операции эквивалентны. git rebase определяет, когда нет необходимости выполнять перебазировку, и избегает ее, а слияние с ускоренной перемоткой просто обновляет заголовок до нового местоположения. Единственное время, когда вы выбираете, имеет значение, когда операция не ускорена.

В вашем случае, независимо от того, какую операцию вы выполняете, вы захотите проверить master и запустить git merge --ff-only feature. Это выполнит часть операции ускоренной перемотки вперед и завершится неудачей, если не будет ускоренной перемотки вперед.

Выполнение git rebase feature приведет к перебазировке master поверх feature, что не приведет к результаты, которые вы ищете в этом случае.

1 голос
/ 20 января 2020

«Лучшая практика» - вопрос мнений и, следовательно, не в порядке c в StackOverflow. Более важно то, что вы понимаете, что эти различные варианты делают , так что вы можете выбрать, какой из них подходит вас больше всего.

У меня есть только 1 Коммит, который я хотел бы аккуратно объединить с мастером (используя ускоренное слияние).

В этом случае (и с учетом ваших шагов настройки), git rebase, за которым следует git merge, будет работать чаще чем просто git merge. Это потому, что иногда - возможно, в большинстве случаев, в зависимости от того, с кем еще вы могли бы работать с здесь - git rebase ничего не будет делать. В тех случаях, когда git rebase что-то делает, это будет необходимо.

Но ваш пример последовательности команд немного отличается:

git checkout master
git pull
git checkout feature
git rebase master

Что что git pull делает там? Что ж, мы доберемся до этого, потому что git pull означает run git fetch, затем выполните вторую команду Git , и сама эта вторая команда будет git merge или git rebase. Это означает, что вам нужно понять хотя бы одно из git rebase и git merge, в зависимости от того, какую вторую команду вы выберете. Вам также нужно понимать git fetch.

TL; DR

Следующее является неизбежно длинным, потому что Git немного сложно, но есть короткая версия:

Если хотите, вы можете использовать git merge --ff-only и просто позволить ему потерпеть неудачу в тех случаях, когда его нельзя использовать. Это говорит вам: остановитесь, сделайте шаг назад, внимательно посмотрите на то, что у вас есть сейчас, и решите, хотите ли вы перебазировать, или объединить, или выполнить более длительную последовательность операций. См. bk2204 принятый ответ , который пришел, пока я писал все это ...

(я также предлагаю Git новичкам избегать git pull, так что они точно знают, какие команды они выполняют. Как только вы ознакомитесь с этими частями, , а затем вы можете использовать git pull для удобства, если вам это удобно. Когда я был новичком в Git Еще в 2005 или 2006 или около того я обнаружил, что git pull делает неясным, что на самом деле происходит. Еще одна причина, по которой следует избегать git pull, заключается в том, что выполнение второй команды зависит от того, что делает fetch. заранее знать, что git fetch собирается получить! Ну, это, или не очень забота , а не очень забота - это довольно распространенное явление.)

Краткое резюме вещи, которые вы, возможно, уже знаете, но с изображениями

  1. Каждый коммит в Git имеет уникальный идентификатор ha sh. Этот ha sh ID в некотором смысле является «истинным именем» коммита. Никакой другой коммит, даже один в каком-либо другом Git хранилище, никогда не сможет использовать идентификатор ha sh этого коммита для другого коммита. Ни один прошлый или будущий коммит не может повторно использовать этот идентификатор: он предназначен для этого коммита, в зависимости от того, какой коммит "this commit".

  2. Каждый коммит хранит две группы вещей : часть data , которая содержит снимок всех ваших файлов, а не изменения с момента предыдущей фиксации, просто снимок, и часть metadata , которая содержит такие элементы, как ваше имя и адрес электронной почты, даты и времени, сообщения журнала и т. д. Один из элементов метаданных каждого коммита - это список sh идентификаторов предыдущего или родительского коммитов. Большинство коммитов имеют ровно один предыдущий / родительский коммит.

  3. A имя ветви подобно master содержит идентификатор ha sh одного коммита. Этот коммит считается последним коммитом в ветке. Вот и все, что у него есть, только один идентификатор sh. Это обязывает себя , чтобы помнить, кто идет "перед" ними.

Всякий раз, когда что-то удерживает коммит с sh ID, мы говорим, что эта вещь указывает на , которые совершают. Таким образом, мы можем нарисовать все это так:

... <-F <-G <-H   <-- master
               \
                I <-J   <-- feature

name master здесь указывает на коммит H, так что H является последним коммит в этой ветке. Имя feature указывает на фиксацию J, поэтому J является последним коммитом в этой ветви. Commit I находится в feature, потому что J указывает на I. Коммит H также находится в ветви функций, поскольку I указывает на H.

Другими словами, коммиты могут быть на более чем на одной ветви в время. Если мы создадим вторую ветвь из master и добавим туда несколько коммитов, мы получим:

             I--J   <-- feature
            /
...--F--G--H   <-- master
            \
             K   <-- feature2

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

В этом случае коммиты до H включены все три ветви, в то время как набор I-J только на feature и K только на feature2.

Чтобы сделать новый коммит, мы git checkout в зависимости от того, какая ветвь name мы хотим, чтобы новый коммит был "включен". Это выбирает фиксацию и присоединяет имя HEAD к имени ветви. Итак, если мы запустим git checkout master, мы получим:

             I--J   <-- feature
            /
...--F--G--H   <-- master (HEAD)
            \
             K   <-- feature2

Теперь у нас есть коммит H. Если мы запустим git checkout feature, мы получим:

             I--J   <-- feature (HEAD)
            /
...--F--G--H   <-- master
            \
             K   <-- feature2

, и теперь у нас есть коммит J out. Давайте git checkout feature2 выберем коммит K:

             I--J   <-- feature
            /
...--F--G--H   <-- master
            \
             K   <-- feature2 (HEAD)

Теперь давайте сделаем новый коммит L обычным способом: отредактируйте некоторые файлы git add и запустите git commit. Git сделает новый коммит из того, что находится в index или staging area (два разных термина для одного и того же). Снимок остановит все эти файлы навсегда (или пока существует коммит L). Git соберет наше сообщение журнала, добавит наше имя как автора и коммиттера, установит родителя нового коммита L для существующего коммита K и, наконец, запишет новый идентификатор L ha sh ID - что угодно is - в имя feature2, так что мы имеем:

             I--J   <-- feature
            /
...--F--G--H   <-- master
            \
             K--L   <-- feature2 (HEAD)

Текущий коммит теперь является нашим новым коммитом L, и имена ветвей указывают на коммиты, как мы их нарисовали. (Конечно, их настоящие идентификаторы ha sh представляют собой большие уродливые строки, которые выглядят случайными, которые мы никогда не вспомним, и которые нам придется вырезать и вставить, чтобы получить правильные значения. Отсюда и использование простых букв здесь.)

Слияние - это объединение работы (кроме случаев, когда это ускоренная перемотка вперед)

Давайте нарисуем вещи так:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

Вы можете видеть из этой диаграммы у нас просто есть два интересных имени ветви, и мы находимся на branch1, что означает commit J. Если мы теперь запустим git merge branch2, Git будет заставлять выполнить полное слияние.

Полный процесс слияния начинается с нахождения base merge base commit, что в данном случае H. Затем он сравнивает снимок в H со снимком в J, где мы сейчас находимся, чтобы увидеть, что мы изменили. Это ours изменения; J - это коммит ours. Затем git сравнивает снимок в H со снимком в L, чтобы увидеть, что они изменились. Это theirs изменения с L как коммит theirs. Процесс слияния объединит изменения, применит объединенные изменения к базе слияния H и, если все пойдет хорошо, сделает новый коммит слияния M:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

Новый коммит M будет иметь в качестве своего снимка все, что было в H, измененное путем добавления как наших изменений (H -vs- J), так и их изменений (H -vs- L) ). Поэтому мы сохраняем наши изменения и добавляем их: в конце концов, это и есть слияние. Чтобы вспомнить, что слилось, новый коммит M будет иметь двух родителей вместо одного. Первым родителем будет коммит, который мы проверили в момент go, который был коммитом J. Вторым родителем будет другой коммит, который мы слили: коммит L.

Слияния с ускоренной перемоткой не могут объединить

Но что, если входное изображение не выглядит нравится? Предположим, вместо этого у нас есть более простая картина:

...--G--H   <-- master (HEAD)
         \
          I--J  <-- feature

Сейчас мы находимся на H через имя master, к которому прикреплен HEAD. Мы бежим git merge feature или git merge <em>hash-of-J. Git находит лучший общий коммит - лучший коммит, который находится на обеих ветвях - но это коммит H, на котором мы находимся! Если бы Git сделал полное слияние, он сравнил бы H против H, чтобы увидеть, что мы изменили. Это, конечно, ничего не будет. Затем он сравнил бы H с J, чтобы увидеть, какие "они" (действительно, мы, на feature) изменились. Затем он добавил бы эти изменения в H. Результат всегда будет точно соответствовать коммиту J.

Если бы Git сделал полное слияние, мы получили бы:

...--G--H------K   <-- master (HEAD)
         \    /
          I--J  <-- feature

где снимок в K соответствует этому в J. Разница между K и J заключается в том, что K - это другой коммит, с (1) другим идентификатором ha sh и (2) разными родителями: H и J. Что ж, вероятно (3) K также будет иметь разные метки даты и времени.

Мы можем, если захотим, заставить Git сделать это, но Git По умолчанию не делает это. Вместо создания нового коммита K и записи этого sh ID в master, Git можно просто проверить существующий коммит J и поместить этого га sh ID в master:

...--G--H
         \
          I--J   <-- master (HEAD), feature

Вы получаете тот же снимок , но нет дополнительной фиксации. Два имени, master и feature, теперь идентифицируют одинаковый коммит , J, и нет никакой причины рисовать график с изломом в нем:

...--G--H--I--J   <-- master (HEAD), feature

и мы можем безопасно удалить имя feature, если захотим.

Мы можем Git: выполнить слияние, но только если вы можете сделать это как ускоренная перемотка вперед . Для этого мы используем:

git merge --ff-only <branch-or-commit-hash>

Это проверяет, может ли объединение быть просто операцией быстрой перемотки вперед. Если это так, Git на самом деле не сливается, а просто выполняет ускоренную перемотку вперед. Если нет, Git не выполнит полное слияние . (Если бы мы пропустили --ff-only, Git попытался бы выполнить полное слияние.)

Rebase действительно означает копирование commitits

Предположим, у нас есть следующий график :

          I---J   <-- feature
         /
...--G--H--K--L   <-- master

(Мы еще не выбрали ветку для git checkout, поэтому нет обозначения (HEAD).) Если мы хотим объединить их прямо сейчас, как есть, Git будет вынужден совершить настоящее слияние, независимо от того, git checkout master; git merge feature или git checkout feature; git merge master. В любом случае, Git нужно найти базу слияния H, сделать два diff с, объединить изменения и сделать коммит слияния.

Если мы не хотим коммит слияния, однако, мы можем rebase commit I, скопировав его в новый коммит I'. Мы запустим:

git checkout feature
git rebase master

Git выведет список идентификаторов коммитов га sh, которые нужно скопировать - что для нас очевидно; это I и J - и затем начнется перебазировка путем отсоединения HEAD от feature, чтобы он указывал непосредственно на L:

          I--J   <-- feature
         /
...--G--H--K--L   <-- HEAD, master

Это режим Git detached HEAD , в котором ребаз используется довольно интенсивно. Теперь Git необходимо скопировать коммит I в новый коммит. Он должен сравнить I со своим родителем H, чтобы увидеть, что изменилось. Затем он должен применить эти изменения к коммиту L и сделать новый коммит. Мы могли бы назвать коммит M, но так как это копия I, мы будем называть его I'. Имя HEAD будет автоматически обновляться, чтобы указывать на новый коммит:

          I--J   <-- feature
         /
...--G--H--K--L   <-- master
               \
                I'  <-- HEAD

Снимок в I' является результатом объединения H -vs- I с H -vs- L. То есть эта операция, которую Git вызывает cherry-pick , фактически использует тот же процесс слияния, что и git merge! Но последний коммит, I', является обычным коммитом без слияния, с одним родителем.

В любом случае, скопировав I в I', Git теперь должен скопировать J с новым коммитом J', таким же образом: Git будет сравнивать I против J, чтобы увидеть, какие "они" (мы) изменились, и сравнить I против I', чтобы увидеть, что мы изменили здесь, и объединить эти изменения. Это приводит к добавлению I -vs- J к нашей копии I', чтобы мы получили:

          I--J   <-- feature
         /
...--G--H--K--L   <-- master
               \
                I'-J'  <-- HEAD

Не беспокойтесь, если это кажется сложным. Это это сложно! Though Конечный результат довольно ясен: у нас есть новые коммиты I' и J', которые "так же хороши, как" оригиналы, за исключением того, что они лучше , потому что родительский элемент I' это L. Таким образом, новая цепочка из двух коммитов похожа на старую цепочку, за исключением того, что:

  • она начинается с другого снимка (L) и, следовательно, заканчивается другим снимком (J'), и
  • это приходит сразу после L.

Теперь, когда мы закончили копировать коммиты с помощью cherry-picking, rebase делает свой последний шаг, который переместить имя feature, чтобы указать на последний скопированный коммит, и повторно присоединить нашу ГОЛОВУ:

          I--J   [abandoned]
         /
...--G--H--K--L   <-- master
               \
                I'-J'  <-- feature (HEAD)

Исходные коммиты I-J все еще в репозиторий , но мы не можем найти их больше, потому что мы всегда начинаем с просмотра имен - feature или master - и работаем в обратном направлении. (В конце концов - примерно через 30 дней в обычной обстановке - если никто не может найти I и J, и вы не умышленно воскресили их, чтобы отменить вашу ребазу, Git сметет их по-настоящему, и те моментальные снимки исчезнут.)

Выполнение такого рода перебазирования позволяет ускорить

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

...--G--H--K--L   <-- master
               \
                I'-J'  <-- feature (HEAD)

, мы можем использовать git checkout master, затем git merge --ff-only feature и получить:

...--G--H--K--L--I'-J'  <-- master (HEAD), feature

, как и раньше.

Иногда rebase ненужный

Если мы начнем с:

...--G--H  <-- master
         \
          I   <-- feature (HEAD)

и запустим git rebase master, Git:

  1. Выводит список коммитов, которые включены feature но не master: I.
  2. Проверяет master как отдельную ГОЛОВУ.
  3. Копирует коммит I, чтобы прийти туда, где коммит I: это не так требуется любое копирование, и Git просто говорит самому себе, ах, давайте снова используем I вместо .
  4. Закончено копирование, поэтому имя feature перемещается, чтобы указать здесь , на который он уже указывает, и снова присоединяет HEAD.

В результате получается размытое движение - перечисление и отсоединение, и в действительности ничего не делается, а затем снова присоединяется, что не приводит к изменениям вообще. Теперь мы готовы к git merge --ff-only.

А как насчет git fetch?

Ваша последовательность git pull представила дополнительную пару команд Git. Сначала запускается git fetch, затем вторая команда, либо git rebase (если вы выберете это), либо git merge (по умолчанию). Выше мы видели, что может сделать git merge: реальное слияние или ускоренная перемотка вперед. Но как насчет шага git fetch? 1470 *

Что действительно означает git fetch, так это совместное использование коммитов с некоторым другим Git репозиторием. Это означает, что нам нужно иметь другое хранилище Git и поместить это в наши картинки. Этот другой Git репозиторий может быть на GitHub или Bitbucket или GitLab или на одной из этих различных служб хостинга, или это может быть рабочий компьютер или что-то еще. Но это Git репозиторий, и это означает, что он имеет коммиты, и у него есть его собственные ветви.

Наши Git вызовут их Git и получат их список их имена ветвей и зафиксировать га sh идентификаторы. Когда они перечисляют «интересное» имя ветки и га sh ID, наша Git получит эту информацию. Тогда наш Git увидит, есть ли у нас этот коммит, по этому sh ID. Помните, что идентификаторы ha sh уникальны для каждого коммита, но у них есть еще одно ключевое свойство: каждые Git везде использует одинаковые га sh ID для , который фиксирует. Так что либо у нас есть sh ID в нашем Git, поэтому мы имеем коммит; или у нас нет идентификатора ha sh, поэтому у нас нет коммита.

Если у них есть какие-то коммиты, которых у нас нет, мы можем нарисовать это так:

our Git repository:
...--G--H   <-- master (HEAD)

their Git repository:
...--G--H--I--J   <-- master

Наши Git увидят, что у нас нет J. Наши Git затем спросят у Git о J, и они также предложат родителю J I (по номеру sh). Наши Git увидят, что он нам тоже нужен, и попросят его (по номеру sh), а их Git предложит H. Наши Git увидят, что нам не нужен H и скажут «нет, спасибо», у нас есть такой.

Теперь они упакуют то, что нужно нашим Git для добавления I и J для нашей коллекции, Borg - мода. Они отправят это снова, и наш Git добавит его в наш репозиторий:

...--G--H   <-- master (HEAD)
         \
          I--J   <-- ???

Но теперь нам нужно имя , потому что наш Git покажет только us фиксирует, когда может найти их по имени. Имя, которое будет использовать Git, - это имя для удаленного слежения: мы возьмем их имя master и поставим перед ним префикс, например origin/. 1 Таким образом, после того, как git fetch завершится и выйдет, реальная картинка, которую мы должны нарисовать, теперь выглядит так:

...--G--H   <-- master (HEAD)
         \
          I--J   <-- origin/master

Команда pull теперь будет запускать Git или git merge или git rebase. 2 По умолчанию используется git merge. Git выполнит коммит слияния J, и до тех пор, пока слияние происходит в ускоренном режиме - как и в этом случае - мы получим:

...--G--H--I--J   <-- master (HEAD), origin/master

в качестве нашего результата.

Если не поступают коммиты, так что origin/master и master (HEAD) выбирают тот же коммит H до и после git fetch, git pull не будет делать ничего лишнего. Таким образом, шаг pull (или fetch-and-then-command-command) необходим только в том случае, если у другого Git есть новые коммиты, которые мы хотим включить.


1 Часть origin/ происходит от названия remote , который вы используете для разговора с другим Git. Технически эти ссылки находятся в другом пространстве имен , под refs/remotes/, чем refs/heads/. Git обычно скрывает это от нас, иногда немного больше, иногда немного меньше: git branch иногда показывает имя origin/master, а иногда - remotes/origin/master. Я не знаю, почему это не согласовано здесь.

2 Команда pull выполняет это слияние или перебазирование в текущей ветви и только текущая ветка, независимо от любых других имен git fetch может быть обновлено. Он напрямую использует необработанные идентификаторы ha sh и устанавливает конкретное сообщение слияния при использовании git merge.


Выводы

  1. Общая идея перебазирования это: У меня есть некоторые коммиты, они в порядке, но они были бы улучшены, если бы я их переместил. Вы не можете на самом деле переместить коммит - коммит, один раз made, только для чтения на 100%, но вы можете скопировать их в новые и улучшенные коммиты с новыми и разными идентификаторами ha sh.

  2. A fast-forward операция действительно означает перемещение имени ветви, чтобы указать на какой-то уже существующий коммит, который находится дальше по цепочке коммитов. Рисование графика позволит вам увидеть, возможно ли это на самом деле. Когда git merge выполняет ускоренную перемотку вперед, он также проверяет коммит, в который он переместил имя ветви.

  3. A git merge выполняет ускоренную перемотку, если может, и настоящее слияние, если не может. Добавление --ff-only говорит: Если вы не можете сделать это как ускоренную перемотку вперед, просто скажите мне об этом и выйдите.

  4. Используя git fetch, Вы можете получить чьи-либо коммиты из другого Git репозитория в ваш репозиторий. Этот шаг всегда безопасен и может быть запущен в любое время в любой ветке. Но, получив эти коммиты, вам нужно будет использовать команду second Git для фактического включения их коммитов в имена веток.

  5. использовать rebase или слияние для включения извлеченных коммитов - вопрос мнений (а также использовать ли rebase вообще когда-либо). Но что бы вы ни делали для включения извлеченных коммитов, эта часть происходит в вашей текущей ветви , потому что и git rebase, и git merge используют текущую ветвь.

  6. Команда git pull означает: Выполнить git fetch, затем выполнить вторую команду, чтобы повлиять на текущую ветвь, чтобы включить то, что мы получили. В некоторых случаях нет второй команды для запуска, потому что вы ничего не подобрали на этапе извлечения; и в редком случае - новый репозиторий, в котором ранее не было коммитов, следовательно, нет текущей ветки; или сразу после git checkout --orphan - перебирать или объединять тоже нечего. (Вероятно, вы не попадете в этот редкий случай, но в дурные старые времена 2005 года или около того, git pull может разрушить ваше рабочее дерево, если оно у вас есть. К счастью, оно давно исправлено.)

Нет единого правильного рабочего процесса, но тот, который вы используете, подходит.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...