Существуют ли принципиальные различия между хранением, извлечением и извлечением по сравнению с фиксацией и извлечением --rebase?

Моя цель рабочего процесса часто состоит в том, чтобы избежать бессмысленного слияния. Для этого я часто делаю одну из двух вещей:

  1. Я фиксирую свои изменения и git pull --rebase, чтобы предотвратить ненужное слияние. Если позже я захочу изменить существующую фиксацию, я вношу изменения, фиксирую и объединяю их с помощью git rebase --interactive
  2. Я просто git stash свои изменения, а затем git pull обычно и просто git stash pop эти изменения позже, когда я хочу изменить и/или зафиксировать их.

Некоторые коллеги предупреждали меня, что git stash save/git stash pop небезопасны. Мне интересно, есть ли какие-то тонкие преимущества использования коммитов и git pull --rebase по сравнению со списком тайников.


person AturSams    schedule 23.02.2014    source источник


Ответы (1)


TL;DR версия

Использование git stash позволяет избежать «бессмысленного слияния», только если у вас нет собственных коммитов. Метод git pull --rebase является более общим.

Длинная обучающая версия: что происходит, почему и т. д.

Использование git stash просто создает некоторые коммиты,1, так что в первом приближении фиксация и фиксация в основном одинаковы. Коммиты тайника принимают форму того, что я люблю называть «мешком для тайника», который подвешивается к самой вершине коммита любой ветки, на которой вы находитесь, когда выполняете stash.

Первое отличие состоит в том, что коммиты в заначке не перемещают верхушку ветки. То есть предположим, что ветка (назовем ее devel) выглядит так:

... <- E <- F     <-- devel

то после скрытия это выглядит так:

... <- E <- F     <-- devel
            |\
            i-w   <-- "the stash"

(Это изображение [части] «графа коммитов» в ASCII-арте: коммит F является вершиной ветки. Коммит F имеет в качестве родительского коммита коммит E: F «указывает обратно на» E. Между тем E указывает обратно на своего родителя и т. д. Имя devel просто указывает на самую окончательную фиксацию, то есть F.)

Если бы вы сделали обычную фиксацию, вы бы получили новую фиксацию G, указывающую назад на F, а devel будет изменено, чтобы указывать на G, "перемещая кончик вперед".

Второе отличие с git stash (по сравнению с git commit) происходит как бы на «другом конце». Когда вы запускаете git stash apply,2, git делает примерно следующее. (Есть много деталей реализации, которые затрудняют точное описание, но я думаю, что это способ думать об этом. Если вы apply --keep-index, это сложнее; имейте в виду, что эта упрощенная картина только "достаточно близка" для не---keep-index кейс.)

  1. Сравните заначку с фиксацией, на которой она висит.
  2. Затем примените этот diff в качестве исправления везде, где вы сейчас находитесь, используя механизм слияния git, чтобы сделать работу лучше, чем простое прямое исправление. (То есть git может сказать, были ли уже сделаны части патча, и если да, то пропустить их.)

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


Команду pull лучше всего описать как fetch-затем-merge. (Это даже указано на страницах руководства для него.) С --rebase это лучше всего описать как fetch-затем-rebase. Таким образом, у нас есть два очень разных случая с двумя разными способами вызова pull.

Шаг fetch достаточно легко описать. Вы fetch с какого-то «удаленного», который говорит git вызывать удаленный репозиторий через интернет-телефон :-) 3 и спрашивать его, какие ветки и коммиты и т. д. это имеет. Затем ваш git передает своему git все новые возможности, которые ваш git хранит в вашем репозитории, так что у вас есть все, что они делают, плюс, конечно, что-то свое. .4

Опять же, предположим, что вы находитесь на ветке devel и выполняете такой шаг git fetch. Предположим далее, что когда вы это сделаете, ветка devel будет выглядеть так:

... E - F - G   <-- devel

Когда ваш git связывается с их git, он может найти новые коммиты, которых у вас раньше не было, например коммит H, который указывает на какой-то более ранний коммит в качестве своего родителя.

