Найти точку ветвления с помощью Git?


455

У меня есть репозиторий с мастером веток и A и много операций слияния между ними. Как я могу найти коммит в моем репозитории, когда ветка A была создана на основе master?

Мой репозиторий в основном выглядит так:

-- X -- A -- B -- C -- D -- F  (master) 
          \     /   \     /
           \   /     \   /
             G -- H -- I -- J  (branch A)

Я ищу ревизию А, которая не git merge-base (--all)находит.


Ответы:


499

Я искал то же самое, и я нашел этот вопрос. Спасибо, что спросили!

Однако я обнаружил, что ответы, которые я вижу здесь, похоже, не совсем дают ответ, который вы просили (или что я искал) - они, похоже, дают 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

Таким образом, цель: найти B . Вот три способа, которые я нашел после небольшой переделки:


1. визуально с гитком:

Вы должны визуально увидеть такое дерево (если смотреть с мастера):

Захват экрана GITK от мастера

или здесь (если смотреть из темы):

захват экрана из темы

в обоих случаях я выбрал коммит Bв моем графике. После того, как вы щелкнете по нему, его полный SHA будет представлен в поле ввода текста чуть ниже графика.


2. визуально, но из терминала:

git log --graph --oneline --all

(Изменить / примечание: добавление --decorate также может быть интересным; оно добавляет указание имен веток, тегов и т. Д. Не добавляя это в приведенную выше командную строку, поскольку приведенные ниже результаты не отражают ее использование.)

который показывает (при условии git config --global color.ui auto):

вывод git log --graph --oneline --all

Или прямым текстом:

* 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 как (примечание: конечная черта важна; спасибо Брайану за то, что вы обратили на это внимание) :

