Как определить, находится ли данный коммит в цепочке первого родителя ветви
Я пытаюсь написать способ, чтобы определить, находится ли данный коммит в цепочке первого родителя данной ветви. Так, например, база слияния не будет летать, так как коммит мог бы быть слит в. Я хочу знать, был ли точный коммит когда-либо верхушкой ветви.
Примечание. Для рассматриваемой ветви применяется стратегия слияния без ускоренной перемотки вперед.
3 ответа
Причудливый путь
Простой тест "is ancestor", очевидно, не подойдет, так как фиксации вниз по родительским цепочкам 2-го или более позднего поколения также являются предками:
...o--o--A--o--o--o--T
\ /
...-o--*--B----o
\
C
И то и другое A
а также B
предки T
, но вы хотите принять A
отклоняя B
а также C
, (Предполагать --first-parent
верхняя строка здесь.)
С помощью git merge-base
Тем не менее, на самом деле будет выполнять часть работы. Вам не нужно --is-ancestor
режим git merge-base
Впрочем, и нужна дополнительная обработка.
Обратите внимание, что независимо от пути между T
и некоторый предок, база слияния T
и этот предок (такой как A
или же B
) либо сам предок (A
или же B
соответственно здесь) или какой-то предок предка, например коммит *
если мы посмотрим на T
а также C
как пара. (Это справедливо даже в случае нескольких базисов слияний, хотя я оставляю вам доказательство этого).
Если, или любой произвольно выбранный из множества всех, объединить базу (ы) тестового коммита и ответвление ветвления еще не является тест-коммитом, у нас есть случай, подобный C
и может отклонить это из-под контроля. (Или мы можем использовать --is-ancestor
чтобы отклонить его, или... ну, см. ниже.) Если нет, мы должны перечислить коммиты в пути предков между коммитом и указанным ответвлением. За A
это:
o--o--*--T
и для B это:
*--T
/
o
Если любой такой коммит является коммитом слияния, как тот, который помечен *
нам нужно убедиться, что первый родительский элемент содержит один из коммитов, перечисленных в этом пути. Самые сложные случаи - это те, которые топологически похожи на:
o--o
/ \
...--A o--T
\ /
o--o
так как --ancestry-path
между ними включает в себя слияние и два способа достижения A
одним из которых является путь первого родителя, а другим - нет. (Это правда, если T
само по себе тоже слияние.)
Нам на самом деле не нужно сначала искать базу слияния. Мы используем базу слияния только для того, чтобы изучить путь предков. Если база слияния не является самим тестовым коммитом, то тестовый коммит не является предком коммитного наконечника, и testcommit..tipcommit
не будет включать testcommit
сам. Кроме того, добавляя --ancestry-path
- который отбрасывает все коммиты, которые сами по себе не являются детьми левой стороны, - тогда отбрасывает все коммиты в git rev-list
вывод: случай как C
не имеет потомков, которые являются предками T
(если это так, C
будет база слияния).
Следовательно, мы хотим изучить коммиты в git rev-list --ancestry-path testcommit..branchtip
, Если этот список пуст, тестовый коммит не является прародителем подсказки ветвления. У нас есть случай, как совершить C
; Итак, у нас есть наш ответ. Если список не пуст, уменьшите его до его компонентов слияния (запустите снова с --merges
или скормить список git rev-list --stdin --merges
, чтобы составить сокращенный список). Если этот список не пуст, проверьте каждое слияние, найдя его --first-parent
ID и убедитесь, что результат находится в первом списке.
В реальном (хотя и не проверенном) коде сценария оболочки:
TF=$(mktemp) || exit 1
trap "rm -f $TF" 0 1 2 3 15
git rev-list --ancestry-path $testcommit..$branch > $TF
test -s $TF || exit 1 # not ancestor
git rev-list --stdin --merges < $TF | while read hash; do
parent1=$(git rev-parse ${hash}^1)
grep "$parent1" $TF >/dev/null || exit 1 # on wrong path
done
exit 0 # on correct path
Перебор
Вышеупомянутые тесты как можно меньше коммитов, но в некотором смысле было бы намного более практичным, в некотором смысле, просто выполнить:
git rev-list --first-parent ${testcommit}^@..$branch
Если вывод включает в себя $testcommit
сам тогда $testcommit
достижимо только от первого родителя из branch
, (Мы используем ^@
исключить всех родителей $testcommit
так что это работает даже для корневого коммита; для других коммитов, ${testcommit}^
достаточно, так как мы используем --first-parent
.) Более того, если мы убедимся, что это сделано в топологическом порядке, последний идентификатор фиксации, выданный из git rev-list
команда будет $testcommit
само по себе, если и только если $testcommit
доступен от $branch
, Следовательно:
hash=$(git rev-parse "$testcommit") || exit 1
t=$(git rev-list --first-parent --topo-order ${hash}^@..$branch | tail -1)
test $hash = "$t"
должен сделать свое дело. Цитаты вокруг $t
в случае, если он расширяется до пустой строки.
Это удобный однострочник:
git rev-parse HEAD~"$( git rev-list --count --ancestry-path <commit>..HEAD )"
Если на выходе ваш
<commit>
, то это предок по первому родителю.
Идея состоит в том, что мы измеряем кратчайший путь между двумя коммитами с помощью
rev-list --count --ancestry-path
, затем получите фиксацию в этой позиции в цепочке first-parent. Очевидно, они должны быть такими же, если проверенная фиксация была предком-первым родителем. Подавленные ошибки (например, слишком короткая цепочка «первый родитель») не имеют значения.
Чтобы сделать его более сложным, вы могли бы создать псевдоним git, подкрепленный хорошо читаемым сценарием оболочки.
Сначала напишите файл сценария:
#!/bin/sh
ref="$1"
head="$2"
if [ -z "$head" ]; then
head="HEAD"
fi
commit=$( git rev-parse "$ref"^{commit} )
distance="$( git rev-list --count --ancestry-path "$commit".."$head" )"
found="$( git rev-parse HEAD~"$distance" )"
if [ "$commit" != "$found" ]; then
echo "${ref} is not a first-parent ancestor of ${head}"
exit 1
fi
echo "${ref} is a first-parent ancestor of ${head} at a distance of ${distance}"
exit 0
Сохраните его в соответствующем месте в вашей системе, сделайте его исполняемым, а затем установите как псевдоним git:
git config --global alias.fp '!<script-path>'
Заменять
fp
с чем угодно, что вам удобнее. Заменять
<script-path>
с местоположением вашего файла сценария, но оставьте
!
символ, необходимо использовать внешние файлы.
После этого вы можете использовать новый псевдоним как обычную команду git:
$ git fp 66e339c
66e339c is a first-parent ancestor of HEAD at a distance of 45
Стратегия без быстрой перемотки означает, что вы, вероятно, можете git log --first-parent
, Вы, вероятно, хотите только хэши, поэтому вы можете использовать git rev-list
вместо
git rev-list --first-parent | grep <commit hash>
В противном случае используйте --format
с git log
для отображения данных, которые вы хотите.
Изменить: этот пост может дать вам некоторые идеи
Как я могу определить, является ли один коммит предком другого (или наоборот)?