FeatureBranch

Original author: Martin Fowler
  • Translation
С распространением распределенных систем управления версиями (DVCS), таких как Git и Mercurial, я все чаще вижу дискуссии на тему правильного использования ветвления(брэнч) и слияния(мердж), и о том, как это укладывается в идею непрерывной интеграции (CI). В данном вопросе есть определенная неясность, особенно когда речь заходит о feature branching (ветвь на функциональность) и ее соответствие идеям CI.

Простой (изолированный) Feature Branch

Основная идея feature branch заключается в создании нового брэнча, когда вы начинаете работать над какой-то функциональностью. В DVCS вы делаете это в своем собственном репозитории, но те же принципы работают и в централизованных VCS.

Я проиллюстрирую свои мысли следующим рядом диаграмм. В них основная линия разработки (trunk) отмечена синим, и двое разработчиков, отмеченные зеленым и фиолетовым (Reverend Green и Professor Plum).

image



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

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

Основное преимущество feature branching заключается в том, что каждый разработчик может работать над своей задачей и быть изолированным от того что происходит вокруг. Они могут сливать изменения из основной линии в своем собственном темпе и быть уверенными, что это не помешает разрабатываемой функциональности. Более того, это дает возможность команде выбрать, что из новых разработок внести в релиз, а что оставить на потом. Если Reverend Green опаздывает, мы можем предоставить версию только с изменениями Professor Plum. Или же мы можем наоборот, отложить дополнения профессора, быть может потому что мы не уверены, что они работают так, как мы хотим. В данном случае мы просто попросим профессора не сливать свои изменения в основную линию, до тех пор пока мы не будем готовы выпустить его функциональность. Такой подход дает нам возможность проявлять избирательность, команда решает какую функциональность сливать перед каждым релизом.

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

image

Хотя разработчики могут работать над своей функциональностью в изоляции, в какой то момент результат их трудов должен быть интегрирован. В нашем примере Professor Plum с легкостью обновляет основную линию своими изменениями, слияния нету, ведь он уже получил все изменения в основной линии в свою ветвь (и прошел билд). Однако не все так просто для Reverend Green, он должен слить все свои изменения (G1-6) с изменениями Professor Plum (P1-5).

(В этом примере многие пользователи DVCS могут почувствовать что я пропускаю многие детали, в таком простом, даже упрощенном объяснении feature branching. Я объясню более сложную схему позже.)

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

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

Проблема которая беспокоит нас сильнее, это семантические конфликты. Самым простым примером может быть тот случай, в котором Professor Plum изменяет имя метода, который Reverend Green вызывает в своем коде. Инструменты для рефакторинга помогут вам переименовать метод без проблем, но только в вашем коде. Поэтому, если G1-6 содержат новый код, который вызывает foo, Professor Plum не узнает об этом, поскольку это изменение не находится в его брэнче. Осознание того, где собака зарыта к нам прийдет только в большом мердже.

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

Последствием страха перед большими мерджами является нежелание реафакторинга. Содержать код в чистоте требует постоянных усилий и чтобы в этом преуспеть каждый должен прибирать мусор когда его видит. Однако, такой рефакторинг в feature branch проблематичен, постольку поскольку он делает Большой Страшный Мердж еще больше и страшнее. В результате разработчики боятся рефакторинга как огня и код обрастает уродами.

В приведенной выше проблеме я вижу основную причину, по которой feature branching является плохой идеей. В тот момент когда команда боится рефакторинга для поддержания здорового кода — они в продолжительном пике без шансов на элегантный выход.

Непрерывная интеграция

Именно эти проблемы и должна решать непрерывная интеграция. С CI моя диаграмма будет выглядеть так.
image

Здесь намного больше мерджей, но слияние это одно из тех вещей которые лучше делать понемногу часто чем редко и тоннами. В резулттате если Professor Plum изменяет часть кода на котором зависит Reverend Green, наш зеленый коллега выяснит это намного раньше, в мерждах P1-2. На данный момент ему нужно изменить G1-2 для работы с этими изменениями, вместо G1-6 (как это было в прошлом примере).

