Как совершить слияние ветки во время слияния - PullRequest
0 голосов
/ 22 ноября 2018

Я часто делаю git merge --no-ff --no-commit <feature_branch> своему мастеру, чтобы проверить, что все работает, как ожидалось, перед тем, как на самом деле совершить слияние.

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

Поэтому я вместо этого отменяю слияние (git reset --hard), теряя все разрешение конфликтов, которые я уже сделал, переключаюсь назад на ветку функций (git checkout <feature_branch>), внедрить исправление (которое я должен помнить до сих пор и не могу проверить в контексте слияния), git commit (+ git push), затем переключиться обратно на мастер (git checkout master) и повторно-после слияния.

Этот процесс громоздок и подвержен ошибкам.

Есть ли способ зафиксировать изменения в ветви функций с в разрешении слияния или, еслиэто невозможно, зафиксируйте ветвь функции в другом терминале, а затем обновите объединение, чтобы включить новый набор изменений без потери существующего прогресса?

Или есть другой рабочий процесс, который решит эту проблему?

1 Ответ

0 голосов
/ 22 ноября 2018

Есть ли способ зафиксировать изменения в ветви функций из разрешения слияния

Нет: конфликты слияния используют индекс для хранения всех трех версий (базовая, --ours--theirs в слотах индекса 1, 2 и 3 соответственно) каждого конфликтующего файла.Git создает новые коммиты из того, что находится в индексе, и требует, чтобы каждый файл в индексе был в своем обычном «разрешенном» слоте (нулевой слот), а не в качестве трех ненулевых слотов для разрешения слияния-конфликта.

(Копия файла рабочего дерева содержит все усилия Git по объединению этих трех входов, но Git использует его снова только после того, как вы исправите его и запустите git add <em>file</em>. Это копирует содержимое file обратно в индекс, запись в нулевой слот и очистка слотов 1-3. Теперь файл разрешен и готов к фиксации.)

Поскольку для любого заданного рабочего дерева существует только один индекс, 1 и этот индекс "занят" слиянием, у вас нет индекса (и, если на то пошло, рабочего дерева), в котором можно внести изменения в ветвь объекта.Эта фраза, в частности, в любом заданном рабочем дереве , является большой подсказкой, однако:

или, если это невозможно, зафиксировать ветвь функции в другом терминале...

Да, это возможно.Это значительно проще, чем в Git версии 2.5, которая изучила новую функцию: git worktree add.


1 Возможно установить временный индекс, и в версиях Git, предшествующих 2.5было несколько хакерских сценариев, которые выполняли эквивалент git worktree add, используя символические ссылки и временные индексные файлы, и много другого.


Использование git worktree add

Каждая добавленная работаДерево имеет свой собственный индекс (и, не случайно, также свой собственный HEAD).Если вы находитесь в процессе слияния feature в master, так что основное рабочее дерево и индекс хранилища имеют дело с веткой master, вы можете запустить с верхнего уровня рабочего дерева:

git worktree add ../feature

или:

git worktree add /path/to/where/I/want/feature

или:

git worktree add /path/to/dir feature

(но не просто git worktree add /path/to/dir, поскольку это попыталось бы проверить ветку с именем dir).

Это:

  • создаст новый каталог ../feature или /path/to/where/I/want/feature или /path/to/dir
  • по сути, запустить git checkout feature по этому пути.

Теперь у вас есть дополнительное рабочее дерево, связанное с текущим хранилищем.Это добавленное рабочее дерево находится на ветке feature.(Ваше основное рабочее дерево все еще работает на master, а продолжающееся слияние все еще продолжается.) В этом другом рабочем дереве вы можете изменять файлы, git add их в его index и git commit для добавления новых коммитов в ветку feature.

Однако проблема все еще существует.Это более очевидно, если вы используете отдельный клон;давайте немного рассмотрим это сейчас.

Использование отдельного клона

Если у вас есть Git до 2.5 или вы беспокоитесь о различных ошибках в git worktree add (их несколько, включая некоторые довольноважные только недавно исправленные в 2.18 или 2.19 или около того), вы можете просто повторно клонировать ваш репозиторий.Вы можете либо повторно клонировать исходное хранилище в новый клон, либо повторно клонировать свой клон в новый клон.В любом случае, вы получаете новый репозиторий с его собственными ветвями, индексом и рабочим деревом, и в этом клоне вы можете делать все, что захотите.

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

Передача коммитов получает коммиты в исходный клон, что нормально;но очевидно, что слияние, которое вы делаете, использует коммиты, которые вы имели при запуске слияния, а не какие-либо новые, сделанные вами в другом клоне.Но это верно даже для git worktree add, как мы увидим через мгновение.

Добавлены рабочие деревья против отдельных клонов

