Найти коммиты 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 слияние. Даже если у вас есть больше слияний, самый старый из них содержит изменения из всех трех коммитов

Другие вопросы по тегам