Внедрение Git в корпоративную систему разработки

vtfpoukjlruesmucbjvwrgshncc.jpeg

Разработчиков, которые знают и умеют работать с git, за последнее время выросло на порядок. Привыкаешь к скорости выполнения команд. Привыкаешь к удобству веток и легкого отката изменений. Разрешение конфликтов настолько обыденно, что программисты привыкли к героическому разрешению конфликтов там, где их быть не должно.

Наша команда в Directum разрабатывает инструмент разработки для платформенных решений. Если вы видели 1С, то примерно сможете представить рабочее окружение наших «клиентов» — прикладных разработчиков. С помощью этого самого инструмента разработки прикладной разработчик создает прикладное решение для заказчиков.

Перед нашей командой встала задача упростить жизнь нашим прикладникам. Мы разбалованы современными фишками из Visual Studio, ReSharper и IDEA. Прикладники требовали от нас внедрить в инструмент работу с git «из коробки».

Сложность то вот в чем. В инструменте на каждый тип сущности (договор, отчет, справочник, модуль) могла присутствовать блокировка. Один разработчик начинал редактировать тип сущности и блокировал его до тех пор, пока не завершит изменения и не закомитит их на сервер. Остальные разработчики в это время просматривают тот же тип сущности только на чтение. Разработка чем-то напоминала работу в SVN или пересылку документа Word по почте между несколькими пользователями. Хочется сразу всем, а может только один.

У каждого типа сущности может быть много обработчиков (открытие документа, валидация перед сохранением, запись в БД), в которых требуется написать код, который работает с конкретным экземпляром сущности. Например, заблокировать кнопки, отобразить пользователю сообщение или создать новое задание исполнителям. Весь код в рамках API, предоставляемого платформой. Обработчики — классы, в которых лежит много методов. Когда двум людям было необходимо поправить один и тот же файл с кодом, сделать это не представлялось возможным, потому что платформа блокировала тип сущности целиком вместе с зависимым кодом.

Наши прикладники пошли во все тяжкие. Они тихонько форкнули себе «нелегальную» копию нашей среды разработки, закоментировали часть с блокировками и мержили к себе наши коммиты. Прикладной код держали под гитом, коммитили через сторонние инструменты (git bash, SourceTree и прочие). Мы сделали свои выводы:


  1. Наша команда недооценила готовность прикладных разработчиков влезть в платформу. Огромное уважение и почет!
  2. Решение, предложенное ими, на продакшен не годится. С git у человека развязаны руки и он способен сотворить всё, что угодно. Поддерживать всё многообразие будет глупо, не угонимся. К тому же придется обучить заказчиков платформы. Задокументировать все команды git применительно к платформе свело бы команду документирования с ума.

2oo9ikx4h9ctyrdh86yktmh-waa.jpeg

Значит отдавать на продакшен гитом наружу не годится. Решили как-то инкапсулировать логику основных операций и ограничить их количество. По крайней мере для первого релиза. Список команд сокращали как могли и остались:


  • status
  • commit
  • pull
  • push
  • reset --hard к HEAD
  • reset к последнему «серверному» коммиту

Для первого релиза от работы с ветками решили отказаться. Не то, чтобы это очень сложно, просто команда не уложилась в ресурс времени.

Периодически наши партнёры присылают свою прикладную разработку и спрашивают: «У нас что-то не работает. Что мы делаем не так?». В этом случае прикладник загружает себе чужую разработку и глядит в код. Раньше это работало так:


  1. Разработчик забирал себе архив с разработкой;
  2. Изменял в конфигах локальную БД;
  3. Заливал чужую разработку к себе в базу;
  4. Отлаживал, находил ошибки;
  5. Выдавал рекомендации;
  6. Возвращал свою разработку назад.

Новая методология не укладывалась в старый подход. Пришлось поломать голову. Команда предложила два подхода для решения этой проблемы:


  1. Хранить все разработки в одном git-репозитории. При необходимости работы с чужим решением создавать временную ветку.
  2. Хранить разработки разных команд в разных репозиториях. Вынести настройки загружаемых в среду папок в конфигурационный файл.

Решили идти по второму пути. Первый показался сложнее в реализации и к тому же, легче выстрелить себе в ногу с переключениями веток.

Но и со вторым тоже не сладко. Команды, которые описаны выше, должны работать не просто в пределах одного репозитория, а сразу с несколькими. Есть изменения в типах сущностей из разных репозиториев? Мы показываем их в одном окошечке. Так удобнее и прозрачно для прикладного разработчика. Нажимая кнопку commit, инструмент фиксирует изменения в каждом из репозиториев. Соответственно команды pull/push/reset «под капотом» работают с физически разными репозиториями.

cjjxhctyhbggbu0zuy2-tfw1zhs.jpeg

Для работы с git выбирали из двух вариантов:


  1. Работать с git, установленным в системе, дёргая его через Process.Start и разбирая вывод.
  2. Использовать libgit2sharp, который через pinvoke дёргает библиотеку libgit2.

Нам показалось, что использовать готовую библиотеку — разумное решение. Зря. Чуть позже расскажу почему. На первых порах библиотека дала нам возможность быстро выкатить рабочий прототип.

