Найти коммиты Git, содержащие несколько конкретных коммитов
Общая проблема: Учитывая набор коммитов, как мне найти список коммитов, в которых все эти коммиты являются предками, или, соответственно, первые коммиты, которые содержат все эти коммиты.
Я могу найти ветви (аналогично тегам), которые содержат коммиты, ища ветви, которые возвращаются git branch --contains <commit>
для всех коммитов в наборе, но git rev-list
не имеет --contains
вариант. По сути, я ищу способ объединения регулярных --contains
аргументы с git rev-list
и ограничение вывода коммитами, которые содержат все перечисленные коммиты, а не один из них (как это --contains
работает нормально).
Конкретный пример: данные коммиты a
, b
, c
Как я могу найти первый коммит, который имеет все три коммитов в своем происхождении?
Например, учитывая приведенное ниже дерево, как мне найти коммит, помеченный X?
* (master)
|
X
|\
a *
| |
b c
|/
*
|
*
Я предполагаю, что есть какая-то магия, с которой я могу сделать git rev-list
и, возможно, с участием <commit1>...<commit2>
запись, но я не могу работать дальше, чем это.
3 ответа
Я думаю, что ответ на этот вопрос заключается в том, что Git не был создан для этого. Git действительно не нравится идея "детей коммитов", и для этого есть очень веская причина: она не очень четко определена. Поскольку коммит не знает о своих детях, это очень расплывчатый набор. Возможно, у вас не все ветки в вашем репо, и поэтому вам не хватает некоторых детей.
Внутренняя структура хранения Gits также делает поиск дочерних элементов коммита довольно дорогой операцией, так как вам нужно пройти граф ревизий всех головок либо к их соответствующим корням, либо пока вы не увидите все коммиты, чьи дочерние элементы вы хотите знать.
Единственная концепция такого рода, которую поддерживает git, - это идея одного коммита, содержащего другой коммит. Но эта функция поддерживается только несколькими командами git (git branch
будучи одним из них). И где git поддерживает его, он не поддерживает его для произвольных коммитов, а только заголовки веток.
Все это может показаться довольно жестким ограничением git, но на практике оказывается, что вам не нужны "потомки" коммита, а обычно нужно только знать, какие ветви содержат конкретный коммит.
Все это говорит: если вы действительно хотите получить ответ на свой вопрос, вам придется написать собственный скрипт, который его найдет. Самый простой способ - начать с вывода git rev-list --parents --reverse --all
, Анализируя эту строку построчно, вы должны построить дерево, и для каждого узла отметьте, является ли он дочерним элементом коммитов, которые вы ищете. Вы делаете это, отмечая коммиты сами, как только встретите их, а затем передаете это имущество всем своим детям и так далее.
Получив коммит, помеченный как содержащий все коммиты, вы добавляете его в "список решений" и помечаете всех его дочерних элементов как мертвые - они больше не могут содержать никаких первых коммитов. Это свойство затем также будет передано всем его потомкам.
Здесь вы можете сэкономить немного памяти, если не храните какие-либо части дерева, которые не содержат ни одного из запрошенных вами коммитов.
редактировать взломанный код Python
#!/usr/bin/python -O
import os
import sys
if len(sys.argv) < 2:
print ("USAGE: {0} <list-of-revs>".format([sys.argv[0]]))
exit(1)
rev_list = os.popen('git rev-list --parents --reverse --all')
looking_for = os.popen('git rev-parse {0}'
.format(" ".join(sys.argv[1:]))).read().splitlines()
solutions = set()
commits = {}
for line in rev_list:
line = line.strip().split(" ")
commit = set()
sha = line[0]
for parent in line[1:]:
if not parent in commits:
continue
commit.update(commits[parent])
if parent in solutions:
commit.add("dead")
if sha in looking_for:
commit.add(sha)
if not "dead" in commit and commit.issuperset(looking_for):
solutions.add(sha)
# only keep commit if it's a child of looking_for
if len(commit) > 0:
commits[sha] = commit
print "\n".join(solutions)
Одно из возможных решений:
Используйте 'git merge-base a b c', чтобы получить коммит для использования в качестве отправной точки в вызове rev-list; мы назовем это $MERGE_BASE.
Используйте вызов git rev-list $MERGE_BASE..HEAD, чтобы получить список всех коммитов от их общего предка до HEAD. Перебрать этот вывод (псевдокод):
if commit == a || b || c
break
else
$OLDEST_DESCENDANT = commit
return $OLDEST_DESCENDANT
Это будет работать для вашего примера выше, но даст ложный положительный результат, если они никогда не были объединены, не были объединены в коммите сразу после самого младшего из a, b, c, или если было несколько коммитов слияния для объединения a, б и в (если каждый из них проживал в своей ветви). Осталось немного работы, чтобы найти этого самого старого потомка.
Затем следует выполнить вышесказанное, начав с $OLDEST_DESCENDANT и переместившись назад в DAG от него к HEAD (rev-list --reverse $OLDEST_DESCENDANT~..HEAD), проверяя, что вывод 'rev-list $MERGE_BASE~..$OLDEST содержит все необходимые коммиты a, b и c (хотя, возможно, есть лучший способ проверить, что они достижимы, чем rev-list).
Как отмечает Твальберг, индивидуальное тестирование коммитов кажется неоптимальным и медленным, но это только начало. Этот подход имеет преимущество перед его методом списка слияний в том, что он обеспечивает правильный ответ, когда все входные коммиты находятся в одной ветви.
На производительность в основном будут влиять расстояния между базой слияния, головкой, X и самым младшим из желаемого набора коммитов (a, b и c).
Как насчет:
MERGE_BASE=`git merge-base A B C`
git log $MERGE_BASE...HEAD --merges
Предполагая, что у вас есть только 1 слияние. Даже если у вас есть больше слияний, самый старый из них содержит изменения из всех трех коммитов