Когда вы используете git worktree add, два рабочих дерева совместно используют базовый репозиторий.Это означает, что коммиты, которые вы делаете из или рабочего дерева, немедленно доступны для вас, работающего в другом рабочем дереве .Однако они также имеют общие имена ветвей , что приводит к ограничению, которое git worktree add включает в себя то, что отдельный клон этого не делает.

В частности, каждое добавленное рабочее дерево имеет побочный эффект:«заблокировать» доступ к названию этой ветви.То есть, как только вы добавили рабочее дерево feature, нет другого рабочего дерева , которое может использовать ветку feature.Если основное рабочее дерево находится на master, никакое добавленное рабочее дерево не может использовать ветку master.Имя каждой ветви является эксклюзивным для каждого добавленного рабочего дерева.(Примечание: вы можете использовать git checkout в добавленном рабочем дереве для изменения ветвей, если вы сохраняете свойство эксклюзивности.)

Вы, конечно, можете просто удалить добавленное рабочее дерево.Поскольку этого рабочего дерева теперь нет, ветка, на которую он имел исключительные права, теперь доступна для любого другого рабочего дерева.Подробности смотрите в документации .

Ошибки, которые были исправлены в последнее время, имеют отношение к более старым добавленным рабочим деревьям.Если вы добавляете рабочее дерево, рекомендуется завершить работу в , которая добавила рабочее дерево, в течение нескольких недель, а затем отказаться от нее.Другими словами, используйте его для относительно быстрых проектов.(Самая страшная ошибка, на мой взгляд, заключается в том, что git gc иногда думает, что объект не используется, потому что он не может проверить HEAD и индексные файлы добавленных рабочих деревьев для идентификаторов объектов. Время сокращения по умолчанию 2недели означает, что вы защищены от этой ошибки в течение как минимум двух недель с момента начала работы в добавленном рабочем дереве. Вы получаете больше времени, сбрасывая часы при каждой фиксации, при условии, что добавленное рабочее дерево не включеноОтдельная ГОЛОВА.)

Сцепка

Я упоминал, что независимо от того, что вы здесь делаете, проблема все еще есть.Это связано с тем, как Git работает "под капотом" . 2

, а затем обновляет объединение, чтобы включить новый набор изменений без потерисуществующий прогресс?

Слияние, в котором вы находитесь, слияние feature с master, - по крайней мере, в некотором смысле - не слияние ветви .Это объединение commit .

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

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

A <-B <-C

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

Главное, что делает имя ветви в Git, это позволяет Git найти, что last совершает коммитфилиал:

A <-B <-C   <--master

Имя master находит коммит C.Git определяет так, чтобы любой идентификатор находился внутри master, , который является коммитом tip из master.Следовательно, чтобы добавить новый коммит к master, нам нужно, чтобы Git выписал содержимое нашего нового коммита, установив родительский элемент new в C.Новый коммит получает новый большой хадный идентификатор хеша, но мы просто назовем его D.Затем Git записывает хэш-идентификатор D в имя master:

A <-B <-C <-D   <--master

имя по-прежнему master, но хэш-идентификатор , которыйmaster представляет изменилось. По сути, имя перемещено из коммита C в коммит D, когда мы сделали новый коммит.

Эти внутри-коммитстрелки, которые всегда указывают назад, 3 , неизменны, как и любая другая часть любого коммита.Поэтому нам не нужно их рисовать: мы знаем, что они присоединяются ко второму коммиту связанной пары.Однако стрелки, выходящие из названий ветвей, перемещаются, поэтому мы должны их нарисовать.Это дает нам то, что мы видим, когда собираемся запустить git merge:

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

В этот момент, когда HEAD присоединен к master, мы запускаем git merge feature.Git:

  • находит наш текущий коммит H, используя HEAD;
  • находит другой коммит L, используя имя feature;
  • используетсам график, чтобы найти лучший коммит, который находится на обеих ветвях, то есть коммит F;
  • работает, в действительности, two git diffs:

    git diff --find-renames <hash-of-F> <hash-of-H>   # what we changed on master
    git diff --find-renames <hash-of-F> <hash-of-L>   # what they changed on feature
    
  • пытается объединить две разницы и применить полученные изменения к снимку из F;

  • , если это удается, делаетновый коммит, но если нет, останавливается конфликтом слияния.

новый коммит будет иметь двух родителей - две обратные ссылки, указывающиедо H и L.Как только Git сделает это - либо автоматически, потому что слияние выполнено успешно, либо потому, что мы разрешаем конфликты и используем git merge --continue или git commit, чтобы завершить слияние самостоятельно - у нас будет:

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

Но предположим, что слияние прекращаетсяс конфликтами, так что у нас еще есть:

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

