Если вы хоть раз пытались вести «одно расширение на несколько конфигураций» (УНФ/Розница/УТ/КА/ERP) в Git, то знаете три чувства: diff, merge и «почему опять конфликт в Configuration.xml». Мы прошли этот квест на проекте 1C AI Autofill и собрали рабочую схему, которую можно забрать себе.

Дисклеймер: если я говорю «мы», это значит я и мои агенты в Cursor IDE. Они тоже участвуют в мерже — и тоже нервничают, когда конфликтов в Configuration.xml больше, чем изменений в коде.

Что это за проект (в двух строках): расширение для 1С:Предприятие 8.3, которое помогает генерировать продающие описания номенклатуры и (по настройкам) заполнять доп. реквизиты/характеристики с помощью AI. Исходники открыты, релизы лежат на GitHub, а логика разработки — внятная и повторяемая.

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

Коротко (TL;DR)

  • main — ядро (вся логика и «истина»), а ветки конфигураций — тонкие адаптеры (в основном отличаются xml/Configuration.xml).

  • git worktree — чтобы держать одновременно несколько веток рядом и не переключаться туда-сюда.

  • Только XML-выгрузка — чтобы diff был читаемым, а merge — предсказуемым.

  • Сборка автоматизирована (PowerShell + 1cv8 DESIGNER): из XML → .cfe → загрузка → обновление БД (опционально).

Мем-пауза, чтобы пережить merge

Голлум: «Моя прелесть — merge без конфликтов»
Голлум: «Моя прелесть — merge без конфликтов»

Моя прелесть — merge без конфликтов.

Содержание: Git-схема · Почему XML · Фрагменты кода · Сборка .cfe · Чек-лист · Планы · Как контрибьютить · Что сделать прямо сейчас · Ссылки

0. Для кого статья

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

  • Те, кто хочет «как в нормальном мире»: GitHub, релизы, воспроизводимая сборка, минимум ручных действий.

  • Те, кто устал от EDT-артефактов и конфликтов, когда «вроде ничего не менял, а изменилось всё».

1. Git «по-взрослому»: одно ядро и несколько веток конфигураций

Фраза «main — это состояние души» смешная ровно до первого релиза. Потом она становится правилом: в main живёт ядро, а ветки конфигураций периодически подтягивают изменения и собирают свои .cfe.

Структура веток

В репозитории держим несколько веток/сборок:

  • main — ядро расширения: полная XML-выгрузка (включая Configuration.xml и ConfigDumpInfo.xml).

  • UNF_Rozn — сборка для УНФ и Розницы.

  • UNF_Rozn_Fresh — сборка для 1cfresh (отличается, в том числе, именем расширения).

  • UT_KA_ERP — сборка для УТ, КА и ERP.

Где обычно отличия: в xml/Configuration.xml — имя конфигурации (<Name>) и иногда версия (<Version>). Всё остальное — как в main (если не было осознанного точечного отличия под конкретную конфигурацию).

Worktrees: «хочу видеть всё сразу»

Вместо вечного git checkout — отдельный worktree на каждую ветку. Это резко снижает количество «ой, я правил не там».

git worktree add ../worktrees/UNF_Rozn UNF_Rozn
git worktree add ../worktrees/UT_KA_ERP UT_KA_ERP

Поток разработки (как живём каждый день)

  • Фичи и общая логика — делаем в main.

  • Ветки конфигураций — периодически делают git merge main, разрешают конфликты по правилам и выпускают свой релиз.

  • Редкие иск��ючения: если правка нужна только одной конфигурации — она живёт в её ветке (и вы заранее понимаете, что на следующем merge это может дать конфликт).

Merge main → ветка конфигурации

Команды максимально скучные (а это хорошо):

cd worktrees/UNF_Rozn
git merge main

Конфликты: быстрые правила, чтобы не «вспоминать проект по ходу мержа»

Если Git не знает, кого любить — пусть любит ваши правила:

  • xml/Configuration.xml: оставляем своё значение <Name> (и при необходимости <Version>) для ветки конфигурации.

  • xml/ConfigDumpInfo.xml: оставляем свою версию/идентификаторы (или разрешаем по смыслу, если менялись осознанно).

  • Модули, формы, общая логика: почти всегда берём из main (main — источник правды).

Подробно (и тем же смыслом, но в формате «инструкции») это уже оформлено в репозитории: MERGE.md.

2. Почему «чистая XML-выгрузка» выигрывает у EDT в таких репозиториях

EDT мы пробовали. Оно умеет многое, но для сценария «одно ядро → несколько веток конфигураций → частые мержи» цена оказывается слишком высокой.

