git команда для разрешения конфликтов в ребазе на основе предыдущей истории - PullRequest
1 голос
/ 08 февраля 2020

Я пытаюсь собрать воедино соответствующую команду git для «Взять branch_c», найти все коммиты, где он отличается от branch_b, основать его на branch_a и разрешить любые конфликты на основе существующих изменяется с branch_b на branch_c. "

Сценарий

  • Вы и коллега работаете над функциями, которые должны быть рассмотрены и объединены в мастер или некоторые другие другая ветка вверх по течению. (скажем branch_a).

  • Ваш коллега работает над набором функций, который скоро будет представлен на branch_b. Вы постоянно просматриваете его, поскольку они go.

  • . Одновременно вы работаете над функцией, которая должна быть представлена ​​в будущем на основе branch_b. Вы работаете над этой функцией на branch_c и постоянно перебазируете ее и настраиваете на branch_b.

  • Иногда возникает конфликт между вашей веткой функций (branch_c) и вашим собеседником. филиал (branch_b). Вы решаете их, когда они продолжают работать.

  • Ваша подруга заканчивает свою работу и просит отзыв у всей команды. По ряду причин некоторые коммиты удаляются. Возможно, для ясности есть небольшая сква sh, или, может быть, команда решит вытащить некоторые изменения и внести их в более позднюю ветку.

  • Изменения сделаны, и ветка вашего коллеги объединена в branch_a.

  • Теперь вы хотите перебрать новую историю, но когда вы это сделаете, вам вдруг придется заново разрешить все конфликты, которые вы когда-либо разрешали.

Обычно в такой ситуации я просто запоминаю и повторяю каждое разрешение конфликта. Но, конечно, есть лучший способ?

Я ожидал, что что-то вроде git rebase --strategy-option=ours <branch_c> --onto <branch_a> <divergent_ref> (отредактированное для исправления аргумента команды в соответствии с приведенным ниже разговором) сработает - исправить основы при разрешении конфликтов на основе предыдущего («нашего»). ") history, , но этого не происходит - вместо этого он воспроизводит историю, как если бы вы всегда игнорировали другую ветвь. (edit: я неверно истолковал результат этой команды - in факт, что он делает именно то, что я описал, и создает именно ту историю, которую я считаю наиболее желательной в этом сценарии. )

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

Почему бы просто не использовать git слияние?

(примечание: этот раздел был добавлен после краткого обсуждения в комментариях ниже.)

Моя цель - достоверно изобразить линейную прогрессию для будущих читателей истории будь то git журнал, git вина, git бисс, или любым другим способом. Я не хочу, чтобы будущие пользователи комментировали одну из этих измененных строк и переходили к фиксации, которая, по-видимому, основана на состоянии branch_c в произвольный момент времени до слияния branch_b. Вместо вкрапленной истории я хочу, чтобы будущие читатели могли четко аннотировать эти изменения до момента объединения branch_b.

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

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

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

Ответы [ 2 ]

1 голос
/ 08 февраля 2020

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

Давайте использовать master, feature и feature1 вместо branch_a, branch_b и branch_c.

До того, как feature будет рассмотрен и объединен в master вас есть что-то подобное Вы работаете над feature1 в ожидании слияния feature. Я бы назвал feature1 «веткой ожидания».

A - B [master]
     \
      C - D - E [origin/feature]
               \
                F - G [feature1]

Вы можете легко быть в курсе событий с помощью git rebase origin/feature.

feature, объединенного в master с изменениями. Допустим, D удален, а E изменен. feature завершено, поэтому оно удалено.

A - B ------ M [master]
     \      /
      C - E1
       \
        D - E - F - G [feature1]

git rebase master воскресит старый D и E. Git не может знать, что D и E когда-то были частью feature.

A - B ------ M [master]
     \      / \
      C - E1   D - E - F - G [feature1]

Вам необходимо использовать --onto. Вы хотите сделать ребаз из старого наконечника feature, то есть E. git rebase --onto master E feature1

A - B ------ M [master]
     \      / \
      C - E1   F - G [feature1]

Вы также можете получить старую позицию origin/feature из журнала. git rebase --onto master origin/feature@{1} feature1.

См. Восстановление из Upstream Rebase в документах git -rebase , в частности в трудном случае.

