Почему перебазирование в ветку, которая перебазировала, приводит к конфликтам слияния и, кажется, стирает мои текущие изменения? - PullRequest
0 голосов
/ 18 мая 2019

Я работаю с некоторыми другими в git-репозитории, где было сделано большое изменение в branch a. Ветвь была в основном закончена и ждала одобрения, поэтому, когда я начал внедрять свою новую функцию, я выбрал ее вместо master. Я работаю на branch b. Были некоторые незначительные изменения в branch a, которые я переместил в branch b со слиянием (хотя в ретроспективе мне следовало бы перебазировать).

После этого изменение branch c было объединено в master. branch b был перебазирован в master.

Я хочу перейти на branch a, чтобы получить эти изменения и в моей ветке, но когда я это делаю, я получаю несколько запутанных конфликтов в файлах, которых я не трогал. Где я изменил файлы, то, что я вижу в конфликте, не отражает мои изменения. Вместо этого мне кажется, что я разрешаю branch a и master, только branch a помечен как HEAD, а master как branch a. Я в замешательстве.

Как это случилось, и что мне лучше всего сделать?

Если я объединяюсь снова, а не перебазирую, я вижу свои собственные изменения - хотя в файлах все еще есть конфликты, которые я не изменил, и конфликты охватывают многие строки, которые не были затронуты branch a с тех пор, как я разветвился Это. Я могу решить это хорошо, но я надеялся не повторить мою предыдущую ошибку.

Я ожидал, что мне нужно будет просто разрешить 3-4 строки, которые изменились на branch c в файлах, которые я изменил.

1 Ответ

0 голосов
/ 18 мая 2019

По сути, эти проблемы возникают потому, что - ну, в основном , потому что - ребаз работает копирует фиксирует. Вопрос в том, какие коммиты копируются - , что , , где , , когда , и , почему : четыре из пять Ws . Кому также может быть интересно, и для шестого фактора, как ... хорошо, мы увидим это через мгновение.

Исправление беспорядка, скорее всего, просто копирование , только правильные коммиты сейчас, что вы могли бы сделать с git rebase, или что может быть проще с git cherry-pick.

Фон

Чтобы правильно понять это, вам необходимо четко понимать, что такое коммит, как коммиты обнаруживаются и как коммиты копируются. Для правильного подробного объяснения того, как коммиты будут найдены, см. Думайте как (а) Git , но мы немного коснемся этого здесь.

Что означает * состоит из двух частей: данные - снимок вашего источника - и метаданные, такие как ваше имя, адрес электронной почты и другая информация о фиксация:

  • Снимок - это всего лишь полная копия каждого исходного файла, однако она остается на момент выполнения коммита. Снимок на самом деле сделан из index , а не из источника, который вы можете видеть, и поэтому вам постоянно приходится git add файлов снова и снова: git add somefile действительно означает copy файл somefile из версии, которую я вижу и с которой работаю, заменив любую копию, которая сейчас может быть в индексе, на эту улучшенную. Запуск git commit снимает копию индекса.

  • Метаданные включают в себя другие данные, которые git log может отображать: ваше имя и адрес электронной почты (из ваших настроенных user.name и user.email); дата и время, когда вы сделали коммит; и самое важное для людей, но неважно для Git, причина , которую вы совершили: сообщение журнала. Но он также включает в себя еще один элемент, а иногда и несколько элементов, которые крайне важны для Git: идентификатор хеша каждого из родительских коммитов этого нового коммита.

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

Поскольку хеш-идентификаторы являются криптографическими контрольными суммами, ни вы, ни кто-либо еще, даже Git, не можете изменить что-либо в коммите после его фиксации. Если вы берете существующий коммит, извлекаете его, возитесь с ним вообще и делаете новый коммит, то либо вы сохранили каждый бит исходного коммита и, следовательно, не изменили его, и он все еще является просто оригинальным коммитом, или вы получаете новый и другой коммит с совершенно другим хеш-идентификатором. Это означает, что в некотором смысле идентификатор хеша равен коммиту.

Между тем каждый коммит хранит хэш-идентификатор своего родителя (ей). С любой простой линейной цепочкой коммитов это означает, что мы можем начать с last commit и использовать его для поиска каждого более раннего коммита. Представляя коммиты в виде одной заглавной буквы вместо реальных хеш-идентификаторов, мы можем нарисовать это как:

... <-F <-G <-H

где H - это хэш-идентификатор последнего коммита в некоторой ветви. Более того, это работает и для нескольких веток: нам просто нужно где-то сохранить хэш-идентификатор каждого окончательного принятия:

...--F--G--H   <-- somehow remember H
         \
          I--J   <-- somehow remember J

Это где ГиВводятся имена ветвей t. Что делает имя ветки, так это запоминает хэш-идентификатор одного коммита. Если master запоминает H и dev запоминает J, то у нас есть несколько общихкоммиты на обеих ветвях - все через коммит G совместно используются - и один коммит приватный master и два приватных dev.

