Меня зовут Александр Болдышев. Я представляю компанию Axenix. Вот уже 18 лет я работаю, внедряя системы 1С как архитектор, разработчик, как человек, занимающийся внутренними производственными процессами.
В этой статье я хочу рассказать о том, как и зачем можно, а может быть даже и нужно внедрять Git на проектной разработке.
Статья будет состоять из трех частей:
-
Первая из них попытается ответить на вопрос, зачем нам Git.
-
Во второй части мы посмотрим на наиболее любопытный, на мой взгляд, с точки зрения нашей работы функционал Git – на его ветвление: как оно работает, как устроено.
-
И, наконец, посмотрим на все это в применении к 1С.
Зачем нам Git. Проблемы проектных команд
Почему такой акцент на проектных командах? На мой взгляд, проектным командам по сравнению с эксплуатационными или продуктовыми несколько тяжелее внедрять всякие красивые, удобные штуки в разработке.
Мы сменяем проекты, часто меняется окружение, наша команда, какие-то входящие регламенты от заказчиков, не меняются только жесткие дедлайны и неотвратимо приближающийся запуск в эксплуатацию.
А мы тоже хотим жить красиво, хотим, чтобы наш не протестированный код не протекал в продуктив и в общую тестовую среду. Хотим знать, зачем написана та или иная строчка, с какой целью, как-то ее прослеживать.
Расскажу, как этого достичь, если вы еще сами этого не достигли. А тем, кто уже внедряет Git на проектной работе, я попробую рассказать что-нибудь новое.
Хранилище как база версионирования, возможности и ограничения
Здесь хочется начать от «печки». Мы все знаем наше уже родное хранилище, наш санитарный минимум, базу. Оно позволяет безопасно извлечь код с рабочей машины разработчика и положить его куда-то в сторонку, и там в сторонке его еще немножечко забэкапить. Оно позволяет откатиться к предыдущему состоянию, если что-то пошло совсем не так. Позволяет разработчикам работать над конфигурацией совместно, почти не мешая друг другу, не считая захвата корня и каких-то нужных объектов.
В теории все неплохо, на практике бывает всякое. Например, разработчику нужно отдать на тестирование код. Тестирование будет проходить на другой базе, и передача происходит через хранилище – ветка у нас одна, код пошел в хранилище и его получили все. В коде есть ошибки – их тоже получили все.
Придумано немало регламентов, технологий и даже костылей про то, как этого избегать. Когда мы делаем коммит, мы его очень боимся и стараемся, чтобы в хранилище попало то, что нужно. Нашим вендором придумана технология разветвленной разработки, в которой в роли веток пытаются представить отдельные хранилища, поддерживаемые определенной методологией в СППР. Это более-менее работает на небольших конфигурациях, а с расширениями уже работает не очень хорошо из-за отсутствия в расширении поддержки механизма поставок.
И вот тут к нам врывается Git со своими тремя, на мой взгляд, ключевыми фичами:
-
Первая – самая интересная и фактически ключевая для этой статьи – это ветвление.
-
Вторая фича – распределенность: у Git нет какого-то одного центрального сервера. Да, может быть репозиторий, который мы отслеживаем, но он может быть не один, и он, строго говоря, может обходиться даже без сервера.
-
Вследствие своей распределенности Git хорошо играет роль шлюза по передаче кода в какие-то внешние инструменты, будь то трекеры задач, если мы хотим обеспечить прослеживаемость, всякие тулы CI/CD, автоматической сборки или автоматического тестирования.
Ветвление Git
Как вообще устроено ветвление в Git, и что делает его таким особенным относительно большинства систем версионирования?
Самое большое преимущество Git – он очень быстро работает с ветками. И это очень приятно, потому что ветка – это не что-то абстрактное. Ветка – это конкретный изолированный контекст. Как на уровне разработчика, который сейчас работает с одной задачей, а потом с другой – так и на уровне команды, когда у нас есть разное окружение: продуктивное плюс одно или несколько тестовых окружений разработки.
Именно агрессивное ветвление, когда мы создаем очень много веток по системе – это сама суть Git. Какой бы процесс мы не строили вокруг него, мы так или иначе попытаемся утилизировать это его качество себе на пользу.
Репозиторий Git – это всегда дерево с одним корнем, одним корневым коммитом, у которого появляются потомки – один или несколько потомков на один коммит.
На картинке у одного корневого коммита три потомка, у которых потом появляются свои «детишки». Коммиты при этом всегда очень хорошо помнят о своих родителях (если это не корневой коммит – он у нас «сирота»). Поэтому фактически это часть их идентичности, что дает нам возможность проследить историю до самого начала жизни репозитория. Набор коммитов превращается в дерево, и они всегда стремятся к корню этого дерева при любом состоянии системы.
Внутреннее устройство Git, объекты и их взаимодействие
Здесь хочется немного остановиться и поговорить о том, что же обеспечивает такую быструю работу с ветками.
Git состоит из нескольких видов объектов. Самый известный объект – коммит.
Коммит прежде всего хранит информацию о себе: как его зовут, его хэш-имя, кто его автор, когда его сделали, какое сообщение было к нему прицеплено. Принято считать, что коммит в Git хранит снэпшот – снимок состояния репозитория на момент его создания. В принципе, это правда, с одним уточнением – на самом деле сам он эту информации не хранит, а поручает хранение деревьям, у которых тоже, кстати, есть хэш-имена.
При этом деревья тоже отказываются непосредственно в себе хранить информацию о содержании файлов. Их можно представить в виде каталога файловой системы, в котором перечислены имена и хэши других вложенных в него каталогов-деревьев и файлов. А вот за хранение содержания файлов отвечает такой объект, как blob (блоб).
Блоб – это третий вид объектов. У него тоже есть хэш-имя, а в остальном он просто кусок двоичных данных, обычно представляющий собой содержимое файла (впрочем, не всегда).
При этом Git очень аккуратно относится к дисковому пространству. Если во время коммита не изменился какой-то файл, то не нужно повторно создавать его снимок или копию – он сохранится в системе, просто разные деревья будут на него ссылаться.
Базовой устройство гита довольно простое и интересное, но при чем тут ветвление? А вот при чем – кроме объектов в репозитории есть и такая сущность, как ссылки. Видов их тоже три штуки. И самая главная – это как раз ветки.
Ветка – просто указатель на коммит, обычный курсор. Буквально все, что она в себе содержит, это хэш-имя коммита, на который она указывает. Больше она ничего не знает – она просто текстовый файл с хэшем коммита. При этом ветка всегда считает свой коммит последним для себя – до момента, пока у него него не появится потомок. Если у коммита появляется новый потомок, текущая ветка перескочит на него, и уже он будет текущим и на данный момент последним для нее.
Что отличает текущую ветку от всех остальных веток? Только то, что на нее указывает другой указатель – HEAD, голова. Это тоже специальный файл в репозитории Git, в котором записано имя ветки. Изредка он еще указывает сразу на коммит, а не на ветку, но это отдельная история. Кстати, текущая ветка на картинке выше – feat/01.
Наконец, есть третий вид ссылок – теги. Они тоже, как и ветки, указывают на коммит и содержат его хэш-имя. Отличие только в том, что на потомков своих коммитов они не перепрыгивают, а все время проводят со своим коммитом.
Есть еще специальные так называемые аннотированные теги, которые хранят в виде сообщения информацию о том, кто их создал, когда и зачем. В таком случае теги неожиданно превращаются в специальные объекты, ведь ссылки умеют только указывать на другие ссылки и коммиты.
Закончим с этой скучной теорией и перейдем ближе к практике.
Слияние и перебазирование – два способа объединения веток
Если бы ветки просто разрастались в дереве во все стороны, то смысла в этом было бы немного. Поэтому ветки еще тем или иным образом сливаются обратно. Так как ветка указывает на коммит, который хранит текущее состояние, или контекст, кода, то сливая ветки, мы объединяем этот контекст, перенося его изменения в другую ветку. Для этого у Git есть два с половиной способа.
Первый метод – классический Merge, то есть слияние, когда две ветки смыкаются в верхней точке, и в месте склейки образуется коммит (на схеме он рыжего цвета). У него как раз два родителя, но их может быть неограниченное количество – сколько веток мы захотели за раз замкнуть, столько у него будет родителей.
Во время слияния происходит знакомое нам по обновлению конфигураций трехстороннее объединение. Оно хорошо тем, что Git с его помощью автоматически делает подавляющее количество работы за нас. Если все же произошел какой-то конфликт, когда мы изменили одно и то же место в разных ветках, тут разработчику с помощью специальных тулов сравнения или ручным редактированием нужно этот конфликт решить, сообщив гиту целевое состояние конкретных файлов.
Второй метод часто окружен неким ореолом таинственности и даже опасности – это Rebase, перебазирование. При его использовании текущая ветка пересаживается: теперь она растет из другого коммита, на который мы указали.
Технически это происходит так: Git определяет место, где текущая ветка разошлась с целевой. После этого для каждого коммита текущей ветки (а мы перебазируем именно ее) рассчитывается diff, то есть его разница с его родителем, и эти изменения по очереди применяются на новое место роста. Таким образом, для каждого старого коммита образуется почти такой же коммит, но все же другой – как минимум родитель у него поменялся. Как мы помним, родители – это часть идентичности коммита, поэтому «тройка» на картинке уже со штрихом. При перебазировании тоже могут быть конфликты изменений, результаты решения которых тоже отразятся на новом коммите.
Вообще Rebase – очень мощный и опасный инструмент, потому что он переписывает историю. С точки зрения истории ветки feat/01 на картинке выше, коммит 3 перестанет существовать, а его заменит коммит 3’. Сам репозиторий глубоко внутри еще будет некоторое время помнить об исходном коммите 3, поэтому шанс восстановить старую историю есть, но чем дальше будет развиваться репозиторий, тем сложнее будет возможный откат.
Поэтому существует навсегда писаное, золотое правило: можно историю переписывать только на личных ветках с неопубликованными для других разработчиков коммитами. Почти все гит-серверы поддерживают автоматическую защиту публичных веток, чтобы это правило исполнялось.
Rebase хорош как инструмент подготовки текущей ветки разработки к слиянию с публичной веткой. Во-первых, перед слиянием свою ветку можно (и нужно!) пересадить на самый вверх публичной ветки, решив еще до слияния все конфликты, возникшие к моменту слияния. Во-вторых, Rebase позволяет «причесать» свою локальную ветку, пересобрать коммиты определенным образом, объединив какие-то коммиты, а какие-то даже убрав.
Если вы сделали Rebase своей ветки на последний коммит целевой ветки, то вы можете спокойно ее слить с этой веткой. Как классическим образом, через merge-коммит, так и «перемоткой», fast-forward. Потому что если вы сделали Rebase, ваша ветка теперь буквально «растет» из ветки, куда вы хотите влиться, как бы продолжает ее. И Git дает возможность просто перенести указатель целевой базы на новый коммит.
Есть разные мнения на счет уместности «перемотки». Некоторые стратегии ветвления отвергают ее, требуя коммит слияния как явную и конкретную точку объединения контекстов. Другие нацелены на «чистую», выпрямленную историю основной ветки разработки и приветствуют fast-forward.
Применение Git в 1С без EDT
Стоит начать с того, что EDT – это хороший инструмент, это настоящая Git-ориентированная IDE, логически развязавшая код и базы, в ней много хороших фич.
Почему же эта статья не про EDT? Дело в том, что очень большое количество разработчиков привыкли к конфигуратору, у нас железо на контуре разработки, которое далеко не всегда может потянуть ERP или подобные конфигурации, очень тяжелые с точки зрения работы в EDT. И мы хотим снизить этот порог входа как для разработчиков, так и для железа.
Кроме того, работа с конфигуратором позволяет не тащить основную конфигурацию в Git, тратя время на выгрузку и загрузку огромного типового кода, а сосредоточиться на своих доработках.
Процесс внедрения Git: мотивация, регламенты и поддержка
Важно понимать, что внедрение Git, как и внедрение любого инструмента – это некий процесс, некие правила, некая последовательность действий, которые так или иначе выполняют люди. И ключевой момент здесь, на мой взгляд, мотивация.
Поэтому очень важно проговаривать с командой суть изменений: что мы делаем, зачем,чего хотим добиться, и для чего нам сначала придется немного пострадать.
Кроме того, следует составить подробный регламент-шпаргалку, в котором будет написано, что делать в той или иной ситуации, потому что не все и не всё поймут сразу, это невозможно. Каждый сотрудник, каждый член команды должен знать, что после А мы идем к Б, даже если до не конца понятно, почему.
После старта не забываем обязательно проводить ревью с командой хотя бы раз в неделю, максимум в две. И записываем эти ревью, потому что будут появляться новые разработчики, да и старые, как показывает опыт, их пересматривают, когда что-то становится непонятно.
Даже если все пошло по плану и где-то даже поехало, команду далеко отпускать нельзя, потому что обязательно найдется разработчик или два, которые устроят себе персональный ад, пытаясь воспроизвести старые привычки в новом месте. Это может сильно осложнить им работу. Поэтому надо заходить к команде, проверять, как дела с процессом, а не только с задачами, и смотреть, нет ли каких-то странных коммитов.
Кстати, важно выделить аппрувера, то есть человека, который одобряет перенос коммитов в основную ветку. При этом ему даже не обязательно (хотя и желательно) смотреть код детально, как на классическом код-ревью – достаточно обращать внимание на подозрительные коммиты с большим количеством удалений.
Главную ветку нужно защитить от такой вещи, как force push. Это действие, когда на сервер отправляется коммит, который переписывает историю основной ветки. Подавляющее большинство серверов умеет такую защиту ставить. Если есть аппрувер, то лучше спрятать основную ветку за Merge Request (он же Pull Request в терминологии GitHub)
Ежедневный релиз, цели и принципы процесса
Здесь хочу рассказать об одном из вариантов организации процесса – ежедневном релизе. Я намеренно выбрал именно такую экстремальную релизную модель, чтобы преимущества работы с гитом подсветились ярче.
Цель ежедневного релиза – поддержать разработку с ежедневным релизом готовых задач в условиях частых релизов и смены приоритетов, а значит и с переключением разработчиков с задачи на задачу. Разработчик должен иметь возможность начать задачу, бросить ее, переключиться на другую, а потом вернуться к старой. При этом он должен не запутаться, а в релиз не должен попасть лишний, не протестированный код.
Репозиторий должен быть легким и быстрым.
Для начала мы хотим снизить количество сложных слияний и вызванных ошибок. Мы не хотим с помощью Git создавать ошибки, мы хотим их предотвращать. Для этого мы должны избавляться от долгоживущих веток разработки, которые успевают сильно устареть к моменту их вливания в основную ветку. И сохранить максимальную простоту процесса.
Реализация процесса: шаги и инструменты
Поговорим про принципы, а потом пробежимся по шагам.
-
Первый принцип очень простой – из конфигурации убирается все, что требует версионирования. Конфигурация хранит в себе только структуру объектов, хранящих данные, подсистемы и в некоторых случаях роли (если мы хотим создать новую роль, затрагивающую большое количество типовых объектов).
-
Все остальное – доработка типовых форм, формы новых объектов, все модули – делается только в расширении. Если вы работали с расширениями, у вас наверняка уже есть своя система, как вы эти расширения создаете, как делите код между ними.
-
Одно расширение должно жить в одном репозитории. Не нужно засовывать разные расширения в один репозиторий, это совершенно ничего не даст, но может создать некоторую путаницу.
-
У вас наверняка есть какие-то миграционные обработки и отчеты – для них лучше завести отдельный репозиторий.
Теперь про шаги.
Каждый шаг начинается с подготовки контекста. Мы вытянули основную ветку, все ее последние изменения, с сервера, после чего создали ветку feat/01 – и переключились на нее. Таким образом, мы обновили и зафиксировали контекст, с которого начнется разработка по задаче. После чего загрузили этот контекст в базу разработки загрузкой файлов 1С или, например, с помощью precommit1c, который использует ту же загрузку файлов, собрали расширение (precommit1c из коробки не умеет собирать расширения, но научить его этому несложно).
Теперь мы поработали в конфигураторе, сделали какую-то разработку или ее часть и, допустим, хотим отдаться на тест. Или нам нужно переключиться на другую задачу. Соответственно, мы хотим «сохраниться» – положить куда-то текущий контекст, чтобы потом к нему вернуться. А это значит, что мы хотим сделать коммит. Готовим контекст для выгрузки, убедившись, что находимся на нашей ветке, выгружаем доработки из базы напрямую или, в случае использования precommit1c, выгружаем cfe, и делаем коммит. Если выгрузили cfe, precommit1c все нам разберет на файлы.
Вообще на этом инструменте хотелось бы немного остановиться. Precommit1c, на мой взгляд, замечательный инструмент, который мы используем в основном для каких-то внешний обработок и отчетов. Это скрипт, который фактически сидит на хуке (обработчике события) pre-commit (перед записью коммита) и превращает бинарные erf, epf, cfe в исходные файлы, а также при должной доработке выполняет разные манипуляции. Мы, например, научили его брать большие модули больших миграционных обработок и разрезать их по областям, а потом обратно склеивать при сборке. Это сильно облегчает коллективную работу над одной и той же обработкой. Другой вариант доработки – автоматическая сортировка дерева конфигурации.
Итак, мы выгрузили конфигурацию из файла, сделали коммит и отправили свою ветку на сервер, просто чтобы она не лежала на машине разработчика. И, допустим, приготовились поработать над другой, более срочной задачей.
Для переключения снова подготовили контекст: обновили основную ветку и на ее основе создали вторую ветку для разработки – feat/02. Переключились на нее и далее пошли работать по циклу предыдущей задачи: загрузка в конфигуратор, разработка, выгрузка, коммит.
После окончания разработки может быть код-ревью (под него создали для своей ветки Merge Request, если это практикуется на проекте). Дальше тестирование и после него перенос в основную ветку. То есть мы делаем следующее:
-
Обновили контекст основной ветки
git switch master && git pull --rebase
(Можно короче, но так нагляднее) -
Сделали rebase feat-ветки на master, чтобы «подтянуть» наши доработки к свежему состоянию основной кодовой базы и исправить возможные конфликты именно на своей ветке:
git rebase master feat/01 -
Делаем слияние master и feat/01 (через fast-forward или merge-коммит)
git merge feat/01 -
Отправляем master на сервер
git push origin master
Все эти шаги может заменить обработка Merge Request в интерфейсе сервера – но только если ветка не успела устареть настолько, что появились конфликты слияния. В таком случае rebase и решение конфликтов надо будет делать локально.
Релиз и тегирование, завершение процесса
Кстати, когда мы говорим «ежедневный релиз», это не значит, что у вас уже есть прод. Вы можете работать еще до запуска и делать релизы в какой-нибудь препрод. Здесь все зависит от того, как устроен проект.
-
Получили контекст основной ветки
git switch master && git pull --rebase
(Можно короче, но так нагляднее) -
Поставили на коммит тег и отправили на сервер
git tag –a r-2024.10.10 –m “daily release 2024.10.10” && git push origin tag r-2024.10.10
-
Загрузили контекст в целевую базу:
-
обновили конфигурацию из хранилища,
-
собрали расширение скриптом сборки,
-
ИЛИ
-
напрямую загрузили из файлов.
GitLab Flow – продвинутый подход к управлению ветками
Выше я описал максимально простой и экстремальный процесс – в нем нет qa-процесса перед релизом, совмещены продуктивная и основная ветки. Но уже в таком виде он может выполнять задачу изоляции недоделок от основной ветки – и целевой для релиза базы.
Коротко расскажу еще об одном чуть более совершенном и распространенном процессе – GitLab Flow (не путать с GitFlow!)
Он также работает с основной веткой master (или main) и ветками feat.
Дальше идут отличия. Даже после rebase feat-ветка никогда не вливается в основную через fast-forward. Всегда создается коммит-смычка, merge-коммит, чтобы в истории навсегда остался момент, в который ветка разработки примкнула к основной ветке.
При этом, в зависимости от потребностей проекта, добавляются дополнительные ветки, которые «ползут» за основной, ответвляясь хотфиксами: это продуктивный контур и тестовый staging (или столько контуров тестирования, сколько предусмотрено в проекте). Причем процесс поддерживает параллельное ведение нескольких версий продуктивных и тестовых веток.
Если возникает необходимость в hotfix, GitLab Flow тоже с этим справляется. Часто ветка hotfix создается напрямую от продуктивной ветки, если исправлять нужно именно на копии продуктивного кода. В hotfix-ветке вносим изменения, проверяем их, выкатываем на прод (возможно, через qa-ветку), затем спускаем изменения в основную ветку master через cherry-pick (копирование коммита на новое место).
На этом статья заканчивается. Главной ее целью было показать реальное, ощутимое преимущество работы Git даже в конфигураторе – для любых команд в любых условиях. Надеюсь, что тем, кто еще не работал с ним, этот материал дал пищу для размышлений. Главное – помнить: за каждым стартом всегда идет развитие. Все зависит только от нас – стоит лишь начать.
*************
Статья написана по итогам доклада (видео), прочитанного на конференции INFOSTART TECH EVENT.
Вступайте в нашу телеграмм-группу Инфостарт