Я ожидал что-то вроде git rebase --ours --onto to work - исправлять базы при разрешении конфликтов на основе предыдущей («нашей») истории, но этого не происходит - вместо этого кажется, что она воспроизводит историю, как если бы вы всегда игнорируется другая ветвь.

rebase не занимает --ours. Я не могу сказать, что пошло не так, не зная, что ты сделал.

0 голосов
/ 08 февраля 2020

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

Все это - значительный объем работы, и отдача от нее неясна. И все же я сам предпочитаю этот метод «предварительной очистки истории», когда я могу себе это позволить.

Теперь давайте добавим и это:

git rebase --strategy-option=ours feature --onto master <divergent_ref>

Вы должны быть очень осторожными с -X ours: Git следует простым текстовым правилам, которые не будут работать во всех ситуациях. (Если у вас есть автоматизированные тесты, рассмотрите возможность добавления --exec для их запуска.)

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

Вы можете использовать git rerere здесь, иногда. В основном --onto - это то, что вам нужно (см. ответ Шверна ).

Есть несколько вещей, которые необходимо знать, прежде чем начать этот путь:

  • Во-первых, помните, что git rebase по существу означает копировать некоторые коммиты, как если бы это было git cherry-pick. Некоторые операции перебазирования действительно используют git cherry-pick. Один - самая старая форма rebase - использует git format-patch и git am. В целом это работает не так хорошо, но работает быстрее. Поскольку он не использует git cherry-pick, я думаю, что git rerere также не будет применяться. Но добавление --strategy-option вызывает вариант выбора вишни; добавление -m или -i или --exec.

  • вишня является слиянием!

  • После перечисления некоторых идентификаторов коммитов ha sh, rebase начинает свою реальную работу с отсоединения HEAD в коммите, к которому должны быть добавлены копии. Затем он делает копии. После того, как выбранные коммиты были скопированы, операция rebase записывает идентификатор ha sh последнего скопированного или HEAD commit в название ветки, в которой вы работали, когда все это запускало.

  • Но мы также должны взглянуть на набор коммитов, которые должны быть скопированы. В первом предложении выше написано , перечислив некоторые коммиты ha sh ID . Какие га sh идентификаторы? Вот где приходит --onto.

Обычный git rebase дает вам одну ручку управления, которая документация git rebase вызывает upstream. Эта одна ручка управления выбирает и копирует и , куда помещать копии . Использование git rebase --onto дает вам отдельную ручку для , куда помещать копии , что освобождает аргумент upstream, чтобы вы могли более тщательно выбирать, что копировать.

Аргумент upstream обычно является и , куда помещать копии и , что не , чтобы скопировать , с что копировать список определяется по результату git rev-list <em>upstream</em>..HEAD, более или менее. Мы увидим это в действии ниже. Но более или менее здесь важен: документация по rebase цитирует эту запись A..B как то, как она определяет, что копировать, а что не копировать, но на самом деле она меньше. Опять же, мы увидим больше об этом ниже.

Как express что, я думаю, ваша настоящая проблема здесь

В любом случае, давайте нарисуем то, что я считаю вашей реальной проблемой- со временем, как несколько разных моментальных снимков:

                   E--F--G   <-- feature1
                  /
...--o--*--o--o--*--o--o   <-- mainline
         \
          A--B--C--D   <-- feature2

Вот так все и начинается: идет какой-то проект с какой-то основной веткой и разрабатываются две функции. Но теперь выясняется, что feature2 зависит от чего-то в feature1. Итак, теперь вы хотите сделать ребаз, то есть скопировать некоторые коммиты из feature2, чтобы они появились в feature1. На этом этапе этот ребаз легко вызывается:

git checkout feature2; git rebase feature1

уже выбирает правильные коммиты и правильную точку копирования. Если это копирование выполняется посредством cherry-pick, каждая копия выполняется механизмом слияния, который включает сохранение разрешений конфликтов с помощью git rerere, если установлено rerere.enabled. (Если установлено , а не , Git не сохраняет ни конфликты, ни их разрешения.)

Конечный результат:

                           A'-B'-C'-D'  <-- feature2
                          /
                   E--F--G   <-- feature1
                  /
...--o--*--o--o--*--o--o   <-- mainline
         \
          A--B--C--D   [abandoned]