[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 будет работать - синтаксис не существует в vanilla . (Еще раз спасибо, @conny, за то, что сообщили мне об этом в комментарии к другому ответу на этой странице!)<()sh

Примечание: альтернативная версия выше:

Спасибо 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`.."'

Счастливого парения!


5
Благодаря lindes, опция оболочки отлично подходит для ситуаций, когда вы хотите найти точку ветвления ветки длительного обслуживания. Когда вы ищете ревизию, которая в прошлом могла содержать тысячу коммитов, визуальные опции на самом деле не собираются ее сокращать. * 8 ')
Марк Бут

4
В вашем третьем методе вы зависите от того, что контекст покажет первую неизменную строку. Этого не произойдет в определенных крайних случаях или если у вас будут немного другие требования (например, мне нужно, чтобы только одна из историй была --first-parent, и я использую этот метод в сценарии, который иногда может использовать то же самое ветви с обеих сторон). Я нашел , что это безопаснее использовать diff«S , если-то-иначе режим и Стирание изменения / удаления строки из его вывода вместо подсчета на имеющих достаточно большой контекст, на.: diff --old-line-format='' --new-line-format='' <(git rev-list …) <(git rev-list …)|head -1.
Лиори

3
git log -n1 --format=format:%H $(git log --reverse --format=format:%H master..topic | head -1)~я думаю, будет работать
Тобиас Шульте

4
@ JakubNarębski: У вас есть способ git merge-base --fork-point ...дать коммит B (commit 6aafd7f) для этого дерева? Когда вы опубликовали это, я был занят другими вещами, и это звучало хорошо, но я, наконец, просто попробовал это, и я не заставляю его работать ... Я либо получаю что-то более новое, либо просто тихий сбой (без ошибок сообщение, но статус выхода 1), пытаясь аргументы , как git co master; git merge-base --fork-point topic, git co topic; git merge-base --fork-point master, git merge-base --fork-point topic master(или для проверки) и т.д. Есть ли что - то , что я делаю неправильно или не хватает?
Линдес

25
@ JakubNarębski @lindes --fork-pointоснован на reflog, поэтому он будет работать, только если вы внесли изменения локально. Даже тогда записи reflog могли истечь. Это полезно, но совсем не надежно.
Даниэль Любаров

131

Вы можете искать git merge-base:

git merge-base находит наилучшего общего предка (ей) между двумя коммитами для использования в трехстороннем слиянии. Один общий предок лучше другого общего предка, если последний является предком первого. Общий предок, который не имеет лучшего общего предка, является лучшим общим предком , то есть базой слияния . Обратите внимание, что для пары коммитов может быть несколько баз слияния.


10
Обратите внимание также на --allопцию " git merge-base"
Якуб Наребски

10
Это не отвечает на первоначальный вопрос, но большинство людей задают гораздо более простой вопрос, на который это ответ :)
FelipeC

6
он сказал, что не хочет быть результатом git merge-base
Том Таннер

9
@TomTanner: Я только что посмотрел на историю вопроса, и оригинальный вопрос был отредактирован, чтобы включить эту заметку примерно через git merge-baseпять часов после того, как мой ответ был опубликован (вероятно, в ответ на мой ответ). Тем не менее, я оставлю этот ответ как есть, потому что он все еще может быть полезен для кого-то еще, кто находит этот вопрос через поиск.
Грег Хьюгилл

Я полагаю, что вы можете использовать результаты из merge-base --all, а затем перебрать список коммитов возврата, используя merge-base --is-ancestor, чтобы найти самого старого общего предка из списка, но это не самый простой способ ,
Matt_G

42

Я использовал 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

Примечание: это, кажется, дает первый коммит на ветке, а не общего предка. (т.е. он дает Gвместо A, согласно графику в исходном вопросе.) У меня есть ответ A, который я получу, что я буду публиковать в настоящее время.
Линд

@lindes: Это дает общего предка в каждом случае, я попробовал это. У вас есть пример, где это не так?
Mipadi

Да. В моем ответе (в котором есть ссылка на репозиторий, который вы можете клонировать, git checkout topicа затем запустить его topicвместо branch-a), он перечисляет 648ca357b946939654da12aaf2dc072763f3caeeи 37ad15952db5c065679d7fc31838369712f0b338- и то, 37ad159и другое 648ca35в предке текущих веток (последняя является текущей HEAD of topic), но ни одна точка не имеет смысла до того, как произошло ветвление. Вы получаете что-то другое?
Линд

@lindes: мне не удалось клонировать ваш репозиторий (возможно, проблема с разрешениями?).
Мипади

Ой, извини! Спасибо, что сообщили мне об этом. Я забыл запустить git update-server-info. Это должно быть хорошо идти сейчас. :)
Линдс

31

Если вам нравятся краткие команды,

git rev-list $ (git rev-list --first-parent ^ ветвь имя_хоста | 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.


3
Это, безусловно, самый чистый ответ, давайте проголосуем за это. Предлагаемое редактирование: git rev-list commit^^!может быть упрощено какgit rev-parse commit^
Рассел Дэвис

Это должен быть ответ!
тринадцатое

2
Этот ответ хороший, я просто заменить git rev-list --first-parent ^branch_name masterс , git rev-list --first-parent branch_name ^masterпотому что если главный ветвь 0 фиксаций впереди другой ветви (быстро FORWARDABLE к нему), вывод не будет создан. С моим решением, выход не создается, если мастер строго впереди (т. Е. Ветвь полностью объединена), что я и хочу.
Михаэль Шмайссер

3
Это не сработает, если я не пропущу что-то. В ветвях примера есть слияния в обоих направлениях. Похоже, вы пытались принять это во внимание, но я верю, что это приведет к тому, что ваш ответ потерпит неудачу. git rev-list --first-parent ^topic masterбудет только принять вас обратно к первой фиксации после последнего слияния из masterв topic(если вообще существует).
Джерри

1
@jerry Вы правы, этот ответ - мусор; например, в случае, когда только что произошло обратное слияние (объединение мастера с темой), и после этого мастер не имеет новых коммитов, первая команда git rev-list --first-parent вообще ничего не выводит.
TamaMcGlinn

19

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

Журнал

Создавая репозиторий с заданной структурой, мы получаем git log:

$ 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без промежуточных фиксаций , то решение Lindes' только дает вам самый последний первый divergance .

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

Это на самом деле все сводится к тому, gits не хватает того , что 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

Спасибо Чарльзу Бэйли за то, что он показал мне более компактный способ создания сценария репозитория.


2
Решение по Карлу легко исправить: diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1. Спасибо за предоставление инструкций по созданию репо :)
FelipeC

Я думаю, что вы имеете в виду «Уточненный вариант решения, предоставленного Карлом, возвращает X». Оригинал работал отлично, это было просто уродливо :-)
Карл

Нет, ваш оригинал не работает нормально. Конечно, вариант работает даже хуже. Но добавление опции --topo-order заставит вашу версию работать :)
FelipeC

@felipec - см. мой последний комментарий к ответу Чарльза Бейли . Увы, наш чат (и, следовательно, все старые комментарии) теперь удалены. Я постараюсь обновить свой ответ, когда у меня будет время.
Марк Бут

Интересно. Я бы предположил, что топологическим является значение по умолчанию. Глупый я :-)
Карл

10

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

В 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"


3
Спасибо, Чарльз, вы меня убедили, если я хочу узнать точку, в которой ветвь изначально расходилась , мне придется пометить ее. Я действительно хочу , чтобы gitбыло эквивалент hg«ы имени филиалов, это сделало бы управление долгоживущими ветвями технического обслуживания , так гораздо проще.
Марк Бут

1
«В git ветки - это всего лишь текущие названия подсказок разделов истории. У них нет сильной идентичности». Страшно сказать это и убедил меня, что мне нужно лучше понимать ветви Git - спасибо (+ 1)
Дамблдад

В истории ветвей ветвление и слияние до того, как названная ветвь была разветвленной, а промежуточная ветвь двух именованных ветвей выглядит одинаково. Ага. +1.
user1071847

5

Как насчет чего-то вроде

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`^

Это работает. Это действительно громоздко, но это единственное, что я обнаружил, что на самом деле, кажется, делает эту работу.
GaryO

1
Эквивалент Git alias: diverges = !bash -c 'git rev-parse $(diff <(git log --pretty=oneline ${1}) <(git log --pretty=oneline ${2}) | tail -1 | cut -c 3-42)^'(без временных файлов)
conny

@conny: Ого, я никогда не видел синтаксис <(foo) ... это невероятно полезно, спасибо! (Работает в Zsh тоже к вашему сведению.)
Линдс

это, кажется, дает мне первый коммит на ветке, а не общего предка. (т.е. он дает Gвместо A, согласно графику в исходном вопросе.) Я думаю, что я нашел ответ, который я опубликую в настоящее время.
Линд

Вместо 'git log --pretty = oneline' вы можете просто сделать 'git rev-list', затем вы также можете пропустить вырезку, более того, это дает родительский коммит точки расхождения, так что просто tail -2 | Глава 1. Итак:diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1
FelipeC

5

конечно, я что-то упускаю, но 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

И это решает все мои обычные дела. Конечно, есть пограничные, которые не покрыты, но ... чао :-)