CI эффективен для нейтрализации проблем больших мерджей, но кроме этого это еще и критически важный коммуникационный механизм. В данном сценарии потенциальный конфликт проявится, когда Professor Plum сольет G1 и поймет что Reverend Green использует библиотеки профессора. Тогда Professor Plum может найти Reverend Green и вместе они могут обсудить взаимодействие их функциональности. Быть может функциональность Professor Pum требует некоторые изменения, которые не уживаются с функциональностью Reverend Green. Вдвоем они могут принять намного лучшие решения по дизайну, которые не помешают их работе. С изолированными брэнчами наши разработчики не узнают о проблеме до последнего момента, когда часто уже поздно чтобы решить конфликт безболезненно. Коммуникация одна из ключевых факторов в разработке программного обеспечения и одно из главных свойств CI — это содействие ей.

Важно упомянуть что в большинстве случаев feature branching имеет другой подход к CI. Один из принципов CI в том, что все коммитят в основную линию каждый день, так что если feature branch живет больше чем один день — это превращает его в что то очень далекое от CI. Я слышал людей, говорящих что они используют CI потому что их билды бегут на CI сервере, на каждой ветви и на каждый коммит. Это непрерывная сборка, и это хорошо, но тут нету интеграции, поэтому это и не CI.

«Беспорядочная» интеграция

Ранее я сказал в скобках, что есть и другие способы feature branching. Скажем Professor Plum и Reverend Green в начале итерации вместе заваривают ароматный зеленый чай и обсуждают свои задачи. Они обнаруживают что среди задач есть взаимодействующие части и решают интегрироваться между друг другом вот так:

image

С таким подходом они сливаются с основной линией в конце, как и в первом примере, но они так же часто делают мерджи между собой, чтобы избежать Большого Страшного Мерджа. Идея в том что основное преимущество feature branching это изоляция. Когда вы изолируете изолируете ваши брэнчи, есть риск мерзкого конфликта, нарастающего вне вашего ведения. Тогда изоляция это иллюзия, которая болезненно разобьется раньше или позже.

Все же, это более трудоемкая интеграция является формой CI или речь идет совсем о другом звере? Я думаю, что они разные, опять же, ключевое свойство CI в том, что каждый интегрируется с основной линией каждый день. Интеграция среди feature branches, которую я с вашего позволения назову «беспорядочной интеграцией» (promiscuous integration, PI), не включает и даже не нуждается в основной линии. Я считаю что эта разница очень важна.

Я вижу CI в основном как средство для рождения release candidate на каждом коммите. Задача CI системы и процесс деплоймента опровергнуть готовность к продакшену текущего release candidate. Эта модель нуждается в какой то основной линии разработки которая представляет текущее состояние полной картины.

--Dave Farley


Беспорядочная интеграция vs непрерывная интеграция

И все же, если PI отличается от CI, то при каких случая PI лучше чем CI?

С CI вы теряете возможность использовать систему управления версиями для выборочного подхода к изменениям. Каждый разработчик влияет на основную линию, поэтому вся функциональность растет в ней же. С CI, основная линия должна всегда быть здоровой, и в теории (а часто и на практике) вы можете делать релиз после каждого коммита. Имея полузаконченную функциональность, или функциональность которую вы предпочитаете не выпускать, вы не повредите функциональности всей системы, но потребует кое какой маскировки, чтобы спрятать это от пользовательского интерфейса, как например не включение нового пункта в меню.

В таких случаях PI может предоставить что то посередине.Это позволяет Reverend Green выбрать когда принять изменения Professor Plum. Если Professor Plum делает какие то изменения в API ядра системы в P2, Reverend Green может импортировать P1-2 но оставить остальные, до тех пор пока Professor Plum не закончит свою работу и не сольет в основную ветвь.

Однако в общем я не считаю что выборка функциональности для релиза с помощью VCS это хорошая идея.

Feature branching это модулярная архитектура для нищих, вместо того чтобы строить систему с возможностью легкой замены функциональности при райнтайме/деплойменте, люди привязывают себя к source control для этого механизма через ручной мердж.

