Найти точку ветвления с помощью Git?
У меня есть хранилище с ветками master и A и множеством действий по слиянию между ними. Как я могу найти коммит в моем репозитории, когда ветка A была создана на основе master?
Мой репозиторий в основном выглядит так:
-- X -- A -- B -- C -- D -- F (master)
\ / \ /
\ / \ /
G -- H -- I -- J (branch A)
Я ищу ревизию А, которая не то, что git merge-base (--all)
находит.
27 ответов
Я искал то же самое, и я нашел этот вопрос. Спасибо, что спросили!
Тем не менее, я обнаружил, что ответы, которые я вижу здесь, кажется, не совсем дают ответ, который вы просили (или что я искал) - они, кажется, дают G
совершать вместо A
совершить.
Итак, я создал следующее дерево (буквы назначены в хронологическом порядке), чтобы я мог проверить вещи:
A - B - D - F - G <- "master" branch (at G)
\ \ /
C - E --' <- "topic" branch (still at E)
Это выглядит немного иначе, чем у вас, потому что я хотел убедиться, что получил (ссылаясь на этот график, а не ваш) B, но не A (и не D или E). Вот письма, прикрепленные к префиксам SHA и сообщениям о коммитах (мой репозиторий можно клонировать отсюда, если это кому-то интересно):
G: a9546a2 merge from topic back to master
F: e7c863d commit on master after master was merged to topic
E: 648ca35 merging master onto topic
D: 37ad159 post-branch commit on master
C: 132ee2a first commit on topic branch
B: 6aafd7f second commit on master before branching
A: 4112403 initial commit on master
Итак, цель: найти Б. Вот три способа, которые я нашел после небольшого переделывания:
1. визуально с гитком:
Вы должны визуально увидеть такое дерево (если смотреть с мастера):
или здесь (если смотреть из темы):
в обоих случаях я выбрал коммит, который B
в моем графике. После того, как вы щелкнете по нему, его полный SHA будет представлен в поле ввода текста чуть ниже графика.
2. визуально, но из терминала:
git log --graph --oneline --all
(Изменить / примечание: добавление --decorate
также может быть интересным; он добавляет указание на имена ветвей, теги и т. д. Не добавляя это в приведенную выше командную строку, поскольку приведенные ниже выходные данные не отражают его использование.)
который показывает (при условии git config --global color.ui auto
):
Или прямым текстом:
* a9546a2 объединить из темы обратно в мастер |\ | * 648ca35 слияние мастер на тему | |\ | * | 132ee2a первый коммит по теме ветка * | | e7c863d коммит на master после слияния master с темой | |/ |/| * | 37ad159 постфилиальный коммит на мастере |/ * 6aafd7f второй коммит на master перед ветвлением * 4112403 начальная фиксация на мастере
в любом случае мы видим коммит 6aafd7f как самую низкую общую точку, т.е. B
на моем графике, или A
в твоем.
3. С магией раковины:
Вы не указываете в своем вопросе, хотите ли вы что-то подобное выше, или единственную команду, которая просто даст вам одну ревизию, и ничего больше. Ну, вот последнее:
diff -u <(git rev-list --first-parent topic) \
<(git rev-list --first-parent master) | \
sed -ne 's/^ //p' | head -1
6aafd7ff98017c816033df18395c5c1e7829960d
Который вы также можете вставить в ваш ~/.gitconfig как (примечание: конечный штрих важен; спасибо Brian White за то, что он обратил на это внимание):
[alias]
oldest-ancestor = !zsh -c 'diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne \"s/^ //p\" | head -1' -
Что можно сделать с помощью следующей (свернутой в кавычки) командной строки:
git config --global alias.oldest-ancestor '!zsh -c '\''diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne "s/^ //p" | head -1'\'' -'
Замечания: zsh
так же легко мог быть bash
, но sh
не будет работать - <()
синтаксис не существует в ванили sh
, (Еще раз спасибо, @conny, за то, что сообщили мне об этом в комментарии к другому ответу на этой странице!)
Примечание: альтернативная версия выше:
Спасибо liori за то, что он указал, что вышеупомянутое может упасть при сравнении идентичных ветвей, и придумать альтернативную форму diff, которая удаляет форму sed из смеси, и делает это "более безопасным" (то есть возвращает результат (а именно, самый последний коммит), даже если вы сравниваете мастер с мастером):
В виде строки.git-config:
[alias]
oldest-ancestor = !zsh -c 'diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1' -
Из оболочки:
git config --global alias.oldest-ancestor '!zsh -c '\''diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1'\'' -'
Итак, в моем дереве тестов (которое было недоступно некоторое время, извините; оно вернулось), теперь оно работает как для мастера, так и для темы (с фиксацией G и B соответственно). Еще раз спасибо, Лиори, за альтернативную форму.
Итак, вот что я [и liori] придумали. Кажется, это работает для меня. Это также допускает дополнительную пару псевдонимов, которые могут оказаться полезными:
git config --global alias.branchdiff '!sh -c "git diff `git oldest-ancestor`.."'
git config --global alias.branchlog '!sh -c "git log `git oldest-ancestor`.."'
Удачи!
Вы можете искать git merge-base
:
git merge-base находит наилучшего общего предка (ей) между двумя коммитами для использования в трехстороннем слиянии. Один общий предок лучше другого общего предка, если последний является предком первого. Общий предок, который не имеет лучшего общего предка, является лучшим общим предком, то есть базой слияния. Обратите внимание, что для пары коммитов может быть несколько баз слияния.
Я использовал git rev-list
для такого рода вещей. Например, (обратите внимание на 3 точки)
$ git rev-list --boundary branch-a...master | grep "^-" | cut -c2-
выплюнет точку ветвления. Теперь это не идеально; так как вы пару раз слили мастер в ветвь А, это разделит пару возможных точек ветвления (в основном, исходную точку ветвления, а затем каждую точку, в которой вы слили мастер в ветвь А). Однако это должно как минимум сузить возможности.
Я добавил эту команду в мои псевдонимы в ~/.gitconfig
как:
[alias]
diverges = !sh -c 'git rev-list --boundary $1...$2 | grep "^-" | cut -c2-'
так что я могу назвать это как:
$ git diverges branch-a master
Если вам нравятся краткие команды,
git rev-list $ (git rev-list --first-parent ^ имя_ветки master | tail -n1)^^!
Вот объяснение.
Следующая команда дает вам список всех коммитов в master, которые произошли после того, как было создано branch_name
git rev-list --first-parent ^ master_name master
Поскольку вы заботитесь только о самых ранних из этих коммитов, вам нужна последняя строка вывода:
git rev-list ^ имя_расширя --first-parent master | хвост -n1
Родитель самого раннего коммита, который не является предком "branch_name", по определению находится в "branch_name" и находится в "master", поскольку он является предком чего-то в "master". Итак, у вас есть самый ранний коммит, который есть в обеих ветках.
Команда
git rev-list commit^^!
это просто способ показать ссылку на родительский коммит. Вы могли бы использовать
git log -1 commit^
или что угодно.
PS: я не согласен с аргументом, что порядок предков не имеет значения. Это зависит от того, что вы хотите. Например, в этом случае
_C1___C2_______ мастер \ \_XXXXX_ ветвь A (X обозначают произвольные переходы между мастером и A) \_____/ филиал B
имеет смысл выводить C2 как "ветвящийся" коммит. Это когда разработчик разветвился от "мастера". Когда он разветвился, ветвь "Б" даже не слилась в его ветке! Вот что дает решение в этом посте.
Если вам нужен последний коммит C, такой, что все пути от источника до последнего коммита в ветви "A" проходят через C, то вы хотите игнорировать порядок предков. Это чисто топология и дает вам представление о том, когда у вас две версии кода работают одновременно. Вот тогда вы бы использовали подходы, основанные на слиянии, и в моем примере он вернет C1.
Учитывая, что так много ответов в этой ветке не дают ответа на вопрос, который был задан, вот краткое изложение результатов каждого решения, а также сценарий, который я использовал для репликации репозитория, приведенного в вопросе.
Журнал
Создавая репозиторий с заданной структурой, мы получаем журнал git:
$ git --no-pager log --graph --oneline --all --decorate
* b80b645 (HEAD, branch_A) J - Work in branch_A branch
| * 3bd4054 (master) F - Merge branch_A into branch master
| |\
| |/
|/|
* | a06711b I - Merge master into branch_A
|\ \
* | | bcad6a3 H - Work in branch_A
| | * b46632a D - Work in branch master
| |/
| * 413851d C - Merge branch_A into branch master
| |\
| |/
|/|
* | 6e343aa G - Work in branch_A
| * 89655bb B - Work in branch master
|/
* 74c6405 (tag: branch_A_tag) A - Work in branch master
* 7a1c939 X - Work in branch master
Мое единственное добавление, это тег, который делает его явным о точке, в которой мы создали ветвь и, следовательно, о коммите, который мы хотим найти.
Решение, которое работает
Единственное решение, которое работает, это то, которое предоставлено lindes, правильно возвращает A
:
$ diff -u <(git rev-list --first-parent branch_A) \
<(git rev-list --first-parent master) | \
sed -ne 's/^ //p' | head -1
74c6405d17e319bd0c07c690ed876d65d89618d5
Как указывает Чарльз Бейли, это решение очень хрупкое.
если ты branch_A
в master
а затем объединить master
в branch_A
без промежуточных коммитов решение Линдеса дает вам только самое первое расхождение.
Это означает, что для моего рабочего процесса, я думаю, мне придется придерживаться маркировки точки ветвления длинных ветвей, так как я не могу гарантировать, что они могут быть надежно найдены позже.
Это действительно все сводится к git
отсутствие чего hg
звонки по названным веткам. Блоггер jhw называет эти родословные и семьи в своей статье " Почему я люблю Mercurial больше, чем Git", и в своей последующей статье " Больше о Mercurial против Git" (с графиками!). Я бы порекомендовал людям прочитать их, чтобы понять, почему некоторые ртутные новообращенные пропускают без именных веток git
,
Решения, которые не работают
Решение, предоставленное Mipadi, возвращает два ответа, I
а также C
:
$ git rev-list --boundary branch_A...master | grep ^- | cut -c2-
a06711b55cf7275e8c3c843748daaa0aa75aef54
413851dfecab2718a3692a4bba13b50b81e36afc
Решение, предоставленное Грегом Хьюгиллом, вернулось I
$ git merge-base master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54
$ git merge-base --all master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54
Решение, предоставленное Карлом, возвращает X
:
$ diff -u <(git log --pretty=oneline branch_A) \
<(git log --pretty=oneline master) | \
tail -1 | cut -c 2-42
7a1c939ec325515acfccb79040b2e4e1c3e7bbe5
Сценарий
mkdir $1
cd $1
git init
git commit --allow-empty -m "X - Work in branch master"
git commit --allow-empty -m "A - Work in branch master"
git branch branch_A
git tag branch_A_tag -m "Tag branch point of branch_A"
git commit --allow-empty -m "B - Work in branch master"
git checkout branch_A
git commit --allow-empty -m "G - Work in branch_A"
git checkout master
git merge branch_A -m "C - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "H - Work in branch_A"
git merge master -m "I - Merge master into branch_A"
git checkout master
git commit --allow-empty -m "D - Work in branch master"
git merge branch_A -m "F - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "J - Work in branch_A branch"
Я сомневаюсь, что версия git имеет большое значение для этого, но:
$ git --version
git version 1.7.1
Спасибо Чарльзу Бэйли за то, что он показал мне более компактный способ создания сценария репозитория.
В общем, это невозможно. В истории ветвей ветвление и слияние до того, как именованная ветвь была разветвленной, а промежуточная ветвь двух именованных ветвей выглядит одинаково.
В git ветки - это просто текущие названия подсказок разделов истории. У них действительно нет сильной идентичности.
Обычно это не является большой проблемой, так как база слияния (см. Ответ Грега Хьюгилла) двух коммитов, как правило, гораздо более полезна, давая самый последний коммит, которым поделились две ветви.
Решение, полагающееся на порядок родителей коммитов, очевидно, не будет работать в ситуациях, когда ветка полностью интегрирована в какой-то момент в истории ветки.
git commit --allow-empty -m root # actual branch commit
git checkout -b branch_A
git commit --allow-empty -m "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git merge --ff-only master
git commit --allow-empty -m "More work on branch_A"
git checkout master
git commit --allow-empty -m "More work on master"
Этот метод также не работает, если объединение интеграции было выполнено с обращенными родителями (например, временная ветвь использовалась для выполнения тестового слияния с мастером, а затем быстро пересылалась в ветвь функций для дальнейшего развития).
git commit --allow-empty -m root # actual branch point
git checkout -b branch_A
git commit --allow-empty -m "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git commit --allow-empty -m "More work on branch_A"
git checkout -b tmp-branch master
git merge -m "Merge branch_A into tmp-branch (master copy)" branch_A
git checkout branch_A
git merge --ff-only tmp-branch
git branch -d tmp-branch
git checkout master
git commit --allow-empty -m "More work on master"
Git 2.36 предлагает более простую команду:
git rev-parse $(git rev-list --exclude-first-parent-only A ^master| head -1)^
-
git rev-list --exclude-first-parent-only A ^master
дает тебеG -- H -- I -- J
-
head -1
дает вам G -
git rev-parse G^
дает вам своего первого родителя:A
С Git 2.36 (второй квартал 2022 г.) "git log
" (мужчина ) и его друзья узнали о возможности распространять бит UNINTERESTING вниз только по цепочке первых родителей, точно так же, как опция показывает коммиты, в которых отсутствует бит UNINTERESTING, только по цепочке первых родителей.
См. (11 января 2022 г.) Джерри Чжана (
jerry-skydio
).
(Объединено Junio C Hamano --
gitster
-- в коммите 708cbef, 17 февраля 2022 г.)
коммит 9d505b7
git-rev-list
: добавить флаг --exclude-first-parent-onlyПодписал: Джерри Чжан
Полезно знать, когда ветка впервые в истории отклонилась от какой-либо ветки интеграции, чтобы иметь возможность перечислить локальные изменения пользователя.
Однако эти локальные изменения могут включать произвольные слияния, поэтому при нахождении точки расхождения необходимо игнорировать эту структуру слияния.Для этого научите «
rev-list
«семья для принятия», которая ограничивает обход исключенных коммитов только переходом по первым родительским ссылкам .-A-----E-F-G--main \ / / B-C-D--topic
В этом примере цель состоит в том, чтобы вернуть множество
{B, C, D
} , который представляет ветку темы, которая была объединена в ветку.
git rev-list topic ^main
( man ) в конечном итоге не вернет никаких коммитов с момента исключенияmain
в конечном итоге пройдёт коммиты наtopic
также.
git rev-list --exclude-first-parent-only topic ^main
(человек ) однако вернется{B, C, D}
по желанию.Добавьте документы для нового флага и уточните документ для
--first-parent
чтобы указать, что это относится только к обходу набора включенных коммитов.
теперь включает в свою справочную страницу :
При поиске коммитов для включения следуйте только первому родительскому коммиту, увидев коммит слияния.
Этот параметр может дать лучший обзор при просмотре эволюции конкретной тематической ветки, потому что слияния в тематической ветке, как правило, связаны только с адаптацией к обновленному восходящему потоку время от времени, и этот параметр позволяет вам игнорировать отдельные коммиты, внесенные в ваша история таким слиянием.
rev-list-options
теперь включает в свою справочную страницу :
--exclude-first-parent-only
При поиске коммитов для исключения (со знаком «{знак вставки}») следуйте только первому родительскому коммиту, увидев коммит слияния.
Это можно использовать для поиска набора изменений в тематической ветви от точки, в которой она расходится с удаленной ветвью, учитывая, что произвольные слияния могут быть действительными изменениями тематической ветви.
Как насчет чего-то вроде
git log --pretty=oneline master > 1
git log --pretty=oneline branch_A > 2
git rev-parse `diff 1 2 | tail -1 | cut -c 3-42`^
Простой способ упростить просмотр точки ветвления в git log --graph
использовать опцию --first-parent
.
Например, возьмите репо из принятого ответа:
$ git log --all --oneline --decorate --graph
* a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
|\
| * 648ca35 (origin/topic) merging master onto topic
| |\
| * | 132ee2a first commit on topic branch
* | | e7c863d commit on master after master was merged to topic
| |/
|/|
* | 37ad159 post-branch commit on master
|/
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master
Теперь добавьте --first-parent
:
$ git log --all --oneline --decorate --graph --first-parent
* a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
| * 648ca35 (origin/topic) merging master onto topic
| * 132ee2a first commit on topic branch
* | e7c863d commit on master after master was merged to topic
* | 37ad159 post-branch commit on master
|/
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master
Так легче!
Обратите внимание, если в репо много ветвей, вы захотите указать две сравниваемые ветки вместо использования --all
:
$ git log --decorate --oneline --graph --first-parent master origin/topic
Конечно, я что-то упускаю, но IMO, все проблемы, описанные выше, вызваны тем, что мы всегда пытаемся найти точку ветвления, уходящую в историю, и это вызывает всевозможные проблемы из-за доступных комбинаций слияния.
Вместо этого я следовал другому подходу, основанному на том факте, что обе ветви имеют большую историю, точно вся история до ветвления на 100% одинакова, поэтому вместо того, чтобы вернуться назад, мое предложение о продвижении вперед (с 1-го commit), ищем 1-ую разницу в обеих ветках. Точка ветвления будет просто родителем первого найденного различия.
На практике:
#!/bin/bash
diff <( git rev-list "${1:-master}" --reverse --topo-order ) \
<( git rev-list "${2:-HEAD}" --reverse --topo-order) \
--unified=1 | sed -ne 's/^ //p' | head -1
И это решает все мои обычные дела. Конечно, есть пограничные, которые не покрыты, но... чао:-)
После долгих исследований и дискуссий становится ясно, что не существует волшебной пули, которая бы работала во всех ситуациях, по крайней мере, в текущей версии Git.
Вот почему я написал пару патчей, которые добавляют концепцию tail
ветка. Каждый раз, когда создается ветвь, создается указатель на исходную точку, tail
ссылка Эта ссылка обновляется каждый раз, когда ветка перебазируется.
Чтобы узнать точку ветвления ветки d evel, все, что вам нужно сделать, это использовать devel@{tail}
, вот и все.
Кажется, я получаю некоторую радость от
git rev-list branch...master
Последняя строка, которую вы получите, это первый коммит на ветке, так что это вопрос получения родителя этого. Так
git rev-list -1 `git rev-list branch...master | tail -1`^
Кажется, работает для меня и не требует различий и так далее (что полезно, поскольку у нас нет этой версии различий)
Исправление: это не работает, если вы находитесь в основной ветке, но я делаю это в скрипте, так что это не проблема
Иногда это фактически невозможно (за некоторыми исключениями, когда вам, возможно, повезет иметь дополнительные данные), и решения здесь не сработают.
Git не сохраняет историю ссылок (которая включает ветки). Он сохраняет только текущую позицию для каждой ветви (главы). Это означает, что вы можете потерять некоторую историю веток в git со временем. Каждый раз, когда вы выполняете ветку, например, сразу теряется, какая ветка была оригинальной. Все, что делает ветка, это:
git checkout branch1 # refs/branch1 -> commit1
git checkout -b branch2 # branch2 -> commit1
Вы можете предположить, что первым подтвержден ответвление. Это имеет место, но это не всегда так. Ничто не мешает вам совершить коммит в первую ветку после вышеуказанной операции. Кроме того, временные метки git не гарантируют надежность. Только когда вы посвятите себя обоим, они действительно станут структурно ветвями.
В то время как в диаграммах мы склонны к нумерации коммитов концептуально, git не имеет реального стабильного понятия последовательности, когда ветки дерева коммитов. В этом случае вы можете предположить, что числа (указывающие порядок) определяются меткой времени (было бы интересно увидеть, как пользовательский интерфейс git обрабатывает вещи, когда вы устанавливаете все метки времени на одно и то же).
Это то, что человек ожидает концептуально:
After branch:
C1 (B1)
/
-
\
C1 (B2)
After first commit:
C1 (B1)
/
-
\
C1 - C2 (B2)
Вот что вы на самом деле получаете:
After branch:
- C1 (B1) (B2)
After first commit (human):
- C1 (B1)
\
C2 (B2)
After first commit (real):
- C1 (B1) - C2 (B2)
Вы могли бы предположить, что B1 является исходной веткой, но она может просто заразиться мертвой веткой (кто-то сделал checkout -b, но никогда не выполнял ее). До тех пор, пока вы не подтвердите обе эти возможности, вы не получите легитимную структуру ветвей в git:
Either:
/ - C2 (B1)
-- C1
\ - C3 (B2)
Or:
/ - C3 (B1)
-- C1
\ - C2 (B2)
Вы всегда знаете, что C1 предшествовал C2 и C3, но вы никогда не могли достоверно знать, предшествовал ли C2 раньше C3 или C3 до C2 (потому что вы можете установить время на своей рабочей станции, например, что угодно). B1 и B2 также вводят в заблуждение, так как вы не можете знать, какая ветвь появилась первой. Вы можете сделать очень хорошее и, как правило, точное предположение во многих случаях. Это немного похоже на гоночную трассу. При прочих равных с машинами, вы можете предположить, что машина, которая находится на круге позади, начала круг позади. У нас также есть соглашения, которые очень надежны, например, master почти всегда будет отображать ветки с наибольшим временем жизни, хотя, к сожалению, я видел случаи, когда даже это не так.
Пример, приведенный здесь, является примером сохранения истории:
Human:
- X - A - B - C - D - F (B1)
\ / \ /
G - H ----- I - J (B2)
Real:
B ----- C - D - F (B1)
/ / \ /
- X - A / \ /
\ / \ /
G - H ----- I - J (B2)
Реальное здесь также вводит в заблуждение, потому что мы, люди, читаем его слева направо, от корня к листу (ссылка). Git этого не делает. Где мы делаем (A->B) в наших головах git делает (A<-B или B->A). Это читает это от ссылки до корня. Реферы могут быть где угодно, но имеют тенденцию быть листами, по крайней мере, для активных веток. Ссылка указывает на коммит, а коммиты содержат только то же самое для своих родителей, а не для своих детей. Когда коммит является коммитом слияния, у него будет более одного родителя. Первый родитель всегда является исходным коммитом, который был объединен. Другие родители всегда коммиты, которые были объединены в первоначальный коммит.
Paths:
F->(D->(C->(B->(A->X)),(H->(G->(A->X))))),(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))
J->(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))
Это не очень эффективное представление, скорее, выражение всех путей, которые git может взять из каждого ref (B1 и B2).
Внутренняя память Git выглядит примерно так (не то, что родительский объект A появляется дважды):
F->D,I | D->C | C->B,H | B->A | A->X | J->I | I->H,C | H->G | G->A
Если вы сбросите raw git commit, вы увидите ноль или более родительских полей. Если есть ноль, это означает, что родитель отсутствует, а фиксация является корнем (у вас может быть несколько корней). Если он есть, это означает, что слияния не было, и это не корневой коммит. Если их несколько, это означает, что фиксация является результатом слияния, и все родители после первого являются коммитами слияния.
Paths simplified:
F->(D->C),I | J->I | I->H,C | C->(B->A),H | H->(G->A) | A->X
Paths first parents only:
F->(D->(C->(B->(A->X)))) | F->D->C->B->A->X
J->(I->(H->(G->(A->X))) | J->I->H->G->A->X
Or:
F->D->C | J->I | I->H | C->B->A | H->G->A | A->X
Paths first parents only simplified:
F->D->C->B->A | J->I->->G->A | A->X
Topological:
- X - A - B - C - D - F (B1)
\
G - H - I - J (B2)
Когда оба поразят А, их цепь будет одинаковой, до этого их цепь будет совершенно другой. Первый коммит, который есть у двух других коммитов, является общим предком и откуда они расходятся. здесь может быть некоторая путаница между терминами commit, branch и ref. На самом деле вы можете объединить коммит. Это то, что действительно делает слияние. Ссылка просто указывает на коммит, а ветвь - это не что иное, как ссылка в папке.git / refs /head, местоположение папки определяет, что ссылка является ветвью, а не чем-то другим, например тегом.
Когда вы теряете историю, слияние будет выполняться в зависимости от обстоятельств.
Рассматривать:
/ - B (B1)
- A
\ - C (B2)
В этом случае объединение в любом направлении создаст новый коммит с первым родителем в качестве коммита, на который указывает текущая извлеченная ветвь, и вторым родителем в качестве коммита в конце ветви, которую вы слили в текущую ветвь. Он должен создать новый коммит, так как обе ветви имеют изменения со времени их общего предка, которые должны быть объединены.
/ - B - D (B1)
- A /
\ --- C (B2)
В этот момент D (B1) теперь имеет оба набора изменений из обеих ветвей (себя и B2). Однако вторая ветвь не имеет изменений от B1. Если вы объединяете изменения из B1 в B2, чтобы они были синхронизированы, то вы можете ожидать что-то, похожее на это (вы можете заставить git merge сделать это, как это, однако с --no-ff):
Expected:
/ - B - D (B1)
- A / \
\ --- C - E (B2)
Reality:
/ - B - D (B1) (B2)
- A /
\ --- C
Вы получите это, даже если у B1 есть дополнительные коммиты. Пока в B2 нет изменений, которых нет в B1, две ветви будут объединены. Он выполняет ускоренную перемотку вперед, которая похожа на перебазирование (перебазирование также использует или линеаризует историю), за исключением того, что в отличие от перебазирования, поскольку только одна ветвь имеет набор изменений, ей не нужно применять набор изменений из одной ветви поверх этой из другой.
From:
/ - B - D - E (B1)
- A /
\ --- C (B2)
To:
/ - B - D - E (B1) (B2)
- A /
\ --- C
Если вы прекратите работу над B1, то в целом все хорошо для сохранения истории в долгосрочной перспективе. Обычно продвигается только B1 (который может быть главным), поэтому местоположение B2 в истории B2 успешно отражает точку, в которой оно было объединено с B1. Это то, что git ожидает от вас, чтобы ветвь B от A, затем вы можете объединить A в B столько, сколько захотите, по мере накопления изменений, однако при объединении B обратно в A не ожидается, что вы будете работать над B и далее., Если вы продолжаете работать над своей веткой после быстрой перемотки вперед, объединяя ее с веткой, над которой вы работали, то каждый раз стираете предыдущую историю B. Вы действительно создаете новую ветку каждый раз после быстрой перемотки в исходный код, а затем в ответ. Когда вы выполняете ускоренную перемотку, вы получаете множество ветвей / слияний, которые вы можете видеть в истории и структуре, но без возможности определить, как называлось название этой ветви, или если то, что выглядит как две отдельные ветви, действительно является одной и той же веткой.,
0 1 2 3 4 (B1)
/-\ /-\ /-\ /-\ /
---- - - - -
\-/ \-/ \-/ \-/ \
5 6 7 8 9 (B2)
От 1 до 3 и от 5 до 8 являются структурными ветвями, которые появляются, если вы проследите историю 4 или 9. В git нет никакого способа узнать, к какой из этих неназванных и не связанных структурных ветвей принадлежит именованная и ссылочная ветви как конец структуры. Из этого рисунка можно предположить, что от 0 до 4 принадлежит B1, а от 4 до 9 принадлежит B2, но, кроме 4 и 9, не мог знать, какая ветвь принадлежит какой ветке, я просто нарисовал его так, чтобы иллюзия этого. 0 может принадлежать B2, а 5 может принадлежать B1. В этом случае существует 16 различных вариантов, к которым именованной ветви может принадлежать каждая из структурных ветвей. Это предполагает, что ни одна из этих структурных ветвей не пришла из удаленной ветви или в результате объединения ветви в себя при извлечении из мастера (одно и то же имя ветви в двух репозиториях фактически влияет на две ветви, отдельный репозиторий похож на ветвление всех ветвей),
Есть много стратегий мерзавца, которые работают вокруг этого. Вы можете заставить git merge никогда не переходить вперед и всегда создавать ветку слияния. Ужасный способ сохранить историю ветвей - использовать теги и / или ветви (теги действительно рекомендуется) в соответствии с некоторыми соглашениями по вашему выбору. Я действительно не рекомендовал бы фиктивный пустой коммит в ветке, в которую вы сливаетесь. Очень распространенное соглашение - не объединяться в ветку интеграции, пока вы не захотите по-настоящему закрыть свою ветку. Это практика, которой люди должны придерживаться, иначе вы работаете над тем, чтобы иметь ветви. Однако в реальном мире идеал не всегда практичен, а делать правильные вещи не всегда жизнеспособно. Если то, что вы делаете в ветке, изолированно, это может работать, но в противном случае вы можете оказаться в ситуации, когда несколько разработчиков работают над чем-то, что им нужно быстро поделиться своими изменениями (в идеале вы, возможно, действительно хотите работать над одной веткой, но Не все ситуации подходят для этого, и вам, как правило, нужно избегать двух человек, работающих на ветке).
Недавно мне также пришлось решить эту проблему, и в итоге я написал для этого скрипт на Ruby: https://github.com/vaneyckt/git-find-branching-point
Следующая команда покажет SHA1 коммита A
git merge-base --fork-point A
Вот улучшенная версия моего предыдущего ответа предыдущего ответа. Он полагается на сообщения коммитов от слияний, чтобы найти, где ветвь была впервые создана.
Он работает на всех репозиториях, упомянутых здесь, и я даже обратился к некоторым хитрым, которые появились в списке рассылки. Я также написал тесты для этого.
find_merge ()
{
local selection extra
test "$2" && extra=" into $2"
git rev-list --min-parents=2 --grep="Merge branch '$1'$extra" --topo-order ${3:---all} | tail -1
}
branch_point ()
{
local first_merge second_merge merge
first_merge=$(find_merge $1 "" "$1 $2")
second_merge=$(find_merge $2 $1 $first_merge)
merge=${second_merge:-$first_merge}
if [ "$merge" ]; then
git merge-base $merge^1 $merge^2
else
git merge-base $1 $2
fi
}
Не совсем решение вопроса, но я подумал, что стоит отметить подход, который я использую, когда у меня долгоживущая ветвь:
В то же время я создаю ветку, я также создаю тег с тем же именем, но с -init
суффикс, например feature-branch
а также feature-branch-init
,
(Это довольно странно, что это такой сложный вопрос!)
Чтобы найти коммиты из точки ветвления, вы можете использовать это.
git log --ancestry-path master..topicbranch
Похоже, использование reflog решает эту проблему.
git reflog <branchname>
показывает все коммиты ветки, включая создание ветки.
Это из ветки, в которой было 2 коммита до того, как она была объединена с мастером.
git reflog june-browser-updates
b898b15 (origin/june-browser-updates, june-browser-updates) june-browser-updates@{0}: commit: sorted cve.csv
467ae0e june-browser-updates@{1}: commit: browser updates and cve additions
d6a37fb june-browser-updates@{2}: branch: Created from HEAD
Решение состоит в том, чтобы найти первое расхождение, а затем взять родительское. С помощью zsh это можно сделать следующим образом (РЕДАКТИРОВАНИЕ: по сравнению с моим первым ответом я добавил недостающее; я забыл об этом, когда проводил тесты в репозитории, где все коммиты имели одинаковую дату, сгенерированную сценарием):
git rev-parse "$(diff <(git rev-list --topo-order --reverse master) \
<(git rev-list --topo-order --reverse branch_A) | \
sed -n '2{s/^. //p;q}')^"
The sed
выбирает первый коммит, который появляется только в одной из ветвей. Тогдаgit rev-parse "$(...)^"
выводит своего родителя.
Примечания:
- Это работает независимо от того, было ли оно объединено или нет (в случае слияния глава
branch_A
соответствует последнему коммиту в этой ветке перед слиянием). - В некоторых случаях понятие точки ветвления неоднозначно. Таким образом, человек может не получить того, чего на самом деле хочет. Этот случай также встречается в репозитории GNU MPFR (
4.0
ветвь). - Это не оптимально, поскольку нужно только первое различие, но это, похоже, самое простое решение со стандартными утилитами Unix.
РЕДАКТИРОВАТЬ:
В случае слияния где-либо возможно, что первые фиксации перед слиянием в одной из двух ветвей не являются частью расхождения, видимого при использовании (хотя в некоторых случаях результат все еще может иметь смысл). Поэтому лучше посмотреть на первое расхождение с обеих сторон, т.е.diff
, первая вставка и первое удаление (если доступно).
Другая возможная проблема с приведенным выше решением заключается в том, что в случае фиксации слияния^
выбирает первого родителя, в то время как хотелось бы, чтобы слияния считались симметричными. Так,^@
предпочтительнее, чтобы выбрать обоих родителей.
Наконец, среди возможных точек ветвления, полученных с помощью приведенных выше замечаний, выбирается самая старая: это делается с помощьюgit rev-list --topo-order --reverse --no-walk ... | head -n 1
ниже (обратите внимание, что из-за--reverse
, нельзя использовать-1
или-n 1
какgit rev-list
вариант вместо трубыhead -n 1
).
Итак, вот полное решение, которое можно использовать в виде скрипта, сmaster
иHEAD
по умолчанию для ветвей:
git rev-list --topo-order --reverse --no-walk \
$(diff -u <(git rev-list --topo-order --reverse "${1:-master}") \
<(git rev-list --topo-order --reverse "${2:-HEAD}") | \
perl -e 'while (<>) { /^([-+])([^-+].*)/ and $h{$1} //= "$2^@\n";
last if %h > 1 }
print values %h') | head -n 1
The --topo-order
описание не очень подробное, но, похоже, оно работает так, как и ожидалось, на различных сложных примерах. Но могут быть и более сложные примеры, в которых точка ветвления не определена четко.
Следующее реализует git-эквивалент svn log --stop-on-copy и может также использоваться для поиска источника ветви.
Подход
- Получить голову для всех отраслей
- собрать mergeBase для целевой ветки друг друга
- git.log и итерация
- Остановить первый коммит, который появляется в списке mergeBase
Подобно тому, как все реки текут к морю, все ветви текут к господству, и поэтому мы находим основание слияния между, казалось бы, не связанными ветвями. Когда мы возвращаемся от головы ветви к предкам, мы можем остановиться на первой потенциальной базе слияния, так как в теории это должна быть начальная точка этой ветви.
Заметки
- Я не пробовал этот подход, когда ветви братьев и сестер сливались между собой.
- Я знаю, что должно быть лучшее решение.
подробности: /questions/18169202/chto-takoe-ekvivalent-jgit-dlya-git-merge-base-fork-point-brancha-branchb/18169211#18169211
Похоже, проблема заключается в том, чтобы найти самое последнее, однокомпонентное сокращение между обеими ветвями с одной стороны и самого раннего общего предка с другой (вероятно, начальный коммит репо). Это соответствует моей интуиции о том, что такое точка "разветвления".
Помните, что это совсем не легко вычислить с помощью обычных команд оболочки git, так как git rev-list
- наш самый мощный инструмент - не позволяет нам ограничивать путь, по которому достигается коммит. Ближайший у нас есть git rev-list --boundary
, который может дать нам набор всех коммитов, которые "заблокировали наш путь". (Заметка: git rev-list --ancestry-path
это интересно, но я не знаю, как сделать это полезным здесь.)
Вот сценарий: https://gist.github.com/abortz/d464c88923c520b79e3d. Это относительно просто, но из-за цикла это достаточно сложно, чтобы оправдать суть.
Обратите внимание, что большинство других решений, предлагаемых здесь, не могут работать во всех ситуациях по простой причине: git rev-list --first-parent
не надежен в истории линеаризации, потому что могут быть слияния с любым порядком.
git rev-list --topo-order
с другой стороны, это очень полезно - для прогулочных коммитов в топографическом порядке - но выполнение различий хрупко: для данного графа существует множество возможных топографических упорядочений, поэтому вы зависите от определенной стабильности упорядочений. Тем не менее, решение strongk7, вероятно, работает чертовски хорошо большую часть времени. Однако он медленнее, потому что мне приходится проходить всю историю репо... дважды.:-)
Почему бы не использовать
git log master..enter_your_branch_here --oneline | tail -1
Что дает вам все коммиты, которые есть у ветви А, которых нет у мастера (функция
..
), а также
tail -1
чтобы вернуть последнюю строку вывода, которая найдет вам первую фиксацию указанной ветки (ветка A).
Затем с помощью SHA этого коммита
git log enter_the_sha_here^1 --oneline | head -1
Что дает вам все коммиты до указанного коммита (функция
^1
) а также
head -1
чтобы вернуть первую строку вывода, которая является «одной фиксацией до» самой ранней фиксации в ветке A, также известной как «точка ветвления».
Как одна исполняемая команда:
for COMMIT in $(git log --format=format:%H master..HEAD | tail -1) ; do
git log $COMMIT^1 --oneline | head -1
done
Запустите вышеуказанное из ветки A(функция HEAD)
Простой ответ
Объедините базу с двумя ветвями, теперь вы можете найти общего предка(ов).
git merge-base master branch_a
# => commit_1234
# now do what you want with the commit id
git show commit_1234
# => commit commit_1234
# Author: BenKoshy
# Date: Today
# etc
```
Вы можете использовать следующую команду для возврата самого старого коммита в branch_a, который недоступен из master:
git rev-list branch_a ^master | tail -1
Возможно, с дополнительной проверкой работоспособности, что родитель этого коммита действительно доступен от master...
Вы можете проверить список журналов ветви A, чтобы найти, из какого коммита он был создан, а также полную историю коммитов, на которые указывает эта ветка. Reflogs находятся в .git/logs
,
Я полагаю, что нашел способ, который имеет дело со всеми угловыми случаями, упомянутыми здесь:
branch=branch_A
merge=$(git rev-list --min-parents=2 --grep="Merge.*$branch" --all | tail -1)
git merge-base $merge^1 $merge^2
Чарльз Бэйли совершенно прав, что решения, основанные на порядке предков, имеют лишь ограниченную ценность; в конце дня вам нужна какая-то запись "этот коммит пришел из ветви X", но такая запись уже существует; по умолчанию "git merge" будет использовать сообщение коммита, такое как "Merge branch 'branch_A' to master", это говорит о том, что все коммиты от второго родителя (commit^2) пришли из 'branch_A' и были объединены с первым parent (commit^1), который является 'master'.
Вооружившись этой информацией, вы можете найти первое слияние 'branch_A' (когда действительно появилось 'branch_A') и найти базу слияния, которая будет точкой ветвления:)
Я попробовал с репозиториями Марка Бута и Чарльза Бейли, и решение работает; как это не могло? Единственный способ, которым это не сработает, - это если вы вручную изменили сообщение о фиксации по умолчанию для слияний, чтобы информация о ветвях действительно терялась.
Для полезности:
[alias]
branch-point = !sh -c 'merge=$(git rev-list --min-parents=2 --grep="Merge.*$1" --all | tail -1) && git merge-base $merge^1 $merge^2'
Тогда вы можете сделатьgit branch-point branch_A
".
Наслаждаться;)