с нашим индексом и рабочим деревом, содержащим результат частичного слияния?Если теперь мы используем git worktree add, чтобы создать второе рабочее дерево, HEAD которого присоединяется к feature.Добавлены файлы рабочего дерева из commit L;его индекс также содержит копию файлов из L.Если затем мы добавим новый коммит с использованием этого рабочего дерева - давайте назовем это N, поскольку мы зарезервировали M для нашего возможного слияния - мы получим:

...--F--G--H   <-- master (HEAD of main work-tree)
      \
       I--J--K--L--N   <-- feature (HEAD of feature work-tree)

Текущее слияние , однако, все еще объединяет коммиты H и L.Когда мы в конечном итоге закончим слияние, мы получим:

...--F--G--H------M   <-- master (HEAD of main work-tree)
      \          /
       I--J--K--L--N   <-- feature (HEAD of feature work-tree)

Это не то, что вы хотели!


2 Эта ссылка ведет на Quora ответ о фразе "под капотом" .( первая ссылка , которую Google придумал для меня , пошла в Urban Dictionary, который имеет довольно ... другое определение, хм. Эта статья Quora утверждает, что начинающие программисты на Pythonне нужно знать о довольно специфическом подходе Python к переменным, когда все объекты всегда в штучной упаковке , а переменные просто связаны с полями, и я не согласен с утверждением - илипо крайней мере, сказал бы только для очень - начала - но описание «под капотом» все еще хорошо.)

3 По какой-то причине мне нравитсяиспользовать британско-английское различие между «назад» и «назад» : «назад» - это прилагательное, в то время как «назад», с -s, - наречие.Опять же, мне также нравится произносить «серый» с помощью E. Но я опускаю U в «color».


Решая повторное слияние

Что вы хотели было:

, а затем обновить объединение, чтобы включить новый набор изменений без потери существующего прогресса?

Это действиефактически требует, чтобы мы вновь объединились .То есть, вы хотели:

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

То, что у вас есть - предположим, вы удалите добавленное рабочее дерево в этот момент, чтобы упростить чертеж - это:

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

Commit M укажет на H и L, несмотря ни на что.Но вы можете , теперь можете делать больше вещей.

Например, вы можете просто позволить M продолжать существовать и снова запускать git merge feature.Git теперь будет делать то же, что и в прошлый раз: преобразовать HEAD в коммит (M), разрешить feature в коммит (N), найти базу слияния -лучший общий коммит - который будет коммитом L, и Git объединит два набора различий.Эффект будет состоять в том, чтобы выбрать исправления, которые вы только что сделали в N, и это обычно даже будет работать без конфликтов, хотя детали зависят от точных исправлений, которые вы сделали в L -vs- N.Git сделает новый коммит слияния M2:

...--F--G--H------M--M2   <-- master (HEAD of main work-tree)
      \          /  /
       I--J--K--L--N   <-- feature (HEAD of feature work-tree)

Обратите внимание, что M2 зависит от M для существования M2.Вы не можете просто бросить M полностью, по крайней мере, пока.Но что, если вы захотите отказаться от M в пользу M2?

Ну, M2 имеет правильно слитый результат как снимок .Итак, давайте сохраним где-то хеш-код M2, используя другое имя ветви или тега:

$ git tag save

Теперь давайте используем git reset --hard HEAD~2 для принудительного удаления обоих M и M2 из графика.HEAD~2 означает «отойти назад от двух первых родительских ссылок из текущего коммита».Первый родитель M2 - M, а первый родитель M - H, так что это говорит Git, чтобы имя master снова указывало на H:

             .............<-- master (HEAD)
            .
...--F--G--H------M--M2   <-- tag: save
      \          /  /
       I--J--K--L--N   <-- feature

Если бы у нас не было тегов, сохраняющих M2 и M видимыми, это выглядело бы так, будто эти коммиты полностью исчезли.Между тем, --hard часть git reset велела Git перезаписать наш индекс и рабочее дерево также содержимым commit H.

Теперь мы можем запустить:

git merge feature

, который говорит Git использовать коммит HEAD для поиска коммита H, использовать имя feature для поиска коммита N, чтобы найти базу слияния (F)и начать процесс слияния.Это вызовет все те же конфликты, что и раньше, и вам придется разрешить их заново, но у вас есть разрешения, доступные вам в коммите M2.Так что сейчас вам просто нужно сказать Git: Возьмите разрешенные файлы из M2.Поместите их в мой индекс и рабочее дерево. Для этого запустите:

$ git checkout save -- .

(из верхнего уровня рабочего дерева).Имя save указывает на фиксацию M2, так что это фиксация, из которой git checkout будет извлекать файлы.-- . говорит git checkout: Не проверять напрямую этот коммит;вместо этого оставьте HEAD в покое, но получите файлы из этого коммита, которые идут с именем ..Скопируйте эти файлы в индекс в нулевом слоте, уничтожив любую информацию о конфликте слияния в слотах 1-3.Скопируйте файлы из индекса в рабочее дерево.

