Ответ 1
Существует много способов сделать это, и какой из них вы должны использовать, зависит от того, какой результат вы хотите, и тем, что вы и кто-то, кто сотрудничает с вами (если это общий репозиторий), ожидают увидеть в будущем.
Три основных способа сделать это:
- Не беспокойтесь. Отключить ветвь
A
, создать новыйA2
и использовать это. -
Используйте
git reset
или эквивалент для повторной точкиA
в другом месте.Методы 1 и 2 фактически одинаковы в долгосрочной перспективе. Например, предположим, что вы начинаете с отказа от
A
. На некоторое время все развиваются наA2
. Затем, когда все используютA2
, вы полностью удаляете имяA
. Теперь, когдаA
ушел, вы даже можете переименоватьA2
вA
(все остальные, использующие его, должны будут сделать то же самое переименование, конечно). На данный момент, что, если что-либо, выглядит по-другому между тем случаем, когда вы использовали метод 1 и случай, когда вы использовали метод 2? (Есть одно место, которое вы все еще можете увидеть разницу, в зависимости от того, как долго длится ваш "длинный пробег", и когда вы истекаете старые рефлоги.) - Сделайте слияние, используя специальную стратегию (см. ниже "альтернативные стратегии слияния" ). Здесь вы хотите
-s theirs
, но не существует, и вы должны подделать его.
Сторона примечания: там, где ветвь "создана из", в основном не имеет значения: git не отслеживает такие вещи, и в общем случае ее невозможно обнаружить позже, поэтому, возможно, это не имеет значения для вас, (Если это действительно имеет значение для вас, вы можете наложить несколько ограничений на то, как вы манипулируете веткой, или пометить ее "начальную точку" тегом, чтобы вы могли восстановить эту информацию механически позже. Но это часто является признаком того, что вы делаете что-то неправильно в первую очередь - или, по крайней мере, ожидаете что-то из git, которое git не предоставляет. См. также сноску 1 ниже.)
Определение ветки (см. также Что именно мы подразумеваем под веткой?)
Ветвь, или, точнее, имя ветки в git - это просто указатель на некоторый конкретный коммит в графе фиксации. Это также относится к другим ссылкам, таким как имена тегов. То, что делает специальную ветвь по сравнению с тегом, например, состоит в том, что когда вы находитесь на ветке и делаете новую фиксацию, git автоматически обновляет имя ветки, чтобы теперь она указывала на новую фиксацию.
Например, предположим, что у нас есть граф фиксации, который выглядит примерно так, где o
узлы (а также отмеченные *
) представляют committ и A
и B
являются именами ветвей:
o <- * <- o <- o <- o <- o <-- A
\
o <- o <- o <-- B
A
и B
каждая точка для наибольшей фиксации ветки ветки или, точнее, структуры данных ветвления, сформированной путем запуска с некоторого фиксации и обработки всех достижимых коммитов, причем каждая фиксация указывает на некоторые parent commit (s).
Если вы используете git checkout B
, так что вы находитесь в ветке B
и делаете новый коммит, новый фиксатор устанавливается с предыдущим концом B
в качестве его (одиночного) родителя и git изменяет идентификатор, хранящийся под именем ветки B
, так что B
указывает на новый отзыв: наибольшая фиксация:
o <- * <- o <- o <- o <- o <-- A
\
o <- o <- o <- o <-- B
Фиксированная метка *
является базой слияния двух советов ветки. Эта фиксация и все предыдущие коммиты находятся в обеих ветвях. 1 База слияния имеет значение, например, для слияния (duh:-)), но также и для таких вещей, как git cherry
и других релизов типа.
Вы упомянули, что ветвь B
иногда сливалась обратно в A
:
git checkout A; git merge B
Это делает новое слияние, которое является фиксацией с двумя родителями 2. Первый родительский элемент является предыдущим концом текущей ветки, то есть предыдущим концом A
, а вторым родителем является именованное коммит, то есть наибольшая фиксация ветки B
. Перерисовывая выше немного более компактно (но добавляя еще немного -
, чтобы линия o
улучшалась), мы начинаем с:
o--*--o--o--o--o <-- A
\
o---o--o---o <-- B
и в итоге:
o--*--o--o--o--o--o <-- A
\ /
o---o--o---* <-- B
Переместим *
в новую базу слияния A
и B
(которая на самом деле является концом B
). Предположительно, мы добавим еще несколько коммитов в B
и, возможно, сможем объединить несколько раз:
...---o--...--o--o <-- A
/ /
...-o--...--*--o--o <-- B
Что git делает по умолчанию с git merge <thing>
Чтобы выполнить слияние, вы проверяете какую-либо ветвь (A
в этих случаях), а затем запустите git merge
и укажите хотя бы один аргумент, обычно другое имя ветки, например B
. Команда слияния начинается с включения имени в идентификатор фиксации. Имя ветки превращается в идентификатор наибольшей фиксации подсказки на ветке.
Затем git merge
находит базу слияния. Это те коммиты, которые мы маркировали с помощью *
. Техническое определение базы слияния является самым низким общим предком в графе фиксации (а в некоторых случаях может быть более одного), но мы просто перейдем к "фиксации помечены *
" здесь для простоты.
Наконец, для обычных слияний git выполняется две команды git diff
. 3 Первый git diff
сравнивает commit *
с фиксацией HEAD
, то есть кончик текущая ветвь. Второй diff сравнивает commit *
с аргументом commit, то есть кончиком другой ветки (вы можете назвать конкретную фиксацию, и она не должна быть концом ветки, но в нашем случае мы объединяем B
в A
, поэтому мы получим те две концы с кончиками ветвей).
Когда git находит какой-либо файл, модифицированный по сравнению с версией слияния, в обеих ветвях, git пытается объединить эти изменения в полу-умном (но не очень умном) способе: если оба изменения добавляют тот же текст в том же регионе, git хранит одну копию сложения. Если оба изменения удаляют один и тот же текст в том же регионе, git удаляет этот текст только один раз. Если оба изменения изменяют текст в том же регионе, вы получаете конфликт, если точно не совпадают изменения (тогда вы получаете одну копию модификаций). Если одна сторона делает одно изменение, а другая сторона делает другое изменение, но изменения, похоже, не перекрываются, git принимает оба изменения. В этом суть трехстороннего слияния.
Наконец, если все идет хорошо, git создает новую фиксацию, в которой есть два (или более, как мы уже отмечали в сноске 2) родителя. Рабочее дерево, связанное с этим новым фиксацией, представляет собой git, когда оно выполнило свое трехстороннее слияние.
Альтернативные стратегии слияния
В то время как git merge
стратегия по умолчанию recursive
имеет -X
параметры ours
и theirs
, они не делают то, что мы хотим здесь. Они просто говорят, что в случае конфликта git должен автоматически разрешить этот конфликт, выбрав "наше изменение" (-X ours
) или "их изменение" (-X theirs
).
Команда merge
имеет другую стратегию полностью, -s ours
: в ней говорится, что вместо того, чтобы различать базу слияния с двумя коммитами, просто используйте наше исходное дерево. Другими словами, если мы находимся на ветке A
, и мы запускаем git merge -s ours B
, git сделает новое слияние с вторым родителем, являющимся концом ветки B
, но исходное дерево, соответствующее версии в предыдущий кончик ветки A
. То есть код для нового коммита будет точно соответствовать коду для его родителя.
Как указано в этом другом ответе, существует несколько способов заставить git эффективно внедрить -s theirs
. Я думаю, что проще всего объяснить эту последовательность:
git checkout A
git merge --no-commit -s ours B
git rm -rf . # make sure you are at the top level!
git checkout B -- .
git commit
Первый шаг - убедиться, что мы находимся на ветке A
, как обычно. Второй - запустить слияние, но не допустить результата (--no-commit
). Чтобы упростить слияние для git - это не нужно, это просто делает вещи более быстрыми и более тихими - мы используем -s ours
, чтобы git мог полностью пропустить шаги diff, и мы избегаем всех конфликтов конфликтов слияния.
На этом этапе мы добираемся до мяса трюка. Сначала мы удаляем весь результат слияния, так как он фактически бесполезен: мы не хотим, чтобы дерево работы было получено от команды tip A
, а от вершины B
. Затем мы проверяем каждый файл с кончика B
, делая его готовым к фиксации.
Наконец, мы завершаем новое слияние, которое имеет в качестве первого родителя старый кончик A
и как его второй родитель - кончик B
, но имеет дерево из commit B
.
Если график перед фиксацией был:
...---o--...--o--o <-- A
/ /
...-o--...--*--o--o <-- B
то новый график теперь:
...---o--...--o--o--o <-- A
/ / /
...-o--...--o--o--* <-- B
Новая база слияния является концом B
, как обычно, и с точки зрения графика фиксации это слияние выглядит точно так же, как и любое другое слияние. Что необычно в том, что исходное дерево для нового слияния на кончике A
точно совпадает с деревом источника для кончика B
.
1 В этом конкретном случае, поскольку две ветки остаются независимыми (никогда не были объединены), это также, вероятно, точка, в которой была создана одна из двух ветвей (или, возможно, даже там, где они были созданы), хотя вы не можете доказать, что на данный момент (потому что кто-то мог использовать git reset
или различные другие трюки, чтобы перемещать метки ветвей в прошлом). Однако, как только мы начинаем слияние, база слияния, очевидно, уже не является отправной точкой, и разумная стартовая точка становится сложнее найти.
2Технически, объединение слиянием - это любое совершение с двумя или более родителями. git звонки сливаются с более чем двумя родителями "осьминог сливается". По моему опыту, они не распространены, кроме как в репозитории git для самого git, и, в конце концов, они достигают того же, что и несколько обычных двух родительских слияний.
3 Диффы обычно выполняются внутри до некоторой степени, а не выполняются фактические команды. Это позволяет много коротких оптимизаций. Это также означает, что если вы пишете пользовательский драйвер слияния, этот пользовательский драйвер слияния не запускается, если git не обнаружит, что файл изменен в обоих diff. Если он изменен только в одном из двух различий, объединение по умолчанию просто принимает измененный.