Копирование одного коммита с git cherry-pick

Если коммит - это моментальный снимок - и это так - как git log или git show может показывать как diff?Как вы можете скопировать один коммит в новый, немного другой коммит?Ответ лежит в тех же родительских соединениях.

Предположим, что commit J на самом деле является важным исправлением ошибки, не имеющим ничего общего с разработкой, продолжающейся dev.Мы хотели бы скопировать исправление в J в master, чтобы сделать новый коммит K, давая нам:

...--F--G--H--K   <-- master
         \
          I--J   <-- dev

Что нам нужно сделать в Git, это сделать снимки в I и J и сравните их друг с другом.Что бы не изменило с I на J, это исправление ошибки, которое мы должны применить к H.

Команда git cherry-pick делает это.Это делается с использованием встроенного в Git механизма слияния , так что любые другие различия между I и H могут быть корректно сохранены, но здесь мы проигнорируем детали: в простых случаяхслияние не требует сложной работы, и мы можем просто представить, что Git просто применяет изменения I -vs- J к H, чтобы создать копию J, которую мы назовем K.

Это дает нам именно то, что мы хотели: K и J делают то же самое вещь , поэтому K является своего рода копией J, но они делаютэто другая начальная точка , поэтому родительский элемент K равен H (на master), а сам K можно безопасно хранить только на master.Поскольку K является копией J, Git по умолчанию будет использовать того же автора и сообщение журнала и даже ту же отметку времени.Вы и сейчас являетесь коммиттером новой копии K, но тот, кто сделал J, является автором .Конечно, J и K имеют разные хеш-идентификаторы - это разные коммиты - но K является копией J.

Rebase - это просто en-masse copy

Предположим, что вместо этого мы начнем с этого:

...--F--G--H   <-- master
      \
       I--J--K   <-- dev

На этот раз нет важных исправлений ошибок.Мы просто хотели бы взять три коммита, которые находятся на dev, и заставить их начинаться с H, а не с F.

На этом этапе мы можем создать new dev2 ветвь, указывающая на коммит H, например:

$ git checkout -b dev2 master

, дающая:

...--F--G--H   <-- master, dev2 (HEAD)
      \
       I--J--K   <-- dev

Специальное имя HEAD говорит нам (иGit) какое имя ветки обновляется по мере продвижения, поэтому HEAD присоединяется к нашему новому dev2.

Теперь мы можем по одному копировать коммит I, затем J,тогда K.Давайте на этот раз назовем копии I', J' и K':

             I'-J'-K'   <-- dev2 (HEAD)
            /
...--F--G--H   <-- master
      \
       I--J--K   <-- dev

Теперь давайте удалим имя dev целиком:

$ git branch -D dev

             I'-J'-K'   <-- dev2 (HEAD)
            /
...--F--G--H   <-- master
      \
       I--J--K   ???

и затем переименование dev2 в dev:

$ git branch -m dev2 dev

             I'-J'-K'   <-- dev (HEAD)
            /
...--F--G--H   <-- master

Без имени, по которому их можно найти, оригинальные коммиты I-J-K, похоже, исчезли.Поскольку имя, под которым мы находим три новых коммита, - dev, оно кажется , как если бы мы волшебным образом заменили исходные три коммита.У нас нет - первоначальные три на самом деле все еще находятся в хранилище и обычно будут оставаться там некоторое время, если мы передумаем и захотим их вернуть.Но обычные команды Git не будут видеть их.Они скрыты, так что мы видим только новую и улучшенную ветвь dev.

Это - копирование коммитов, как будто с помощью git cherry-pick, а затем повторная перетасовка имен ветвей - вот что git rebase делаетНо предположим, что без ведома того, кто выполняет эту перестановку dev, вы в вашем хранилище Git, сделали еще несколько коммитов, которые зависят от коммита K?Если бы вы взяли их вещи, пока у них есть dev2, вы бы увидели это:

             I'-J'-K'   <-- origin/dev2
            /
...--F--G--H   <-- master, origin/master
      \
       I--J--K   <-- dev, origin/dev
              \
               L--M--N   <-- branch-b

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

             I'-J'-K'   <-- origin/dev
            /
...--F--G--H   <-- master, origin/master
      \
       I--J--K   <-- dev
              \
               L--M--N   <-- branch-b

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

             I'-J'-K'   <-- dev, origin/dev
            /
...--F--G--H   <-- master, origin/master
      \
       I--J--K--L--M--N   <-- branch-b