Что обычно ломает кайф

  • Лишние артефакты в репозитории (служебные файлы, метаданные).

  • Тяжёлый diff: «как понять, что реально изменилось?»

  • Merge как квест: конфликтов больше, чем изменений.

Что делаем вместо

Храним расширение в чистой XML-выгрузке (каталог xml/). В результате:

  • diff читается глазами разработчика 1С;

  • merge становится процедурой, а не приключением;

  • репозиторий ос��аётся прозрачным (без «а это что за файл?»).

Следы эпохи EDT обычно выглядят так (и да, их лучше игнорировать):

# EDT/Eclipse служебные файлы
.metadata/
.project
.classpath
.settings/
*.launch

3. Как выглядит код: два фрагмента, чтобы было «осязаемо»

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

Сервер: HTTP + JSON, получение ответа и нормальная ошибка

Функция ОтправитьЗапрос(Промт) Экспорт
	
	СтрокаJSON = СериализоватьЗапросВJSON(Промт);
	Соединение = ПолучитьHTTPСоединение();   
	Запрос = Новый HTTPЗапрос(ПолучитьПутьChatCompletions(), ПолучитьHTTPЗаголовки());  
	
	Запрос.УстановитьТелоИзСтроки(СтрокаJSON);
	Результат = Соединение.ОтправитьДляОбработки(Запрос); //POST
		
	Если Результат.КодСостояния=200 Тогда 
		ЧтениеJson = Новый ЧтениеJson;
		ЧтениеJson.УстановитьСтроку(Результат.ПолучитьТелоКакСтроку());
		РезСтруктура = ПрочитатьJSON(ЧтениеJson);
		
		МассивРезультатов = РезСтруктура["choices"];			
		Если МассивРезультатов <> Неопределено 
			И МассивРезультатов.Количество() 
			И ТипЗнч(МассивРезультатов[0]) = Тип("Структура")
			И МассивРезультатов[0]["message"] <> Неопределено
			И ТипЗнч(МассивРезультатов[0]["message"]) = Тип("Структура") Тогда
			Ответ = СокрЛП(МассивРезультатов[0]["message"]["content"]);	
		Конецесли;
	Иначе 
		СообщениеобОшибке = "Не удалось получить ответ. Код ошибки: "+Результат.КодСостояния +" "+ Результат.ПолучитьТелоКакСтроку();  
		ВызватьИсключение СообщениеобОшибке;
	КонецЕсли; 
	
	Возврат Ответ;
	
КонецФункции  

Функция СериализоватьЗапросВJSON(ТекстЗапроса)	
	Настройки = НастройкаПоУмолчанию();
	Сообщения = Новый Массив;
	Сообщение = Новый Структура;
	Сообщение.Вставить("role", "user"); 
	Сообщение.Вставить("content", ТекстЗапроса); 
	Сообщения.Добавить(Сообщение); 
	
	Структура = Новый Структура;
	Модель = Настройки.МодельИИ;
	Если НЕ ЗначениеЗаполнено(Модель) Тогда
		Модель = "auto";
	КонецЕсли;
	Структура.Вставить("model", Модель);
	Структура.Вставить("messages", Сообщения);
	
	ЗаписьJSON = Новый ЗаписьJSON;
	ЗаписьJSON.УстановитьСтроку();
	ЗаписатьJSON(ЗаписьJSON,Структура);
	
	Возврат ЗаписьJSON.Закрыть();
	
КонецФункции

Клиент: длительная операция, чтобы форма не зависала

Когда пользователь нажимает «Заполнить ИИ», запрос может выполняться заметно долго. Поэтому — длительная операция: запускаем на сервере и ждём с окном ожидания.

&НаКлиенте
Процедура GPT_ЗаполнитьОписание(Команда)  
	
	СостояниеНастроек = GPT_ОбщийВызовСервера.GPT_СостояниеНастроек();
	Если НЕ СостояниеНастроек.Готово Тогда
		ОткрытьФорму("ОбщаяФорма.GPT_Регистрация", , ЭтотОбъект, , , , , РежимОткрытияОкнаФормы.БлокироватьОкноВладельца);
		Возврат;
	КонецЕсли;
	
	GPT_ОтправитьЗапросНаСервере();
	
	ОписаниеОповещения = Новый ОписаниеОповещения("ПолучениеОтветаChatGPT", ЭтотОбъект);
	ПараметрыОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ЭтотОбъект);
	ПараметрыОжидания.ВыводитьОкноОжидания = Истина;
	ДлительныеОперацииКлиент.ОжидатьЗавершение(ЭтаФорма.GPT_ДлительнаяОперация, ОписаниеОповещения, ПараметрыОжидания);	

