Вливание legacy-истории в дерево: нахождение оптимальной точки ответвления

в 8:25, , рубрики: Git, git workflow, release management, Системы управления версиями, метки: , ,

Вливание legacy истории в дерево: нахождение оптимальной точки ответвленияПо долгу службы мне досталась в наследство некая система, имеющая ~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.

Комплект проблем номер два — как вливать в это дерево многочисленные форки в разных инсталляциях — было решено разбирать «по мере поступления». Когда очередная организация просыпалась, нужно было:

  1. получить их форк
  2. понять, откуда он в свое время был форкнут и до какого уровня последний раз перебазировался (если это svn checkout)
  3. создать новый бранч для этого форка
  4. попытаться разделить сделанные изменения на более-менее семантически-связанные кусочки поменьше и закоммитить их все в этот бранч

Основной затык внезапно оказался на шаге 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

Источник

Поделиться

* - обязательные к заполнению поля