Поскольку . означает все файлы в этом каталоге , и вы находитесь в каталоге верхнего уровня, это заменяет все ваших файлов индекса и рабочего дерева с тем, что находится в M2.

Предупреждение: если у вас есть файл в вашем индексе и рабочем дереве прямо сейчас, то есть отсутствует в M2, это не удалит этот файл.То есть предположим, что одним из исправлений, которые вы сделали в N, было удаление файла с именем bogus.Файл bogus существует в M, но исчез в M2.Если bogus также находится в H, это прямо сейчас находится в вашем индексе и рабочем дереве, так как вы начали с файлов из F - у которых было bogus или нет - и приняли все измененияс H, который либо сохранил bogus, либо добавил bogus.Чтобы обойти это, используйте git rm -r . перед git checkout save -- ..Шаг удаления удаляет каждый файл из индекса и рабочего дерева, что нормально, потому что нам нужно только то, что находится в M2, а шаг git checkout save -- . получит все из них.

(Там яБолее короткий способ сделать все это, но это не очень «учебное пособие», поэтому я использую здесь более длинный метод.На самом деле, есть два таких способа: один использует git read-tree в середине слияния, а другой - git commit-tree с двумя -p аргументами, чтобы избежать необходимости вообще запускать git merge.Оба требуют высокого уровня комфорта и знакомства с внутренней работой Git.)

Как только вы извлечете все файлы из save (commit M2), вы готовы закончить объединение как обычно, используя git merge --continue или git commit.Это сделает новое слияние M3:

             -------------M3   <-- master (HEAD)
            /            /
...--F--G--H------M--M2 /  <-- tag: save
      \          /  /__/
       I--J--K--L--N   <-- feature

Теперь вы можете удалить тег save, который делает коммиты M и M2 невидимыми (им требуется некоторое время, чтобыреальный, поскольку их хэш-идентификаторы также хранятся в журнале HEAD).Как только мы перестанем их рисовать, мы можем начать звонить M3 просто M вместо этого:

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

и - это то, что вы хотели .

Использование git rerere

Есть еще один способ сделать все это, избегая тега и сохраняя слияние.Git имеет функцию под названием «rerere», которая означает решение re -use re corded re .Я на самом деле не использую эту функцию, но она предназначена для такого рода вещей.

Чтобы использовать ее, вы запускаете git config rerere.enabled true (или git config rerere.enabled 1, оба означают одно и то же) перед этим вы начинаете слияние, которое может иметь или не иметь конфликты, и может потребоваться, а может и не потребоваться повторное выполнение.Так что вам нужно заранее спланировать это.

Включив rerere, вы просто выполняете слияние как обычно, разрешаете любые конфликты как обычно - возможно, также добавляете рабочее дерево и дополнительные коммиты в ветку Featureдаже если они не будут в этом слиянии, а затем завершат свое слияние как обычно.Затем вы делаете:

git reset --hard HEAD^    # or HEAD~, use whichever spelling you prefer

Это лишает слияния, теряя ваши разрешения - но более ранний шаг завершения слияния спас разрешений.В частности, Git сохранил конфликтов во время остановки слияния, а затем сохранил только их разрешения (не все файлы!) Во время, когда вы сказали Git , что это слияниезакончено .

Теперь вы можете запустить git merge feature снова.Вы можете подождать, пока не добавите еще больше коммитов к feature;rerere сохраненные резолюции останутся на несколько месяцев.(Согласно документации примечания:

По умолчанию неразрешенные конфликты старше 15 дней и разрешенные конфликты старше 60 дней [сборщик мусора]

всякий раз, когда Git запускает git rerere gc, который git gc будет работать для вас. Сам Git будет запускать git gc для вас автоматически, хотя обычно не каждый день, поэтому они могут задерживаться более 15 и 60дней.)

На этот раз, после того, как вы запустите git merge вместо записи конфликтов, Git заметит, что уже имеет записанные разрешения и будет использовать их для исправления конфликтов.Затем вы можете проверить результаты и, если все выглядит хорошо, git add файлы и завершить объединение.(Вы даже можете иметь полностью разрешенные файлы Git auto - add, используя другой элемент конфигурации rerere; подробности см. в документации git config .)

(Этовероятно, самый дружественный режим разработки, если вам нужно многократно повторять слияния. Мне самому это не очень нравится, потому что трудно увидеть, какие разрешения записаны и когда они истекут, а автоматическое повторное использование может произойти дажеесли вы не хотите этого. Вы можете использовать git rerere forget, чтобы очистить записанные разрешения, но это тоже немного болезненно. Поэтому я предпочитаю сохранять полные коммиты, которые имеют более четкие времена жизни. Но я также делаюне нужно многократно повторять слияние.)

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