Возможно, родительский коммит H — это коммит G. Однако, чтобы это произошло, ваш коллега уже должен иметь коммит G. Таким образом, предположив, что вы сделали G, вам нужно его опубликовать (протолкнуть), чтобы она его получила, и сделала H на основе G.

Но, по-видимому, вы еще не нажали G, а ее коммит H указывает на F. Это более общий случай, так что давайте нарисуем его:

... - E - F - G   <-- devel
            \
              H   <-- (her/their idea of what "devel" looks like)

Поскольку коммит G является приватным для вашего репозитория, она, как и все остальные, думает, что цепочка идет E - F - H. Теперь вы должны сделать что-то, чтобы объединить «вашу фиксацию» вместе с «ее фиксацией».

Наиболее точное описание проделанной работы состоит в том, чтобы сделать новый коммит слияния M:

... - E - F - G - M   <-- devel
            \   /
              H

Это то, что будет делать git merge, то же самое произойдет и с простым git pull.

Раздражает то, что полная точность с точки зрения истории заключается в том, что вы получаете эти «бессмысленные слияния».5 Вместо этого вы можете скопировать свой старый коммит G в новый, слегка -другая фиксация, G'. Между G и G' будет два изменения: (1) в рабочем дереве, связанном с G', вы сначала включите ее изменения из H; и (2) в G' вы скажете, что родительская фиксация — это H, а не F. Это будет выглядеть так: давайте переместим ваш старый G вверх, чтобы линия стала E - F - H:

              G       [no longer needed, hence abandoned]
            /
... - E - F - H - G'  <-- devel

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

Это работает, даже если вы сделали целую кучу коммитов, от G1 до G5 или что-то еще, просто требуется больше копирования.

Когда вы используете git pull --rebase, git делает это за вас. Сначала он использует fetch для переноса любых новых коммитов, а затем, если есть новые коммиты, перебазирует ваши предыдущие коммиты на новые.6


Итак, теперь мы можем вернуться к git stash. Если вы не делали никаких новых собственных коммитов на devel, но у вас есть незавершенная работа, и вы используете git stash для ее сохранения, вы получите следующее:

... - E - F       <-- devel
          |\
          i-w

Теперь вы используете git pull без --rebase, и это приводит к коммиту H ("ее" — мы полностью опускаем букву G, оставляя ее пока) и выполняет слияние. Git делает это как «ускоренное слияние», поскольку у вас нет собственных коммитов, и вы получаете это:

... - E - F - H   <-- devel
          |\
          i-w

Затем вы выполняете git stash apply, что заставляет git просматривать изменения между коммитами F и w и объединять их с вашим рабочим каталогом. То есть он применяет ваши изменения к рабочему каталогу для коммита H. Как только вы также drop тайник (или если мы просто не будем его рисовать), git add ваши изменения и git commit, вы получите новый коммит. Почему-то :-) назовем его G' вместо G. Итак, теперь у вас есть:

... - E - F - H - G'  <-- devel

который выглядит точно так же, как если бы вы сначала зафиксировали, а затем запустили git pull --rebase. На самом деле, "заброшенный" коммит G в предыдущем случае на самом деле тот же коммит, что и (отброшенный, т.е. брошенный) коммит из заначки!7


Но что, если вы уже делали коммит (или несколько, но мы будем использовать только один) коммит, G, перед тем, как git stash внести еще какие-то изменения? Тогда у вас есть это:

... - E - F - G     <-- devel
              |\
              i-w   <-- stash

Теперь вы git pull (без --rebase) берете ее коммит H и объединяете его:

              H
            /   \
... - E - F - G - M    <-- devel
              |\
              i-w   <-- stash

Наконец, вы apply заначиваете, убеждаетесь, что все в порядке, drop его, git add и делаете новый коммит N:

              H
            /   \
... - E - F - G - M - N   <-- devel

и у вас есть одно из тех раздражающих "бессмысленных слияний". Он появился, когда вы сделали git pull без --rebase.


Таким образом, короткая версия заключается в том, что git stash спасает ваш бекон (избегает раздражающего слияния), только если у вас нет собственных коммитов. Метод git pull --rebase является более общим. (Хотя, несмотря на проблематичный случай «перебазирования вверх по течению», я предпочитаю делать отдельный шаг git fetch. Затем я просматриваю то, что пришло, и выбираю, выполнять ли перебазирование или слияние. Но это зависит от вас.)


