Найти точку ветвления с помощью Git? - PullRequest
404 голосов
/ 06 октября 2009

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

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

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

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

Ответы [ 21 ]

3 голосов
/ 30 мая 2012

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

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

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
}
2 голосов
/ 09 ноября 2017

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

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 может взять из каждой ссылки (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

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

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

2 голосов
/ 03 мая 2017

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

git merge-base --fork-point A

2 голосов
/ 11 июля 2014

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

git log --ancestry-path master..topicbranch
2 голосов
/ 21 марта 2013

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

git rev-list branch...master

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

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

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

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

1 голос
/ 31 января 2018

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

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

(Странно, что на такой трудный вопрос ответить!)

0 голосов
/ 12 февраля 2016

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

подход

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

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

Примечания

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

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

0 голосов
/ 01 февраля 2016

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

Помните, что это совсем не просто вычислить с помощью обычных команд оболочки git, поскольку git rev-list - наш самый мощный инструмент - не позволяет нам ограничить путь, по которому a коммит достигнут. Самый близкий у нас есть 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 голосов
/ 07 октября 2009

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

0 голосов
/ 16 апреля 2012

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

git rev-list branch_a ^master | tail -1

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

...