Новая цепочка коммитов, A'-B'-C'-D', очень похоже на вашу оригинальную цепочку, но идентификаторы ha sh отличаются. Поскольку на самом деле никто не смотрит на идентификаторы ha sh, а исходная цепочка A-B-C-D теперь невидима для обычных операций git log, никто никогда не обращает внимания на переход - но это реально. И это собирается укусить вас по-другому.

Теперь, когда вы перебазировали ваш feature2 поверх feature1, кто-то еще (или, может быть, даже вы ) перебазирует feature1. Результат:

                           A'-B'-C'-D'  <-- feature2
                          /
                   E--F--G
                  /
...--o--*--o--o--*--o--o   <-- mainline
                        \
                         E'-F'-G'   <-- feature1

(я перестал рисовать в A-B-C-D, так как они больше не нужны.) Обратите внимание, как E-F-G предполагается отказаться, но на самом деле это не так.

Обычный git checkout feature2; git rebase feature1 выберет для копирования коммиты E-F-G-A'-B'-C'-D'. Используя --onto, вы можете запустить git rebase --onto feature1 <em>hash-of-commit-G</em>to tellgit rebase *do not copy commits E-F-G.

Но на самом деле, внутри git rebase есть удобная функция: она автоматически исключает некоторые коммиты из своего списка коммитов. Я уже упоминал об этом выше, в части более или менее ... фактически меньше . Документация по перебазированию фактически говорит это:

[Копируемые коммиты] представляют собой тот же набор коммитов, который был бы показан git log <upstream>..HEAD; или git log 'fork_point'..HEAD, если --fork-point активен (см. описание на --fork-point ниже); или git log HEAD, если указан параметр --root.

Но это не так! По умолчанию git rebase также исключает:

  • любой коммит слияния и
  • любой коммит в <em>upstream</em>..HEAD, для которого его git patch-id соответствует патчу -ID коммита, который находится в HEAD..<em>upstream</em>.

Предположим, что тот, кто скопировал E-F-G в E'-F'-G', не должен был разрешать конфликты слияния или что-либо еще. В этом случае копии , E' - G' будут иметь те же git patch-id, что и соответствующие им оригинал . Так что git rebase отбросит эти коммиты даже без --onto.

(в документации также упоминается точка ветвления, которая использует собственный рефлог Git для upstream если вы этого не сделали, выберите значение --onto, однако режим точки разворота (1) не всегда активен и (2) не всегда правильный, когда активен . как и сам выбор точки разветвления обманывает меня: я думаю, что он закапывает слишком много магии c. Кроме того, поскольку он использует reflogs, он завершается ошибкой, если истек срок действия критической записи reflog. Но это все равно в стороне. )

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

Но у вас может быть другая проблема. В частности, предположим, что пока вы работаете с feature2, а кто-то еще работает с feature1, они понимают то же самое, что вы сделали, или слышите или видите одно из изменений ваших коммитов, и они добавляют new совершить это частично , но не полностью, исправить то, что вы делаете, но по-другому? Тогда, возможно, они имеют:

                           A'-B'-C'-D'  <-- feature2
                          /
                   E--F--G
                  /
...--o--*--o--o--*--o--o   <-- mainline
                        \
                         H-E'-F'-G'   <-- feature1

, где H больше похож на один из ваших оригинальных A-B-C-D коммитов, чем на ваш обновленный A'-B'-C' коммитов. В этом случае вы можете вернуть свою серию A-B-C-D. Commit D почти наверняка все еще находится в вашем Git, помнятом как feature2@{<em>number</em>}. (Точное число зависит от того, сколько обновлений вы сделали для feature с тех пор.) Или, конечно, вы можете делать то, что я делаю, то есть сохранять исходный указатель feature2, создавая feature2.0, feature2.1 и т. Д. Давайте нарисуем его обратно, как feature2.0, и переименуем feature в feature2.1:

                           A'-B'-C'-D'  <-- feature2.1
                          /
                   E--F--G
                  /
...--o--*--o--o--*--o--o   <-- mainline
         \              \
          \              H-E'-F'-G'   <-- feature1
           \
            A--B--C--D   <-- feature2.0

Если H очень близко к одному из ваших первых четырех коммитов - так что он либо имеет тот же идентификатор патча, либо вы можете использовать drop в интерактивной перебазировке - вы можете использовать его в качестве источника , Если вам нужно было разрешить некоторые конфликты раньше, git rerere сделает это. Теперь мы можем сделать:

git checkout -b feature2 feature2.0
git rebase -i feature1

(интерактивная перебазировка позволяет делать «отбрасывание» и принудительно собирать вишню; если хотите, используйте git rebase -m, чтобы принудительно собирать вишню без интерактивности). Если все пойдет хорошо и при условии, что H == C как бы, мы получим:

                           A'-B'-C'-D'  <-- feature2.1
                          /
                   E--F--G
                  /
...--o--*--o--o--*--o--o  mai...   A"-B"-D"   <-- feature2
         \              \         /
          \              H-E'-F'-G'   <-- feature1
           \
            A--B--C--D   <-- feature2.0

rerere, возможно, был полезен с точки зрения сохранения разрешения конфликта слияния для D, и автоматическое обнаружение идентификатора патча может выдать вам коммит C.

Просмотр rebase в виде серии cherry-picks

Обычное слияние работает, находя база слияния - общий общий коммит между двумя ветвями - и выполнение двух различий. Разница между базой слияния и каждой веткой подсказывает нам, кто что изменил:

          I--J   <-- ours (HEAD)
         /
...--G--H
         \
          K--L   <-- theirs

Сравнение снимка коммита H с J говорит нам, что мы изменено, на ветке ours; сравнение H с L говорит нам, что они изменились; и git merge объединяет изменения. В случае конфликтов (расширенная) стратегия-опция -X ours или -X theirs указывает Git разрешить конфликт автоматически , выбрав H -vs- J («наш») или H -vs- L ("их").

Для обычного слияния окончательный коммит после разрешения всего представляет собой коммит слияния с родителями J и L, в этом порядке (сначала наш, потом их).

Вишневое дерево берет нормальное слияние и подрывает его. Вместо поиска общего коммита база слияния - это просто выбранный коммит parent commit.

Когда мы перебазирование и копирование первого коммита имеет смысл:

...--o--o--*--o--H   <-- mainline, HEAD (detached)
            \
             A--B--C   <-- feature

Теперь мы копируем коммит A. Здесь "наш" - H: коммит наконечника mainline, на который HEAD указывает непосредственно (отсоединен). Основа псевдо-слияния - коммит *: точка, в которой A впервые расходится. Таким образом, мы будем различать * против H, чтобы увидеть, что "мы" изменились, и * против A, чтобы увидеть, что "они" изменились. Тогда мы объединим эти различия: это вернет нас H, плюс все, что мы сделали в A. Мы зафиксируем результат и сделаем A', копию A:

                   A'  <-- HEAD
                  /
...--o--o--*--o--H   <-- mainline
            \
             A--B--C   <-- feature

(Финальный коммит вишневого пика - обычный коммит без слияния.)

Но теперь мы скопируем B. Его родитель A, поэтому мы будем использовать A для базы слияния. Мы рассмотрим A против A', чтобы увидеть, что "мы" изменились, и A против B, чтобы увидеть, что "они" изменились. Если мы слепо примем «наши» - то есть A против A' - в конфликте, мы можем потерять важные изменения с B. Может быть, они нам не нужны - может быть, * -vs- H уже содержали их. Но, возможно, мы это сделаем.

В любом случае, когда все это будет сделано, мы получим:

                   A'-B'  <-- HEAD
                  /
...--o--o--*--o--H   <-- mainline
            \
             A--B--C   <-- feature

, и мы готовы к вишне C, как и раньше. Когда это будет сделано, Git будет восстанавливать имя feature с C и вместо него указать C' (и повторно присоединить HEAD):

                   A'-B'-C'  <-- feature (HEAD)
                  /
...--o--o--*--o--H   <-- mainline
            \
             A--B--C   [abandoned]

, и это наша перебазировка .

Так как все это теоретически, давайте просто сделаем выводы сейчас

Реальные моменты, о которых следует помнить:

  • rebase copy (некоторые) коммиты, как будто или фактически git cherry-pick;
  • , реальный ключ к минимизации работы - это выбор правильных коммитов для копирования;
  • иногда это означает --onto и иногда это может даже означать возвращение к более ранней копии вашей собственной работы.

(на самом деле я сам не использую git rerere и не уверен, использует ли cherry-pick его автоматически. Если нет, вы можете использовать его вручную.)

...