1В частности, он делает не менее двух коммитов. Сначала он делает единицу для текущего индекса, т. е. то, что вы получили бы, если бы сделали git commit без каких-либо git add, git rm и т. д., и заставили коммит существовать (а-ля --allow-empty), даже если дерево не изменилось. Затем он делает многородительскую фиксацию, т. е. фиксацию слияния, с текущим рабочим каталогом в качестве его содержимого. Все эти коммиты выполняются таким образом, чтобы не перемещать кончик ветки. Дополнительные сведения см. в этом ответе.

2Я рекомендую использовать git stash apply, проверить результат, а затем использовать git stash drop, если вас устраивает эффект apply. Команда pop означает просто apply-затем-drop, т. е. предполагает, что вы удовлетворены. Но если вы часто используете git stash, у вас может быть несколько тайников, и вы можете случайно применить не тот, или слишком много из них, или что-то в этом роде. Если у вас есть привычка "сначала apply все настроить, а только потом drop", думаю, вы будете делать меньше ошибок. Конечно, люди разные. :-)

3Если "удаленный" не является действительно локальным, например, file://whatever, или локальным путем; или, возможно, в будущем могут появиться некоторые URL-адреса, не относящиеся к Интернету. Git на самом деле не заботит, как он получает новые данные с удаленного компьютера, он может узнать, что есть на удаленном компьютере, и перенести это так, чтобы теперь он был локальным.

4Когда вы используете git pull, он вызывает fetch с включенными некоторыми специальными ограничениями, так что вы получаете только то, что собираетесь объединить (или перебазировать). В версиях git до 1.8.4 это также запрещает обновление вашей локальной записи «удаленной ветки», т. е. fetch не может сохранить немного полезной информации. Как сказано в примечаниях к выпуску git 1.8.4:

это было раннее дизайнерское решение, чтобы сделать обновление веток удаленного отслеживания предсказуемым, но на практике оказывается, что людям удобнее обновлять их по мере возможности, когда у нас есть шанс, и мы обновляем их, когда запускаем «git push ", что в любом случае уже нарушает первоначальную "предсказуемость".

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

6Стоит отметить, что git pull --rebase очень умен в случае "перезаписи истории вверх по течению". Предположим, перед тем, как тянуть, у вас есть это:

...-o-x-x-Y   <-- branch
         `------- origin/branch

где o и x представляют "их" коммиты, а Y - это "ваши" коммиты (они могут быть Y1-Y2-Y3 и т. д., в конце концов это работает одинаково). Предположим, что при выполнении шага git fetch оказывается, что "они" сами перебазировали branch, так что вместо o-x-x как "на" origin/branch получается o-*-*-*:

...-o-x-x-Y   <-- branch
     \   `------- old origin/branch
      *-*-*   <-- FETCH_HEAD, to become new origin/branch

Очевидно (ну, на этом рисунке должно быть казалось очевидным...), какие коммиты были перебазированы вверх по течению: они написаны как * вместо x. Так что также очевидно (хе-хе), что git может перебазировать цепочку Y на фиксацию подсказки *, на которую указывает FETCH_HEAD:

...-o-x-x-Y     [abandoned]
     \
      *-*-*-Y'  <-- tip
           `------- new origin/tip

Если вы используете «обычную» выборку, а не ту, что в git pull --rebase, это обновит удаленную ветвь, origin/tip, которая скрывает «точку разветвления», которую так легко определить здесь, по крайней мере, до тех пор, пока origin/tip не будет перемещена, чтобы указать на новый наконечник. . К счастью, в рефлогах git достаточно информации для его восстановления, а в git 1.9/2.0 теперь, когда git fetch всегда обновляет удаленные ветки, есть способ попросить git найти точку ветвления позже, чтобы вы могли восстановиться из восходящего потока. перебазируется легче.

7Точнее, у него то же дерево, что и у коммита w в заначке.

person torek    schedule 23.02.2014