По долгу службы мне досталась в наследство некая система, имеющая ~15 лет истории и порядка нескольких десятков инсталляций в разных организациях. Сама по себе системы относительно небольшая (~25K строчек кода, ~1K коммитов), но проблема была в release management:
- было основное дерево в subversion (изначально в cvs, разумеется), где проводился «основной курс партии» — делались какие-то масштабные изменения, добавлялись новые возможности, исправлялись глобальные ошибки и т.п.
- конкретные инсталляции делались путем:
- в лучшем случае — svn checkout, который потом обновлялся через svn update; почти во всех инсталляциях делались локальные доработки «на живую» (как минимум — правились конфигурационные файлы) и эти изменения никуда не коммитились; если при очередном svn update изменения в upstream создавали конфликт — конфликт ресолвился «на месте» тем программистом, который делал update, опять же, без какого-либо трекинга изменений
- в худшем случае — svn export, который потом, понятно, не обновлялся совсем, оставаясь раз и навсегда (или по крайней мере пока начальство не одумается) на уровне развития даты экспорта; в особо запущенных случаях (из конца 1990-х — начала 2000-х) так делали еще и потому, что просто не было физической возможности сделать checkout — в организации не было доступа в интернет, архив просто приносили на дискетке и разворачивали единожды на месте
На практике, разумеется, благодарные заказчики этой системы время от времени хотят все-таки получать поддержку, исправления багов и даже иногда какие-то глобальные доработки в ядре системы.
После недолгого консилиума, продолжать поддержку столь распределенной системы в svn было признано нецелесообразным и решено было мигрировать на git.
Проблема номер один — перетащить мастер-дерево из svn на git — решилась в целом просто стандартными средствами git-svn.
Комплект проблем номер два — как вливать в это дерево многочисленные форки в разных инсталляциях — было решено разбирать «по мере поступления». Когда очередная организация просыпалась, нужно было:
- получить их форк
- понять, откуда он в свое время был форкнут и до какого уровня последний раз перебазировался (если это svn checkout)
- создать новый бранч для этого форка
- попытаться разделить сделанные изменения на более-менее семантически-связанные кусочки поменьше и закоммитить их все в этот бранч
Основной затык внезапно оказался на шаге 2 — понять, откуда же была форкнута очередная инсталляция. В случае svn checkout можно было хотя бы посмотреть на текущее состояние working copy, в случае же svn export угадывать было нетривиально. Потыкавшись с полуручным археологическим исследованием состояния кода пару раз, мне надоело и решено было автоматизировать поиски. Готового решения не нашлось (git bisect здесь, к сожалению, не годится) и получился следующий скрипт:
#!/bin/sh -ef
if [ $# -ne 2 ]; then
echo "Usage: $0 <git-repo-dir> <candidate-checkout-dir>"
exit 1
fi
GIT_REPO="$1"
CANDIDATE_DIR=$(cd "$2" && pwd)
TAB=$(printf 't')
cd "$GIT_REPO"
COMMITS=$(git log --all --format=format:%H)
# Remember current commit
CURRENT_COMMIT=$(git rev-parse HEAD)
for C in $COMMITS; do
git checkout --quiet $C
echo -n "$C$TAB"
diff -urN --exclude=.git --exclude=.svn "$CANDIDATE_DIR" . | wc -l
done | sort -t"$TAB" -k2,2n
# Restore current commit
git checkout --quiet "$CURRENT_COMMIT"
Скрипт принимает 2 параметра: (1) путь к git-репозитарию, (2) путь к очередному форку-кандидату, для которого нужно найти место «врезки» в общее дерево развития проекта. Скрипт банально рассчитывает объем diff'а (в строках) между каждым checkout'ом репозитария и кандидатом-на-врезку. С большой вероятностью — коммит, где объем различий минимален — и есть оптимальное место для базирования бранча. Результат работы выглядит примерно так:
3810315aaa238e32a7106312f9973f1d1f0ea097 651 19b595d87eecc43933ea60d89882319c7ac3f512 835 989cee69664733b773a4a81cc49e2a1a0cdff38a 872 9026dae1154f98018c808b73c7f1c6cd09310dc7 885 802943edf287ad28d5e71a57510400afacb49176 894 c5bd4050fce754e16664e6e1eeb57a4ff3ed06c6 894 dcb70c4a2e9fc0431ceb6154ecd1688189362622 908 ...
Это значит, что скорее всего задача будет решена как-то так:
$ git branch new-organization 3810315aaa238e32a7106312f9973f1d1f0ea097
$ git checkout new-organization
$ cp -r ../new-organization-fork/* .
… после чего можно уже разбираться с изменениями, пытаться разделять их на части и коммитить (возможно, даже с --date и --author, если получится их выяснить).
Буду рад, если приведенное решение окажется полезным кому-то еще. Комментарии и советы, как сделать лучше, приветствуются.
Автор: GreyCat