Удалось реализовать примерно за месяц. На самом деле прикручивание гита было быстрым, а большую часть времени мы пытались вылечить вскрытые раны из-за того, что выпилили старый механизм хранения исходных файлов. В интерфейс просто отдавали всё, что возвращал git status. При клике на каждый файл отображается diff. Выглядело как интерфейс git gui.

Первый вариант оказался чрезмерно информативен. С каждым типом сущности связано сразу много файлов. Эти файлы создавали шум, и становилось неясно, какие же типы сущности изменились и что конкретно.

Сгруппировали файлы по типам сущности. Каждому файлу дали человекочитаемое имя, такое же как и в GUI. Метаданные типа сущности описаны в JSON. Их тоже нужно было представить в человекочитаемом формате. Анализ изменения в версиях json «до» и «после» начали с помощью библиотеки jsondiffpatch, а затем написали собственную реализацию сравнения JSON (далее буду называть jsondiff). Результаты сравнения прогоняем через анализаторы, которые выдают человекочитаемые записи. Много файлов скрыли с глаз, оставляя простую запись в дереве изменений.

Конечный результат получился таким:

g7pl3ku4bqatgba2u9t6_msyxlc.png

Libgit2 выдал большое количество неожиданных сюрпризов. Разобраться с некоторыми оказалось не под силу в разумное время. Расскажу, что вспомню.

Неожиданные и трудновоспроизводимые падения на некоторых стандартных операциях. «No error provided by native library» говорит нам обёртка. Прекрасно. Чертыхаешься, пересобираешь нативную библиотеку в debug, повторяешь упавший ранее кейс, а оно в debug режиме не падает. Пересобираешь в release и снова падает.

Если с libgit2sharp параллельно запущен сторонний инструмент, скажем SourceTree, то commit может не закоммитить некоторые файлы. Или зависает при отображении диффов на некоторых файлах. Как только пытаешься отладить, не получается воспроизвести.

У одного из наших прикладников выполнение аналога git status занимало 40 секунд. Сорок, Карл! При этом запущенный из консоли гит отрабатывал как положено в течение секунды. Я потратил пару дней, чтобы разобраться. Libgit2 при поиске изменений глядит на файловые атрибуты папок и сравнивает их с записью в индексе. Если время модификации отличается, значит внутри папки что-то изменилось и нужно заглянуть внутрь и/или поискать в файлах. А если ничего не менялось, то и внутрь лезть не стоит. Эта оптимизация видимо есть и в консольном git. Не знаю по какой причине, но именно у одного человека в индекс git изменялся mtime. Из-за этого git каждый раз проверял на наличие изменений содержимое ВСЕХ файлов в репозитории.

Ближе к релизу наша команда прогнулась под пожелания прикладников и заменили git pull на fetch + rebase + autostash. И тут к нам приехало ещё куча багов, в том числе и с «No error provided by native library».

status, pull и rebase работают заметно дольше вызова консольных команд.

Файлы в разработке делятся на два типа:


  1. Файлы, которые прикладник видит в инструменте разработки. Например, код, изображения, ресурсы. Такие файлы нужно мержить так, как это делает git.
  2. Файлы JSON, которые создаются средой разработки, но прикладной разработчик их видит только в виде GUI. В них требуется автоматически разрешить конфликты.
  3. Генерируемые файлы, которые автоматически пересоздаются при работе с инструментом разработки. В репозитории эти файлы не попадают, инструмент сразу заботливо кладёт .gitignore.

С новым укладом два разных прикладника смогли изменить один и тот же тип сущности.

Например, Саша изменит информацию о том, как хранить тип сущности в базе данных и напишет обработчик события сохранения, а Сергей стилизует представление сущности. С точки зрения git это конфликтом не будет и оба изменения сольются без сложностей.

А потом Саша изменил свойство Property1 и задал ему обработчик. Сергей создал свойство Property2 и задал обработчик. Если посмотреть на ситуацию сверху, их изменения не конфликтуют, хотя с точки зрения git затронуты одни и те же файлы.
Захотелось, чтобы инструмент смог самостоятельно разрулить подобную ситуацию.

Примерный алгоритм слияния двух JSON при возникновении конфликта:


  1. Загружаем из гита JSON base.


  2. Загружаем из гита JSON ours.


  3. Загружаем из гита JSON theirs.


  4. Используя jsondiff, формируем программные патчи base→ours и применяем к theirs. Получившийся JSON назовём P1.


  5. Используя jsondiff, формируем программные патчи base→theirs и применяем к ours. Получившийся JSON назовём P2.


  6. В идеале после применения патчей P1 === P2. Если это так, то записываем P1 на диск.


  7. В неидеальном случае (когда действительно нашелся конфликт) предлагаем пользователю выбрать между P1 и P2 с возможностью допилить руками. Записываем выбор на диск.

После слияния проверяем, пришли ли к состоянию без ошибок валидации. Если не пришли, то отменяем такое слияние и просим пользователя повторить. Это не лучшее решение, но оно хотя бы гарантирует, что со второй или третьей попытки слияние произойдет без неприятных последствий.


  1. Прикладники довольны, что могут легально пользоваться.
  2. Внедрение git ускорило разработку.
  3. Автоматические слияния вообще выглядят как магия.
  4. Заложим на будущее отказ от libgit2 в пользу вызова процесса git.

© Habrahabr.ru