diff <(git rev-list "$ {1: -master}" --first-parent) <(git rev-list "$ {2: -HEAD}" --first-parent) -U1 | хвост -1
Александр Берд

я обнаружил, что это быстрее (2-100x):comm --nocheck-order -1 -2 <(git rev-list --reverse --topo-order topic) <(git rev-list --reverse --topo-order master) | head -1
Андрей Некулау


4

После долгих исследований и дискуссий становится ясно, что нет волшебной палочки, которая бы работала во всех ситуациях, по крайней мере, в текущей версии Git.

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

Чтобы найти точку ветвления ветки devel, все, что вам нужно сделать, это использовать devel@{tail}, вот и все.

https://github.com/felipec/git/commits/fc/tail


1
Может быть единственным стабильным решением. Вы видели, может ли это попасть в мерзавца? Я не видел запрос на получение.
Александр Климетчек

@AlexanderKlimetschek Я не отправлял патчи, и я не думаю, что они будут приняты. Тем не менее, я попробовал другой метод: ловушку "update-branch", которая делает что-то очень похожее. Таким образом, по умолчанию Git ничего не будет делать, но вы можете включить хук, чтобы обновить хвостовую ветвь. У вас не было бы devel @ {tail}, но было бы неплохо использовать tails / devel.
FelipeC

3

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

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

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
}

3

Следующая команда покажет SHA1 коммита A

git merge-base --fork-point A


Это не будет, если родительские и дочерние ветви имеют промежуточные слияния друг с другом между ними.
Nawfal

2

Кажется, я получаю некоторую радость от

git rev-list branch...master

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

git rev-list -1 `git rev-list branch...master | tail -1`^

Кажется, работает для меня и не требует различий и так далее (что полезно, поскольку у нас нет этой версии различий)

Исправление: это не работает, если вы находитесь в основной ветке, но я делаю это в скрипте, так что это не проблема


2

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

git log --ancestry-path master..topicbranch

У меня эта команда не работает на данном примере. Пожалуйста, что бы вы предоставили в качестве параметров для диапазона фиксации?
Джеспер Ренн-Йенсен

2

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

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 никогда не переходить вперед и всегда создавать ветку слияния. Ужасный способ сохранить историю ветвей - использовать теги и / или ветви (теги действительно рекомендуется) в соответствии с некоторыми соглашениями по вашему выбору. Я действительно не рекомендовал бы фиктивный пустой коммит в ветке, в которую вы сливаетесь. Очень распространенное соглашение - не объединяться в ветку интеграции, пока вы не захотите по-настоящему закрыть свою ветку. Это практика, которой люди должны придерживаться, иначе вы работаете над тем, чтобы иметь ветви. Однако в реальном мире идеал - это не всегда практическое значение, а правильные поступки не жизнеспособны для любой ситуации. Если что ты