Dan Bodart


Я предпочитаю проектировать программное обеспечение так, чтобы можно было включить и выключить функциональность при помощи изменения конфигурации. Для этого есть две полезные техники FeatureToggles и BranchByAbstraction. Они требуют от вас больше размышлений на тему того, что и как разделить на модули и как контролировать эти варианты, но мы пришли к выводу что результат намного более аккуратен, чем тот что выходит, если надеяться на VCS.

Что больше всего меня беспокоит в PI это его подверженность способностям коммуникации внутри команды. С CI основная линия служит коммуникационной точкой. Даже если Professor Plum и Reverend Green никогда не разговаривали, они найдут возникающий конфликт в день его формирования. С PI они будут должны заметить то, что работают над взаимодействующим кодом. Постоянно обновляющаяся основная линия способствует уверенности каждому в том, что он интегрируется со всеми, не надо выяснять кто чем занимается, соответственно и меньше шансов на изменения, которые остаются скрытыми до поздней интеграции.

PI возник из опен-сорса и, предположительно, менее интенсивный темп опен сорс проекта может быть фактором для него. В работе на полную ставку вы работаете немало часов в день над проектом. Это позволяет работать над функциональностью с приоритетами. С опен сорсом люди часто жертвуют час тут и пару дней там. Функциональность может занять одному разработчику не мало времени для выполнения, в то время когда другие, с большим количеством свободного времени, смогут довести свои изменения до приемлемого качества раньше. В такой ситуации выборочный подход может быть более важен.

Важно осознавать, что инструменты, которыми вы, пользуетесь не зависят от стратегии которую вы выбираете. Несмотря на то, что многие ассоциируют DVCS с feature branching, они могут быть использованы и с CI. Все, что вам нужно сделать, это пометить одну из веток как основную линию. Если все делают pull и push в эту ветку каждый день, тогда у вас есть самая что ни на есть основная линия. На самом деле, в хорошо дисциплинированной команде я предпочту использовать DVCS для CI проекта, чем централизованную VCS. С менее дисциплинированной командой я буду беспокоиться что использование DVCS подтолкнет людей к ветвям-долгожителям, в тот момент когда централизованная VCS и усложнение брэнчинга подтолкнет их к частым коммитам в основную линию.

P. S. От переводчика на изучение вопросов к подходам использования VCS меня сподвигла эта статья, благодаря которой я начал искать более подробные описания «правильного» использования бранчинга и натолкнулся на выше переведенный текст. Хотя я не претендую на качество перевода, мне просто хочется попасть в ленту к разработчикам и дать им повод задуматься от противоположного принятому в опен сорсе подходу (форкинг). Не бейте больно палками, но критикуйте конструктивно, я это делаю в первый раз :-).
Share post