КонецПроцедуры

&НаСервере
Функция GPT_ОтправитьЗапросНаСервере()
	
	ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияФункции(Новый УникальныйИдентификатор);
	ПараметрыВыполнения.ОжидатьЗавершение = Ложь; 

	ЭтаФорма.GPT_ДлительнаяОперация = ДлительныеОперации.ВыполнитьФункцию(
		ПараметрыВыполнения,
		"GPT_ОбщийМодульСервер.СгенерироватьОписаниеИДопСвойстваНоменклатуры",
		Объект.Ссылка
	); 
		
КонецФункции

4. Автоматизация сборки: .cfe без рутины «LoadCfg / UpdateDBCfg вручную»

Ручная сборка .cfe — это десятки одинаковых действий, умноженные на количество веток. Автоматизация убирает человеческий фактор: скрипт делает всё одинаково каждый раз.

Типовая схема: main → merge в ветки → скрипт собирает .cfe для каждой ветки → (опционально) можно повесить на CI.

Пример скрипта (PowerShell): параметры

Смысл простой: путь к платформе, строка подключения, путь к XML, куда положить .cfe, имя расширения и флаги.

param(
    [string]$PlatformExe = 'C:\Program Files\1cv8\8.5.1.1150\bin\1cv8.exe',
    [string]$ConnectionString = '',
    [string]$XmlPath = (Join-Path (Split-Path $PSScriptRoot -Parent) 'xml'),
    [string]$ExtensionCfePath = (Join-Path (Split-Path $PSScriptRoot -Parent) 'bin\Расширение.cfe'),
    [string]$ExtensionName = 'GPT_ОписаниеНоменклатуры',
    [switch]$BuildFromXml,
    [switch]$SkipLoadExtension,
    [switch]$SkipDbUpdate,
    [switch]$SkipRunClient,
    [switch]$Wait
)

Сборка .cfe из XML: два вызова DESIGNER

# Сборка .cfe из xml (если -BuildFromXml)
if ($BuildFromXml) {
    $xmlFullPath = [System.IO.Path]::GetFullPath($XmlPath)
    $loadXmlArgs = $baseArgs + @('/LoadConfigFromFiles', $xmlFullPath, '-Extension', $ExtensionName)
    Invoke-Platform -Arguments $loadXmlArgs -OperationName 'Сборка: загрузка расширения из XML в конфигурацию'

    $dumpArgs = $baseArgs + @('/DumpCfg', $cfeFullPath, '-Extension', $ExtensionName)
    Invoke-Platform -Arguments $dumpArgs -OperationName 'Сборка: выгрузка расширения в .cfe'
}

Загрузка .cfe и обновление БД (если нужно)

$loadArgs = $baseArgs + @('/LoadCfg', $cfeFullPath, '-Extension', $ExtensionName)
Invoke-Platform -Arguments $loadArgs -OperationName 'Загрузка расширения из .cfe в конфигурацию'

$updateArgs = $baseArgs + @('/UpdateDBCfg', '-Extension', $ExtensionName)
Invoke-Platform -Arguments $updateArgs -OperationName 'Обновление конфигурации БД'

Полный цикл одной командой обычно выглядит так: .\update-extension-and-run-db.ps1 -BuildFromXml. Скрипт соберёт, загрузит, обновит (если не отключено) и может запустить клиент (если предусмотрено вашей обвязкой).

5. Мини-чек-лист релиза (чтобы не держать всё в голове)

  • В main: влитые фичи/фиксы, при необходимости обновлён <Version> в xml/Configuration.xml.

  • В ветке конфигурации: git merge main + конфликты по правилам (имя/версия свои, логика из main).

  • Сборка: скрипт → получаем .cfe (и при необходимости прогоняем установку в тестовой базе).

  • Публикация: релиз в GitHub (отдельно на каждую ветку/сборку).

6. Планы по развитию

Самое очевидное следующее улучшение — автоматизированное тестирование, чтобы релизы не держались на «проверил руками и вроде ок».

  • Автотест сборки: воспроизводимая сборка .cfe из xml/ (скриптом) в CI.

  • Smoke-тест установки: установка/обновление расширения в тестовой базе и проверка, что оно подключилось без ошибок.

  • Проверка базовых сценариев: «кнопка есть», «запрос ушёл», «ошибка показывается понятно» (минимум, который ловит регрессии).

Ссылки

Заключение

Опенсорс на 1С — это не «страшно и больно», если у вас предсказуемая структура: main как ядро, ветки как сборки, чистая XML-выгрузка и скрипт сборки. Остальное — дисциплина и пару раз пережить первый merge.