«Git не сохраняет историю ссылок», но делает это не по умолчанию и не в течение длительного времени. Смотрите man git-reflogи часть о датах: «master@ enjone.week.ago} означает« где master указывал неделю назад в этом локальном репозитории »». Или обсуждение <refname>@{<date>}в man gitrevisions. И core.reflogExpireв man git-config.
Патрик Мевзек

2

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

В то же время я создаю ветку, я также создаю тег с тем же именем, но с -initсуффиксом, например, feature-branchиfeature-branch-init .

(Это довольно странно, что это такой сложный вопрос!)


1
Учитывая просто ошеломляющую глупость разработки концепции «ветви» без какого-либо представления о том, когда и где она была создана ... плюс огромные сложности других предлагаемых решений - людьми, пытающимися перехитрить это слишком умным Я думаю, я бы предпочел ваше решение. Только это обременяет человека необходимостью ЗАПОМНИТЬ делать это каждый раз, когда вы создаете ветку - вещь, которую делают пользователи git очень часто. Кроме того - я где-то читал, что у тегов есть наказание быть «тяжелыми». Тем не менее, я думаю, что это то, что я думаю, я сделаю.
Мотти Шнеор

1

Простой способ облегчить просмотр точки ветвления 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

Это облегчает!

Обратите внимание, если в репо много веток, вы захотите указать 2 ветви, которые вы сравниваете, вместо того, чтобы использовать --all:

$ git log --decorate --oneline --graph --first-parent master origin/topic

0

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

Помните, что это совсем не просто вычислить с помощью обычных команд оболочки 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, вероятно, работает чертовски хорошо большую часть времени. Однако он медленнее, потому что мне приходится проходить всю историю репо ... дважды. :-)


0

Следующее реализует git эквивалент svn log --stop-on-copy и может также использоваться для нахождения источника ветви.

Подходить

  1. Получить голову для всех отраслей
  2. собрать mergeBase для целевой ветки друг друга
  3. git.log и итерация
  4. Остановить первый коммит, который появляется в списке mergeBase

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

Ноты

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

детали: https://stackoverflow.com/a/35353202/9950


-1

Вы можете использовать следующую команду для возврата самого старого коммита в branch_a, который недоступен из master:

git rev-list branch_a ^master | tail -1

Возможно , с дополнительной проверкой здравомыслия , что родитель , что совершить это на самом деле можно добраться от мастера ...


1
Это не работает Если branch_a объединяется с master один раз, а затем продолжается, то коммиты этого слияния будут считаться частью master, поэтому они не будут отображаться в ^ master.
FelipeC

-2

Вы можете проверить список журналов ветви A, чтобы найти, из какого коммита он был создан, а также полную историю коммитов, на которые указывает эта ветка. Reflogs в .git/logs.


2
Я не думаю, что это работает в целом, потому что reflog может быть сокращен. И я не думаю, что (?) Рефлоги тоже будут выдвинуты, так что это будет работать только в ситуации с одним репо.
GaryO

-2

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

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'.

Наслаждаться ;)


4
Доверие к сообщениям слияния более хрупко, чем гипотезы о родительском порядке. Это не просто гипотетическая ситуация; Я часто использую, git merge -mчтобы сказать, что я слил, а не название потенциально эфемерной ветви (например, «объединить магистраль превращается в особенность XYZ-рефактор»). Предположим, я был менее полезным с моим -mв моем примере? Проблема просто не разрешима в ее полной общности, потому что я могу сделать одну и ту же историю с одной или двумя временными ветвями, и нет никакой возможности определить разницу.
CB Bailey

1
@CharlesBailey Это ваша проблема тогда. Вы не должны удалять эти строки из сообщения фиксации, вы должны добавить оставшуюся часть сообщения ниже оригинального. В настоящее время «git merge» автоматически открывает редактор, в который вы можете добавить все, что вы хотите, а для старых версий git вы можете сделать «git merge --edit». В любом случае, вы можете использовать ловушку коммитов, чтобы добавить «Commited on branch 'foo'» к каждому коммиту, если вы действительно этого хотите. Тем не менее, это решение работает для большинства людей .
FelipeC

2
Не работал для меня. Ветка_А разветвлена ​​из мастера, у которого уже было много слияний. Эта логика не дала мне точный хеш коммита, где была создана branch_A.
Венкат Котра
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.