Comments 27

    +2
    которую я с вашего позволения назову «нерегулярной интеграцией» (promiscuous integration, PI),

    Не «нерегулярная», а «беспорядочная». Как «беспорядочные половые связи», они же промискуитет.
      0
      Вы правы, так звучит значительно лучше. Исправляю.
      +1
      Постоянная интеграция избавляет от проблем с мержами, но хорошо ли в основную ветку сливать незаконченный функционал?
      У нас в проекте по этой причине есть 2 дополнительные ветки, помимо trunk: для тестироавния багфиксов, которые нашлись в стабильной версии и для тестирования нового функционала. Вот с ними идет постоянная синхронизация. Но при этом в стабильную версию(trunk) сливаются только исправления.
      А из функциональных веток уже создаются билды и потом один из них становится следующей стабильной версией.
        +1
        Мартин говорит, что он предпочитает просто не включать его, т.е не добавлять обращение к методу, кнопку на экран, пунк в менюшку, открывать WS и т.д. Согласен что всегда может быть факап, но тот нету серебряной пули и надо плясать от различных факторов, среди которых количество разработчиков (100 кодеров фул тайм над одной системой без CI явно до добра не доведут), насколько взаимосвязаны компоненты системы над которой идет разработка (опять же, чем более связаны — тем больше нужда в CI).
        В моей прошлой фирме у нас был отдельный брэнч для релиза и перед каждым релизом в него мерджились из транка. Но тут возникала другая проблема, из за того что релиз был вечный народ чинил баги в релизе и не сливал назад в транк. К тому же мерджи из транка иногда делались «пинцетом», чтобы не вытащить то, что не хотели и в результате транк потихоньку терял свою актуальность. Я думаю, что классический, проверенный временем подход — транк для разработки, брэнч на каждый релиз — имеет наилучший баланс между всех зол. Все понимают что через две недели этот брэнч умрет, и поэтому чинят сначала в транке, чтобы не получить по башке за некромансию из старых брэнчей, с другой стороны люди будут писать легко включаемые — выключаемые фичи и это будет способствовать модулярному подходу (только тут есть опасность закоментированного кода в VCS, что выглядит ужасно). В Вашем случае у меня сложилось впечатление что ваш транк стал релиз кандидатом и люди в нем не работали, т.е не использовали как мэйнлайн и в результате не получали плюшки CI. Тут уже вопрос что важнее для каждой фирмы, на эту тему я уже рассуждал в прошлом абзаце :-)
          0
          Да, в транке работа практически не ведется, только исправляются ошибки, т.е. поддерживается его стабильность. А в качестве mainline используется ветка feature-testing, которая начинается от транка и используется для совместного тестирвания нового функционала.
          Однако мне кажется что держать функционал в mainline и контролировать его использование/неиспользование сложнее, чем просто собирать билды из feature-веток. Особенно если компоненты приложения слабосвязаны. Ну то есть можно наверное даже комбинировать подходы, просто вариант с ветками мне кажется более упорядоченым и очевидным. По крайней мере можно быстро посмотерть какие ветки слиты в билд и соотв. какие функции есть, а каких нет.
          0
          Многие используют подход "git flow" для ведения веток в git (перевод на Хабре, правд куда-то пропали картинки). Так, чтобы отделить незаконченный функционал от релиза, в git flow используется ветка develop для кода «в разработке», а в master сливаются уже стабильные версии.
            0
            Угу, похожая модель используется у нас. А за ссылочку спасибо)
          0
          Как это все напомнило мне времена бурного развития Cisco IOS… Каждое устройство имело с полтора десятка иногда взаимоисключающих фичерпаков, и все это помножено на несколько веток. Фраза «вам следует обновиться до более старой версии» была нарицательной…
            +6
            При подобной схеме мы получаем:
            а) постоянно поломанный trunc из-за влития нестабильного кода
            б) постоянно поломанные зелёную и фиолетовую ветки: баги из зелёной бесконтрольно приходят в фиолетовую, а баги фиолетовой — в зелёную
            в) нерабочий trunc до тех пор, пока обе feature ветки не будут доделаны и финально влиты в основную ветку

            А теперь представим, что по подобной схеме работают не два, а пять/десять/двацать/сто человек, и можно сразу вешаться.
            В итоге описанная схема приносит больше проблем, чем пользы. К тому же тут ещё применяется вводящяя в заблуждение терминология, «непрерывная интеграция» означает вовсе не это.

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

            Первый способ: от trunc/master ответвляется специальная для этих двух людей ветка, от которой они в свою очередь тоже ответвляются и периодически делают в неё мерж своих изменений, а так же периодически делают на неё ребейз, чтобы подхватить изменения соседа. При этом эту специальную ветку ни в коем случае нельзя ребейзить.
            Фактически это способ, описанный в статье, но применяемый не к trunc/master, а к отдельной ответвившейся от него ветке.

            Второй способ: ответвиться оба могут и от trunc/master, но когда становится ясно, что ведётся работа над пересекающимся функционалом, то зелёная ветка должна ребейзнуться на фиолетовую(либо наоборот). В таком случае фиолетовая ветка может продолжать работать как обычно, не имея никаких проблем, кроме запрета на ребейз своей ветки. А зелёная ветка теперь должна периодически делать ребейз на фиолетовую, правя пересекающийся код и получая чужие баги. Но страдает по этой схеме только один человек, зелёный, а не двое.

            Если кто знает способы лучше, пожалуйста, поделитесь.
              +1
              Вы говорите про CI? Если да, то

              а) если после CI у Вас поломанный транк, значит Вы что — то делаете не так. Каждый коммит сопровождается билдами и тестами, а если они не проходят, все идут дружно помогать тому кто сломал, потому что коммиты остальных зависят от него. Вся идея в CI как раз в вечно здоровом транке.

              б) баги сливаются вместе в транк, но там их в разы легче обнаружить, потому что если они живы, то их не нашли тесты и далее их могут найти только QA (в обычной схеме), но в случае CI есть большой шанс что их найдут еще раньше, другие разработчики которые работают с этими модулями. Всяко лучше чем ВНЕЗАПНО за неделю до релиза сломать мэйнлайн нафиг тонной изолированных багов, а потом иди выясняй кто виноват.

              в) Смотрите выше. Транк не нерабочий, он вполне проходит тесты и билд, хотя функциональность может быть еще не дописанной (но на это есть тоже ответ в статье).

              Я работал в фирме, у которой был (почти, не хватало тестов, но это уже другая проблема, хотя критичная, не спорю) CI, и команда из 80+ разработчиков как раз замечательно уживалась благодаря Jenkins и общему транку.
                +1
                Вот покрытие транка автоматизированными тестами настолько, чтобы быть уверенным в готовности транка к выкату в продакшн — это и есть святой Грааль continuous delivery (не CI). Прекрасный, но я ни разу не видел его достигнутым.
                  +5
                  Под поломанным «транком» и нестабильным кодом я имею в виду даже не ситуации, когда тесты падают, а ситуации когда просто код ещё не дописан до конца, когда либо какая-то логика не доделана, либо или не завершён интерфейс приложения, когда что-то ещё не работает как надо.

                  Ведь если бы всё работало, и фича была бы завершена и протестирована, то незачем было бы промежуточный мерж делать, можно было бы сразу ветку с фичей мержить, верно? А раз мы мержим какой-то промежуточный код, то он по определению ещё не готов и не стабилен.

                  Мало того, что при таком подходе нельзя и заикаться о том, чтобы в любой момент «транк» задеплоить на продакшен (ну или выпусть очередную версию для устанавливаемого приложения), так ещё и лично мне совсем непонятно, как вообще в такой ситуации можно выпускать стабильные релизы. Ведь когда все разработчики мержат свой недоделанный код в основную ветку, то получается, что в любой момент в «транке» находится куча промежуточного кода из различных веток с фичами.

                  Зачем весь этот геморрой, если можно, используя возможности гита/меркуриала, избежать его? И вообще, глядя на эту схему, у меня складывается впечателение, что люди, использовавшие ранее svn, затем пересели на гит(меркуриал?), но продолжили работать по старому, не разобравшись с тем, какие преимущества даёт возможность лёгкого бранчинга в связке с ребейзами.
                0
                А есть у кого опыт ведения проекта состоящего из нескольких подпроектов. Подпроекты это не просто отдельные компоненты ПО, это самостоятельные проекты на разных платформах/языках но при этом они все завязаны друг на друге в одну систему.
                К примеру, несколько мобильных платформ, сервер, БД, web. Интересно услышать об опыте ведения таких проектов в VCS. Какие существует стратегии или методики держать всю эту ораву разношёрстных проектов в одной узде.
                  0
                  А почему вы говорите, что это не отдельные компоненты, а самостоятельные проекты? В чем для вас различие?
                    0
                    возможно плохо написал. Под «самостоятельные» я имел в виду, что они ведутся разными разработчиками под под разные языки и платформы. По сути да, это компоненты одной системы.
                      0
                      И чем это отличается от работы с компонентами одной и той же системы? С точки зрения версионирования и бранчей — уж точно ничем.
                        0
                        Изменения компоненты в одной системе без изменения всех её использующих, скорее всего, приведет к ошибке компиляции, что CI быстро покажет. Изменения в подпроектах к ошибки компиляции в смежных продпроектах не приводят. А выявляются на этапе работы, т.е. грубо говоря, нужна полная функциональная регрессия. Полную функциональную регрессию построить не всегда удается, а у нас и подавно. И того при 4 звенной архитектуре для внедрения одной фичи нужно согласовать до 4 компонент.

                        В начале у нас был только транк, и это был, простите ужас. Получить билд системы, с полной согласованность по функционалу вообще нельзя было. Мы пришли к боле менее консенсусу. Что транк это свалка, и для каждого релиза у нас будет tag.
                        В начале мы пробовали сперва приводить в транке все подпроекты к одному состоянию далее делать срез транка, вот вам тег-релиз для тестирования. Но данный подход отпал из-за разной скорости реализации различных компонент. Теперь мы, как правило, вначале создаем tag и далее каждый подпроект переноситься туда по факту реализации в нем всех заявленных функций для данного тега. Это чуть получше, но тоже не удобно.

                        А еще важный момент, что сейчас все подпроекты лежат в общем для них тнаке. Что опять таки не очень удобно, но и разнести их в разные репы тоже не видится решением проблемы. Так как сейчас у нас как бы есть Мажорная версия проекта, которая лежит к корне транка и она явлется камнем согласования между компонентами, что мол компонента A версии 1.3 не совместима с компонентой B если она уже версии 1.4.
                        Вот, примерно такая каша и происходит. Было бы интерестно послушать тех кто уже прошел этот путь.
                          0
                          Возникающая у вас проблема связана с кросс-языковой разработкой, а не с feature branching. Решается она банально: заводятся метаданные, описывающие контракт взаимодействия между системами, после чего из этих метаданных первым этапом сборки генерятся файлы, используемые каждой системой. Дальше — стандартные практики CI.
                            0
                            Можно абстрактный пример в вакууме?
                              0
                              Можно реальный пример не в вакууме?
                                0
                                Две системы взаимодействуют через сервис. Описываем сервис в WSDL, на основании него генерим интерфейсы для клиента и сервера (язык не имеет значения), эти интерфейсы реализуем и соответствено потребляем. При любом изменении контракта — меняем WSDL, перегенеряем интерфейсы, перекомпилируем сервер и клиент, смотрим на тесты.
                                  0
                                  на основании него генерим интерфейсы для клиента и сервера (язык не имеет значения)

                                  Это как?
                                    0
                                    Ну, я исхожу из того, что для любого языка/платформы можно написать кодогенератор интерфейса из wsdl (если его нет встроенного в платформу).
                                      0
                                      Написать то можно. А что это по сути изменит? Только если при подключении клиента в первую очередь делать фулл валидацию WSDL (проверку всех интервейсов). Окей, а далее. Сервер идет в БД, WEB смотрит в БД, как их связать c WSDL?
                                        +1
                                        А зачем их связывать с WSDL?

                                        Например у нас для работы с БД используется ORM, и есть тест, который проверяет, что описание системы в коде соответствует структуре БД, поэтому расхождения тоже минимизируются. Ну и так далее.

                                        У вас связи должны быть попарно между компонентами, и на этих границах вы и придумываете контракты, которые вас страхуют. Везде, где у вас граница — нужно сформировать контракт и условия его проверки.

                                        PS Не при подключении, при каждой компиляции. Ваша же изначальная проблема звучала вот так:
                                        Изменения компоненты в одной системе без изменения всех её использующих, скорее всего, приведет к ошибке компиляции, что CI быстро покажет. Изменения в подпроектах к ошибки компиляции в смежных продпроектах не приводят.

                                        Я вам и рассказываю, как ее решить.
                                          0
                                          Чувствую какой я профан в многокомпонентных системах :( Опять я пытался «ремонтировать двигатель через выхлопную трубу». С другой стороны, мы сильно ограничены в ресурсах, и потянуть все эту махину это вообще невообразимо в нашем случае.

                Only users with full accounts can post comments. Log in, please.