Это означает, что у вас теперь есть не три, а шесть коммитов на вашем branch-b, которые у них - людей с именами origin/* - нет .

Что касается Git, все шесть коммитов , I-J-K-L-M-N, ваши коммиты.

Если вы вернете свой branch-b на свой / их master прямо сейчас , Git попытается скопировать все шесть коммитов. Если вы вернетесь к их origin/dev, произойдет нечто более интересное, к чему мы вскоре вернемся.

Со временем они могут объединить свою работу с master, и / или вы можете объединить свою работу с вашей. Вот один потенциальный график, если они слили свои K' в свои master (ваш origin/master и ваш теперь обновленный master), а затем вы объединили этот O коммит с вашим branch-b, чтобы произвести слияние P

             I'--J'--K'   <-- dev, origin/dev
            /         \
...--F--G--H-----------O   <-- master, origin/master
      \                 \
       I--J--K--L--M--N--P   <-- branch-b

Всякий раз, когда вы перебазируете, ваш Git перечисляет коммиты для копирования

Когда вы запускаете git rebase <em>target</em>, ваш Git должен выяснить, кто обязуется копировать. Список коммитов для копирования начинается со всех коммитов , доступных с вашей текущей ветви - следовательно, коммиты I-J-K-L-M-N-P, но также F-G-H-...-O-P из-за того, что P является коммитом слияния. Здесь, опять же, я предлагаю проработать Think Like (a) Git .

Из этого большого списка всех достижимых коммитов Git немедленно вычитает все коммиты, достижимые из цели. Поэтому, если вы выберете, скажем, dev в качестве своей цели, Git отменит коммиты F-G-H-I'-J'-K'. Если вы выбрали master, Git исключит F-G-H-I'-J'-K'-O. Это очевидно, чтобы исключить из списка, потому что они уже есть в цели.

Git также пропускает любые merge коммиты, просто автоматически. Так что выбивает O и P. Коммиты слияния в буквальном смысле не могут быть скопированы: копирование коммита требует сравнения его с (одним) родителем, а коммиты слияния имеют двух родителей. У Git нет возможности узнать, какой родитель использовать, поэтому он просто не копирует их.

Но это все равно оставляет I-J-K-L-M-N в качестве списка коммитов (возможно) для копирования. Именно здесь приходит идея Git о том, как идентифицировать уже скопированный коммит.

Для каждого коммита, который находится в вышестоящем списке «уже там, не копируйте», который включает I'-J'-K' - Git вычисляет идентификатор патча , используя программу git patch-id. Это существенно сводит каждый коммит к приближению изменения от родителя этого коммита к этому коммиту. То есть Git находит тот же набор изменений, который git cherry-pick скопировал бы, и вычисляет хеш-идентификатор из этих изменений.

Затем Git вычисляет идентификаторы патчей для всех ваших коммитов I-J-K-L-M-N. Если какой-либо из этих идентификаторов патчей совпадает с идентификаторами патчей для вышестоящих коммитов, Git выбивает эти коммиты. Во многих случаях это полностью спасает вашу перебазировку, и все просто работает. Ваши исходные I-J-K коммиты - ну, ваши , теперь , даже если они не были вашими изначально - остаются позади в вашей ветке, потому что они скопировали их в I'-J'-K', а затем оставили свои I-J-K, но ваши branch-b сохранили их, хотя они думали , что оставили I-J-K позади навсегда. Но если у вашего I-J-K есть идентификаторы патчей, соответствующие их I'-J'-K', ваш ребаз будет работать хорошо.

Если их патч-яDs не соответствует, хотя сейчас у вас есть проблема. Git не может автоматически исключать скопированные коммиты. Коммиты , но не совсем такие же, будут сталкиваться, когда Git отправляется, чтобы применить их на новом месте для перебазирования. Это будут файлы, к которым вы никогда не прикасались, но чьи изменения, которые вы унаследовали в сохраненном файле, подтверждают, что они - кто бы «они» - думали , что они успешно оставили. Ты возвращаешь их работу как свою собственную, хотя это не то, что ты хотел.

Что нужно сделать, чтобы восстановить

Способ избавиться от всего этого - выяснить, какие коммиты должны быть скопированы . Если вам повезет, они все могут быть в аккуратном простом ряду. В этом случае вы можете использовать git rebase --onto, чтобы разделить работу на две части. Обычно вы запускаете:

git rebase <target>

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

git rebase --onto <target> <dontcopy>

и теперь target только указывает место для размещения копий; аргумент dontcopy говорит Git, что не нужно копировать. Те же самые правила достижимости из Think Like (а) Git определяют, какие коммиты будут и не будут включены в список «возможно, копировать». Затем Rebase выбросит все слияния и все коммиты с идентичным патчем и скопирует все, что осталось.

Если вам не повезло, набор коммитов для копирования будет разбросан вокруг, туда и сюда. Вам нужно будет создать новую временную ветвь или использовать режим «detached HEAD» и выполнить серию команд git cherry-pick, чтобы скопировать коммиты, которые должны скопировать, те, которые действительно ваши.

Слияние позволяет избежать этой проблемы, потому что слияние не копирует коммиты

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

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

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

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