Русский
Русский
English
Статистика
Реклама

Git workflow

Поддерживаем разработку нескольких версий продукта в Git. Станислав Лукьянов (GridGain)

28.10.2020 10:05:26 | Автор: admin


Всем привет! Меня зовут Станислав Лукьянов. Я работаю в компании GridGain. Сегодня я хотел поговорить о том, как мы поддерживаем старые версии в Git.



Сначала пара слов о том, что это будет за доклад, кому будет полезен и кто я такой.



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


Эта тема будет полезна:


  • Для тех, кто уже занимается продуктом с большим количеством старых версий и понял, какой это ад пытаться протащить фикс в свой мастер на несколько лет назад.
  • А также тем, кому интересно через что проходят вендоры, продуктами которых вы пользуетесь.
  • И даже если вы сегодня такой продукт не разрабатываете, то, может быть, это пригодится вам в будущем на каком-то другом проекте.

Дальше пару слов о компании GridGain. Тут будет пара слайдов, которые я стащил у наших маркетологов, но, поверьте, я веду к чисто технической теме.



Продукт GridGain это распределенная СУБД и платформа для обработки данных. Она основана на продукте, который называется Apache Ignite, который open source. Все, что есть в Ignite, на слайде выделено красным. И GridGain добавляет вокруг какое-то количество интерпрайзных фич, которые тут по бокам.


И плюс к этому занимается платной поддержкой и выпуском платных патчей для Ignite, чем и зарабатывает.



Кто этим пользуется? Пользуются этим вот эти ребята. Тут много всяких компаний. В основном это финтех, банки и те, кто их обслуживают.


И все, что их объединяет, это то, что это все жесткий и кровавый enterprise. В чем проблема с enterprise?



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



Он хочет обновляться как можно реже и затягивать как можно меньше патчей.



Из-за этого нам приходится поддерживать много старых версий. Много это сколько?


Вот такая у нас release model:



Тут я вывел версии, которые мы выпустили в наших активных ветках за последний год. Мы выпускаем minor-версии примерно раз в квартал. Поддерживаем их два года. Это означает, что мы поддерживаем порядка 8 minor-веток одновременно. В них мы выпускаем патчи. В среднем они выпускаются по одному в месяц на каждую minor-версию.


Если посмотреть на слайд, то получится, что у нас 5-10 версий выходят ежемесячно. Это довольно много.



В чем же проблема? В чем сложность, чтобы поддерживать все старые ветки?


Давайте пойдем наивно и попробуем сделать все нормально.



Есть у нас мастер, мы в него коммитим.



В какой-то момент захотели выпустить версию.



Повесили тэг на коммит, отдали в QA. QA прошел, не нашел никаких проблем. Версию выпустили.



Выпустили так еще одну.



И стали готовиться к следующей.



Пока версия была в QA, произошло следующее.



Сначала мы запилили еще что-то, т. е. какой-то фикс или фичу, которая попала нам в мастер, но которую мы не хотим в версии 1.3. А потом QA нашел в 1.3 какой-то баг, который мы тоже пофиксили.



Вопрос: Как теперь протащить B в 1.3?.


Самое очевидное решение это переставить тэг 1.3.



Кто видит в этом проблему? Естественно, все понимают, что такое scope creep. Мы переставили тэг. И у нас в 1.3 еще попал коммит А. Очевидно, что решение плохое.


Что мы еще можем сделать?



Можем заревертать А. Переставить после этого 1.3. И после выпуска 1.3 А закоммитить обратно в мастер. Но очень много для этого нужно делать телодвижений. Можно было вообще сказать, что надо было мастер зафризить, пока мы 1.3 не выпустим, но слишком много помех для текущей разработки.



Естественно, все знают в 2019-ом году, что мы хотим сделать branch под 1.3 и в отдельном branch проводить стабилизацию, т. е. там зафиксить B.


В той картине, как она выглядит сейчас, кто-то видит еще проблему? А она есть, потому что так, как мы это сделали сейчас, мы забыли дотащить B до мастера.



И как это сделать просто, и как дотащить B и туда, и туда, может быть, неочевидно.


Немножко усложним пример.



Скажем, что мы стали делать branches для каждой ветки, но вспомним, что релизы живут независимо. Выпустили первую версию 1.3.0. Прогнали через тестирование, выпустили.



Выпустили следующую, отбранчевались от нулевой.



Потом приготовились выпускать вторую.



И снова та же ситуация.



Есть коммит А, который мы не хотим в 1.3.2. Но скажем, что мы хотим его когда-нибудь протащить в ветку 1.3, но не сейчас, т. е. не в 1.3.2.


Очевидное решение это сразу сделать ветку 1.3.3. И туда все протащим, и все будет хорошо.



После этого мы снова находим какую-то проблему в 1.3.2, которая нам там нужна.



И теперь нам нужно ее протащить и в 1.3.2, и в 1.3.3.



И проблема здесь в том, что разработчик принимает очень много решений на основе того, что он знает о версиях, которые выпускаются. Он должен знать, что есть 1.3.2. Эта версия в QA и в нее надо тащить не все подряд. Есть 1.3.3., в которую надо тащить все подряд, что должно оказаться в ветке 1.3. Это довольно много информации.


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



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



Сформулируем проблемы, на которые мы посмотрели:


  • У нас есть релизы в разных состояниях: какие-то мы еще не начали, какие-то мы уже выпустили, а какие-то мы наполовину зафризили, они сейчас в стадии стабилизации и все подряд мы туда тащить не хотим.
  • У нас есть риск того, что, если мы что-то пофиксили в одной ветке, мы это не пофиксим в другой. И нужно этот риск как-то минимизировать.
  • Чем больше версий вы поддерживаете, тем больше веток у вас одновременно живет, а значит, что проблемы тоже множатся.


Из этого формулируем свои требования:


  1. Одновременные релизы не должны мешать друг другу и не должны мешать текущей разработке. Если нам надо закоммитить, то мы должны иметь возможность что-то закоммитить. Если нам надо выпускать релиз, вне зависимости оттого, что там еще мы выпускаем, мы имеем возможность забросить релиз в QA на стабилизацию.
  2. Мы недолжны терять консистентности между релизами. Если в старых релизах мы что-то пофиксили, то во всех новых это тоже должно быть пофиксено, потому что пользователь не должен видеть багов, которые для него уже один раз пофиксили.
  3. И все это должно масштабироваться:

  • В ширину на большое количество версий, которые мы захотим поддерживать 2-3-4 года.
  • В глубину часто мы хотим выпускать патчи для патчей. Мы выпускаем версии с тремя-четырьмя-пятью цифрами. У нас есть ветки, которые живут какое-то время, в которых у нас по пять цифр. И это бывает нужно.


Как построим разговор дальше?


  • Начнем с существующих подходов к работе с Git. Рассмотрим парочку популярных и посмотрим, как они работают с версиями.
  • Потом посмотрим на то, как мы строили собственный подход, какие есть развилки.
  • И расскажу о паре проблем, которые мы все еще решаем.


Начнем с Github Flow.



Кто знает, что такое Github Flow? Кто не знает, вы на самом деле знаете, просто не понимали, что пользовались Github Flow.


Это самый простой подход к работе к Git, который может быть.



У вас есть мастер. Когда вы хотите что-то пофиксить, вы делаете feature branch, либо bugfix branch.



Когда закончили, то через pull request при integration testing review вмерживаете все в мастер.



Требования, которые выделяет Github Flow это: мастер должен быть постоянно стабилен и готов к deploy. Если у вас современный CI/CD, то, скорее всего, на каждый коммит в мастер, на каждый merge либо вы делаете deploy в production, либо принципиально можете сделать deploy в production, т. е. никакой стабилизации нет.



И каждый раз, когда вы что-то вмерживаете, можно сказать, что вы выпускаете следующую версию, потому что мастер постоянно стабилен.



Как Github Flow справляется с требованиями, про которые мы говорили?


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

Github Flow нам про версии вообще ничего не говорит и каких-то инструментов, чтобы поддерживать старые версии нам не дает.


Понятно, что это основной способ, как мы все работаем с Git. Фиксы в feature branch и merge в мастер с этого будем начинать. А снаружи будем накручивать все, что связано с выпуском версий.



Git Flow еще один очень популярный процесс. Он поддерживает две главных ветки, помимо мастера есть еще ветка develop.


Мастер это также постоянно стабильная штука, которую можно с любого места деплоить.


Develop это нестабильная ветка, в которой аккумулируются изменения.


Feature branches создаются из develop.



Потом вмерживаются в него обратно.



Когда мы готовимся сделать релиз, мы делаем release branch, в нем проводим стабилизацию.



Когда стабилизация релиза закончилась, мы вмерживаем его в мастер и обратно в develop, т. е. делаем сразу два merge.



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


Кроме обычных релизов, есть еще понятие hotfix релизов.



Отличие от обычных в том, что branch делается не от develop, а от головы мастера. Делаем такой hotfix, а также делаем там стабилизацию. И после этого также вмерживаем в мастер и в develop.



Вот и весь процесс.



Как он справляется с нашими требованиями?


  1. Релизы не мешают изменениям, потому что мы используем release branches. Мы этому научились в самом начале.
  2. Сохраняется ли консистентность между ветками, которые мы поддерживаем? И да, и нет.

  • С одной стороны, мы вмерживаем релизные branches, hotfix branches обратно в develop и в мастер. И это хорошо. Это значит, что их изменения мы не потеряем, по крайней мере, в develop.
  • Но если у вас есть несколько одновременно живущих release branches или вы одновременно готовите hot fix и релиз, то процесс не дает какого-то инструмента, как вам их синхронизировать. Скажем, у вас есть hot fix, который вы готовите с каким-то критическим патчем и какой-то релиз. Hot fix по этому процессу в релиз вмержен не будет. Вам нужно что-то уже накручивать поверх Git Flow для того, чтобы этот коммит не потерять.

  1. А как поддерживать большое количество версий Git Flow нам ничего не говорит. Он предполагает, что мы поддерживаем один hot fix, один релиз и желательно не одновременно.


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



Я дальше буду использовать сквозной пример. У нас есть мастер и 5 версий, которые мы поддерживаем.


К нам пришел кастомер и сказал, что у него есть проблема в версии 1.3. В нее нам нужно что-то закоммитить.


Разберемся, как мы это будем делать. Какие у нас есть развилки?



Во-первых, мы договорились, что мы хотим избегать регрессий. В нашем примере это значит, что, если мы хотим сделать вот так и закоммитить что-то в 1.3.



То мы на самом деле хотим это закоммитить вот так сразу в мастер, в 1.5, в 1.4, 1.3.


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


Тогда у нас остаются два логичных подхода: master-first и target-first.



Master-first или upstream first начинаем с мастера, а потом по очереди промерживаем во все предыдущие ветки.


Ветки не пропускаем. Если хотим промержить в 1.3, то обязаны это сделать сначала в 1.4, а, чтобы сделать в 1.4, надо сделать в 1.5 и т. д.


Какие плюсы?


  • У нас нет моментов, когда мы нарушаем консистентность между релизами. Если у нас фикс есть в более старой ветке, значит мы до этого его уже сделали в более новой ветке. Такой ситуации, когда мы выпустили релиз кастомеру, отдали ему bugfix, а потом отдали ему какой-то релиз из более новой ветки и он этого bugfix не увидел, не будет с этим подходом.


  • Изменения идут только в одну сторону. И это небольшой плюс, потому что это помогает логически разработчикам понимать процесс.



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



Другой противоположный подход target-first.


Начинаем с фикса в 1.3 и поднимаемся вверх. И если нам вдруг надо, спускаемся вниз.


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


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


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


Вот это небольшое сравнение:



Понятно, что в чем хорошо один, в том плох другой. Они антиподы друг для друга.


Мы для себя выбрали master-first, потому что консистентность превыше всего. И мы готовы за нее заплатить иногда более долгим фиксом в той версии, где этот фикс нам нужен прямо сейчас.



Тот пример, который у нас уже был ранее, когда у нас есть какая-то версия 1.3.2, которая уже в QA и нам нужно было протащить фикс А в ветку 1.3, но 1.3.2 уже был в QA, поэтому мы создали dev-ветку 1.3.3 для того, чтобы аккумулировать изменения там.


И после этого у нас два активных релиза в одной и той же ветке: 1.3.2 и 1.3.3.


Вопрос: Как разработчик принимает решение, что ему надо тащить?. Если он знает, что коммит нужен в какой-то версии в рамках ветки 1.3, то как он принимает решение, в какие именно релизы коммит протащить? Это достаточно сложно.



Решение, которое мы для себя придумали, это релиз мастер branches.


Первым для ветки 1.3 создается branch, который называется 1.3-master. Он играет для 1.3 такую же функцию, как мастер играет для всего проекта. Т. е. аккумулирует вообще все-все изменения.


В тот момент, когда нам нужно зарелизить что-то, мы делаем ветку от этого branch, от 1.3-master. Поэтому разработчик может видеть только мастер, протаскивать фиксы только туда. В нужный момент release engineers сделают ветку от этого 1.3-master. Ветки релизов в состоянии development, когда они еще не переданы в QA, нам в этом случае не нужны, потому что изменения могут накапливаться в 1.3-master.


А если нам нужно принести какой-то фикс во время QA, например, как здесь нам нужно было перенести фикс B в 1.3.2, то это делает release engineer, а не разработчик. Release engineer знает о scope релиза, за который он отвечает. У него есть доступ к этой ветке, он может сделать этот cherry-pick.



Следующая минорная, но все же развилка это как нам отмечать версии? Что я имею в виду?



Есть вариант, когда мы на каждую версию создаем по branch. Нужны ли нам там стабилизационные коммиты, находит ли QA проблемы или не находит все равно создаем по branch.



Часто предлагают вместо branches стараться использовать тэги, как это делает, например, Github Flow, когда мы просто на мастере ставим тэги. И только, если нам потребовалась стабилизация для какой-то версии, тогда мы уже делаем для нее branch.



Тут просто:


  • Тэги более легковесные, они не засоряют список branches. Он действительно потихоньку распухает.


  • Branches более универсальные, потому что, если заранее вы не знаете потребуется ли вам стабилизация или нет, то если потребуется, то вам придется тэг заменить на branch или как-то использовать их вместе. И это довольно трудоемко. Поэтому мы просто стали использовать branches. Тэги мы практически не используем в нашем процессе.




Последний момент о развилках. Как мы в мастер коммиты доносим? Мы уже поговорили обо всем, что мы делаем со старыми ветками, но как нам пофиксить мастер?


В чем тут сложность?



В мастер мы, наверное, хотим использовать стандартный процесс, практически Github Flow за тем исключением, что мы не требуем полной стабильности мастера.


Сделали bugfix-ветку, сделали там несколько коммитов. Вмержили в мастер.


Как нам после этого дотянуть несколько коммитов до предыдущих версий?



Первый вариант это прямой cherry-pick. Если, кто не знает, cherry-pick это команда, которая перетаскивает коммиты с одной ветки Git на другую.


Напрямую перетаскиваем все коммиты из bugfix release в старой ветке.


Какие здесь проблемы?


  • У нас могут быть какие-то изменения непосредственно в merge-коммите, которые были частью merge в мастер.


  • Во-вторых, мы таскаем целую кучу коммитов между ветками. Многие из них это всевозможные work-in-progress, которые часто могут быть мусорными штуками.


  • В-третьих, у нас получается некоторая разница между тем, как фикс выглядит в мастере и в старых ветках, потому что в мастере это отдельная ветка и merge-коммит, в старых ветках это просто несколько фиксов прямо в релизных мастерах. И это тоже некоторая проблема, их потом сложнее сопоставлять друг с другом.




Другой вариант cherry-pick -m. Что делает cherry-pick -m? В отличие от обычного cherry-pick он все коммиты схлопывает вместе. Вы натравливаете его на merge-коммит в мастере, говорите, что хотите взять коммиты из ветки bugfix. Он их все схлопывает вместе с изменениями, которые, возможно, происходили при merge. И таким образом вы перетаскиваете изменения в старые ветки.


В чем тут проблема? У нас все еще есть некоторая разница между тем, что у нас лежит в мастере и тем, что у нас лежит в старых версиях. И эта разница достаточно большая. В старых версиях у нас один атомарный коммит, а в мастере у нас их целая куча, да еще и с merge.


И этим достаточно сложно управлять, сложно сопоставлять изменения в мастере и в предыдущих релизах.



Тот путь, который мы выбрали для себя это merge --squash. Что делает merge --squash? Он вместо того, чтобы создавать merge-коммит в мастере, он создает squash-коммит, который примерно, как в cherry-pick -m, схлопывает все коммиты A, B, C из bugfix вместе и кладет их прямо в мастер. В мастере при этом никакого merge-коммита нет. Т. е. в мастере у нас лежит один коммит, соответствующий всему изменению, и мы его легко простым cherry-pickом одного коммита перетаскиваем на предыдущие ветки.


Это максимально удобная штука, если вам нужно часто делать cherry-pick bugfixes и фичей между ветками. И даже если это относится не к поддержке старых версий, а каким-то другим вашим процессам, то merge --squash спасает.



Поговорили обо всех частях процесса по отдельности, теперь посмотрим на то, как весь процесс выглядит вместе.


Сначала о том, как процесс видит разработчик и как мы вносим изменения в продукт, надевая шапку разработчика.



Мы можем не думать о release branches вообще, о конкретных релизах, мы можем думать только о мастер-branches, остальные мы вообще не видим.


И строго говоря, мы по процессу вообще запрещаем для всех, кроме release engineer, коммиты в релизной ветке.


Вот такую картину видят для себя разработчик:



Когда нужно сделать какой-то bugfix, он готовит себе bugfix-ветку. После интеграционного тестирования review и всего остального, вмерживает это все в мастер.



Потом делает cherry-pick по очереди в 1.5,



в 1.4,



в 1.3.



Вот и все. Это все, что нужно сделать разработчику. Процесс прямолинейный. Ни за какими предыдущими релизами следить не нужно.


Откуда разработчик получает информацию о том, в какие из этих веток коммитить. В какой минорной версии нам нужен этот фикс это обозначается прямо в Jira в fix version. И либо это делает тот, кто заводил тикет, либо это делает разработчик сам, исходя из своих знаний, какие фичи и в каких минорных релизах были добавлены. Если фиксят фичу, которая добавлена в 1.3, значит нужно промержить фикс вплоть до 1.3.


Как процесс видят те, кто занимаются выпуском релиза, как его видят release engineers?



Есть мастер, есть релизный master-1.3, от которого мы собираемся сделать релиз.



Когда мы готовимся релиз выпустить, то сначала весь его scope собирается в ветке 1.3-master.


После этого, когда весь scope готов, мы делаем релизную ветку 1.3.2.



Так мы только что ее сделали, мы знаем, что все, что лежит в 1.3-master в этот релиз у нас попало.


Пока релиз тестируется, мастер и 1.3-master продолжают развиваться независимо.



Если нам нужна какая-то стабилизация в 1.3.2, например, мы нашли какой-то баг B, то мы проводим его по обычному процессу.



Разработчики пофиксят его в мастере, потом в 1.3-master.



И потом release engineer протащит его в 1.3.2.




Еще раз посмотрим на те требования, которые мы выдвигали к подходу ранее:


  1. Мы говорили про то, что релизы не должны мешать друг другу и разработке.


  2. А также о том, что между релизами должна сохраняться консистентность и мы не хотим ею рисковать.


  3. И подход должен масштабироваться:



  • В ширину на большое количество версий.
  • В глубину на разные уровни патчей.


Кажется, что наш подход этому всему удовлетворяет:


  1. Одновременные релизы друг другу не мешают за счет того, что мы используем release branches и релиз master branches. Разработка всегда может вестись в master branches. И поэтому никакие работы с релизами не мешают разработчикам вносить новые изменения.


  2. Консистентность между релизами сохраняется за счет интеграции сверху вниз: master-first или upstream first, когда мы точно знаем, что промержили коммит во все более новые версии, если он есть в более старой.


  3. Масштабирование в ширину мы достигаем за счет того, что мы можем добавлять master branches. А масштабирование в глубину мы достигаем за счет того, что будем создавать такие branches, как 1.1.1-master и т. д. Они будут со своим релизным мастером относиться так же, как релизный мастер относится к обычному мастеру.




Какие проблемы у нас все еще остаются?



Первая проблема это проблема с тем, как мы боремся с нарушениями процессов.


Естественно, ни один процесс не будет работать, если ему не следовать. Но нужно сказать, что этот принцип upstream first, которого мы придерживаемся, он более хрупкий. За счет того, что мы не вмерживаем старые релизные branches вверх, если мы в какой-то момент что-то куда-то забыли промержить, т. к. merges между ветками мы не делаем, то фикс может куда-то и не попасть. И это проблема.


Поэтому, если процесс нарушается, то начинается хаос. И решаем мы это через то, что пишем дополнительные инструменты. Сейчас у нас есть первая версия инструмента, который делает нам валидацию git logа и Jira. Здесь нам очень помогает то, что мы делаем merge --squash и то, что у нас ровно один коммит под каждую Jira issue.


Скрипт может посмотреть на список коммитов на fix version, указанной в Jira и точно найти между ними соответствие 1 к 1. За счет этого можно легко поймать все проблемы, если мы что-то куда-то не промержили.



Есть еще одна проблема. Она напрямую не связана с Git, но это проблема поддержки старых версий и промерживания коммитов, и соотношения их по коммитам. Это проблема того, как версии соотносятся.


Представьте, что клиент использует версию 1.1.10 и захотел перейти на какую-то версию в ветке 1.2. На какую версию он может перейти? Может ли он перейти на 1.2.1?


На самом деле мы этого не знаем.


Проблема в том, что 1.1.10 могла выйти позавчера, а 1.2.1 могла выйти 3 месяца назад. В 1.1.10 будет больше фиксов. И то, что клиент уже видел пофиксеным в своей текущей версии, окажется сломанный, если он перейдет на 1.2.1.


Это означает, что при переходе клиента с версии на другую minor-версию, нужно учитывать время выпуска версии и то, какие коммиты туда попали.



Решение, над которым мы работаем сейчас, это составление матрицы совместимости. Это таблица, в которой по обеим осям мы перечисляем все версии, которые мы выпускали и отмечаем на пересечениях можем ли мы сделать такой переход или не можем.


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


Но проблема в том, что такую таблицу сложно поддерживать. Представьте, что есть сотни одновременно живущих версий у кастомера. И это будет гигантская таблица 100 на 100. И это не очень классно, и пользователю такое отдавать сложно.


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


Можем сделать выводы. О чем сегодня поговорили?



  1. Посмотрели, что поддержка нескольких старых версий продукта это не то же самое, что разработка в одном мастере. И у такой разработки есть специальные требования к тому, как мы работаем с ветками Git.

  • Нам нужно не забывать о сохранении консистентности и о том, что мы должны промержить коммиты сразу в несколько местах.


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



  1. Github Flow, Git Flow, какие-то подходы, описание которых мы могли найти публично, этим требованиям не удовлетворяют. Хотя, я уверен, что у всех продуктовых компаний, которые занимаются такой поддержкой, есть какие-то свои собственные велосипеды на этот счет.


  2. Поэтому мы тоже построили свой собственный велосипед. И решили им поделиться.



  • Наш подход основан на интеграции master-first или upstream first сверху вниз и использования release master branches, которые аккумулируют изменения, соответствующие поддерживаемым miner-веткам.



  1. И продолжаем его улучшать и дорабатывать.

  • Мы работаем над дополнительными инструментами такими, как: кросс-валидация Git и Jira.


  • А также работаем над инструментами, которые помогут нам понимать соотношение между нашими версиями.



Спасибо за внимание!



Здравствуйте! Спасибо за доклад! Не могли бы вы чуть более подробно рассказать про выбор мастер first? Получается, что баг приходит на конкретную версию, допустим, на 1.3. И если баг не понятен, то коммитить его будут тоже на версии 1.3. Соответственно, в 1.3 в target его можно легко починить и дальше следуя логическому процессу разработки эволюции решения перенести его во все последующие ветки.


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


Спасибо за хороший вопрос! Можно было бы сказать, что тут есть какой-то компонент того, что так исторически сложилось. Мы сначала пришли к тому, что у нас было upstream first. Это достаточно давно произошло. Кросс-валидация Git и Jira, т. е. контроль за merge у нас появился не так давно. Но он тоже зависит от upstream first, потому что, если мы только что промержили что-то в 1.3 и еще не промержили выше, то вот этот бот, который к нам ходит и присылает нотификации, он сразу начнет сыпать алертами, что мы что-то еще не промержили.


Мы хотим, чтобы у нас были не алерты, на которые мы реагируем, а мы хотим, чтобы у нас алертов не было, потому, если бот постоянно будет сыпать тем, что у нас все баги, которые мы фиксим, должны быть куда-то еще помержены, то люди перестанут на это реагировать.


Добрый день! Спасибо за доклад! Какая политика при случае отката, если коммит не взлетел либо в конкретной версии, либо по всей ветке?


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


Если performance упал?


Если у нас была какая-то регрессия по performance, по чему-то еще, то мы, скорее всего, тоже не будем делать отката. Мы заведем отдельный regression issue и будем хендлить его таким образом.


Если у release engineer есть план, ему разработчик что-то замержил в ветку, это как согласовывается?


Мы на уровне правил проекта на Github лочим ветки соответствующим релизом.


Он замержил в мастер, правильно?


Да, это означает, что у нас регрессия появилась в куче релизных мастеров. И это означает, что мы заведем regression issue, какой-то блокер, который будет блокировать, возможно, релизы во всех мастерах. И мы постараемся быстро его пофиксить.


Это хороший вопрос. У нас, наверное, не так часто случаются такие проблемы.


Все frameworks идут по пути, когда все хорошо, но не всегда же это так. Ни один framework не задает ситуации, когда что-то сломалось.


Я согласен. Но здесь мы решаем проблему 99 % случаев. И 1 %, когда мы дропнули performance каким-то bugfixом в 2 раза и нашли это во время тестирования, а bugfix уже везде промержен, то мы в ручном режиме примем какое-то решение. Может быть, мы сделаем откат, но, скорее всего, мы очень быстро будем фиксить это по всем веткам.


Добрый день! Меня зовут Дмитрий, Информационная система Кодекс. Вы говорите, что вы используете подход upstream first, но при этом, допустим, к вам пришел тикет на исправление версии 1.3. Соответственно, разработчик сначала должен найти и исправить этот баг в 1.3, а затем пойти сверху в 1.5, 1.4, возможно, 1.2? Или же он начинает с самой последней версии в 1.5 и смотрит есть ли этот баг в самой свежей версии?


У нас есть две инженерные команды. Одна, в которой я работаю, работает с кастомерами и воспроизводит их проблемы, и занимается частью bugfix. И есть команда, которая работает преимущественно с направленной вперед разработкой. Обычно команда, которая работает с кастомерами, получает от кастомера запрос. Кастомер не заводит баг репорт, он говорит, что у меня что-то не работает.


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


Поиск начинается с версии пользователя?


Поиск начинается с версии пользователя, потому что в самом начале пользователь приходит с проблемой. Мы заранее не знаем есть ли у нас баг и где у нас проблема. Для того чтобы воспроизвести баг, мы сначала максимально воспроизводим его environment, а потом уже пытаемся подменить версию, например, на самую последнюю и посмотреть воспроизводится все еще или нет.


И еще вопрос. Мастер в вашем примере, который вы приводили, это master-1.6?


Он не станет master-1.6. Когда-нибудь master-1.6 от него отбранчуется. Но фактически да, фактически мастер это релизная ветка для следующего релиза, который мы будем готовить.


И еще маленький вопрос. Версионирование там больше трех, больше вложенностей. И это специфика конкретно вашей разработки? Т. е. по факту находятся какие-то баги, вы их фиксите, но вряд ли получается так, что исправление каким-то кастомерам нужно, каким-то кастомерам не нужно, если это какой-то баг. Т. е. почему не выпускать 1.3, 1.3.1, 1.3.2, 1.3.3 и т. д?


Я понял вопрос. Т. е. почему кастомер не возьмет и не апгрейдится до самого конца, если ему все равно нужны эти все фиксы?


В каком-то смысле, да. И это еще решит ту проблему, что branches копятся. Тогда по факту мы сможем удалять, условно, предпоследнюю ветку всегда в рамках минорных версий. Т. е. мы выпустили 1.3.3, мы можем удалить 1.3.1.


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


Почему нам нужно поддерживать старые ветки, почему кастомеры не переходят? Это всегда какая-то договоренность с кастомерами, по тому, насколько большой прыжок они готовы сделать. Если они сидели полтора года в production и у них все было хорошо, а потом они нашли какой-то один баг, и мы им говорим: Отлично, вы теперь затяните все наши изменения и фичи за полтора года для того, чтобы его пофиксить, то они, скорее всего, будут не очень сильно этому рады. Даже если это захочет сделать сама команда разработки в каком-то банке, то их, скорее всего, развернут какие-то их operation. Они попросят chance list для такого перехода. А в нем будет 1 000 изменений. И они скажут: А можно нам патчик?.


Разве это не проблема версионирования? Получается, что у вас минорные версии на самом деле не минорные.


Почему? Мы выбираем и выпускаем минорную или мажорную версию просто по изменениям. Major ломает compatibility, а minor не ломает. Формально не ломает, по крайней мере. Но в рамках минорной или мажорной версии мы потом продолжаем независимо выпускать на них патчи, чтобы кастомеры, один раз выйдя в prod на минорной версии, было проще в рамках нее апгрейдиться.

Подробнее..

Переписывание истории репозитория кода, или почему иногда можно git push -f

22.09.2020 16:11:47 | Автор: admin


Одно из первых наставлений, которое молодой падаван получает вместе с доступом к git-репозиториям, звучит так: никогда не ешь жёлтый снег делай git push -f. Поскольку это одна из сотен максим, которые нужно усвоить начинающему инженеру-разработчику ПО, никто не тратит время на уточнение, почему именно так нельзя делать. Это как младенцы и огонь: спички детям не игрушки и баста. Но мы растём и развиваемся как люди и как профессионалы, и однажды вопрос а почему, собственно? встаёт в полный рост. Эта статья написана по мотивам нашего внутреннего митапа, на тему: Когда можно и нужно переписывать историю коммитов.



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

Для этого, в свою очередь, нам понадобится быстрый экскурс в физическую структуру git-репозитория. Если вы точно уверены, что знаете об устройстве репо всё, то можете пропустить эту часть, но даже я в процессе выяснения узнал для себя довольно много нового, а кое-что старое оказалось не вполне релевантным.

На самом низком уровне git-репо представляет собой набор объектов и указателей на них. Каждый объект имеет свой уникальный 40-значный хэш (20 байт в 16-ричной системе), который вычисляется на основе содержимого объекта.



Иллюстрация взята из The Git Community Book

Основные типы объектов это blob (просто содержимое файла), tree (набор указателей на blobs и другие trees) и commit. Объект типа commit представляет собой только указатель на tree, на предыдущий коммит и служебную информацию: дата/время, автор и комментарий.

Где здесь ветки и тэги, которыми мы привыкли оперировать? А они не являются объектами, они являются просто указателями: ветка указывает на последний коммит в ней, тэг на произвольный коммит в репо. То есть когда мы в IDE или GUI-клиенте видим красиво нарисованные веточки с кружочками-коммитами на них они строятся на лету, пробегая по цепочкам коммитов от концов веток вниз к корню. Самый первый коммит в репо не имеет предыдущего, вместо указателя там null.

Важный для понимания момент: один и тот же коммит может фигурировать в нескольких ветках одновременно. Коммиты не копируются при создании новой ветки, она просто начинает расти с того места, где был HEAD в момент отдачи команды git checkout -b <branch-name>.

Итак, почему же переписывание истории репозитория вредно?



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

Почему-то мало кто знает, что довольно давно у команды git push существует безопасный ключ --force-with-lease, который заставляет команду завершиться с ошибкой, если в удалённом репозитории есть коммиты, добавленные другими пользователями. Я всегда рекомендую использовать его вместо -f/--force.

Вторая причина, по которой команда git push -f считается вредной, заключается в том, что при попытке слияния (merge) ветки с переписанной историей с ветками, где она сохранилась (точнее, сохранились коммиты, удалённые из переписанной истории), мы получим адское число конфликтов (по числу коммитов, собственно). На это есть простой ответ: если аккуратно соблюдать Gitflow или Gitlab Flow, то такие ситуации, скорее всего, даже не возникнут.

И наконец есть неприятная побочка переписывания истории: те коммиты, которые как бы удаляются при этом из ветки, на самом деле, никуда не исчезают и просто остаются навечно висеть в репо. Мелочь, но неприятно. К счастью, эту проблему разработчики git тоже предусмотрели, введя команду сборки мусора git gc --prune. Большинство git-хостингов, как минимум GitHub и GitLab, время от времени,
производят эту операцию в фоне.

Итак, развеяв опасения перед изменением истории репозитория, можно, наконец, перейти к главному вопросу: зачем оно нужно и когда оправдано?

На самом деле, я уверен, что практически каждый из более-менее активных пользователей git хоть раз, да изменял историю, когда вдруг оказывалось, что в последнем коммите что-то пошло не так: вкралась досадная опечатка в код, сделал коммит не от того пользователя (с личного e-mail вместо рабочего или наоборот), забыл добавить новый файл (если вы, как я, любите пользоваться git commit -a). Даже изменение описания коммита приводит к необходимости его перезаписи, ведь хэш считается и от описания тоже!

Но это тривиальный случай. Давайте рассмотрим более интересные.

Допустим, вы сделали большую фичу, которую пилили несколько дней, отсылая ежедневно результаты работы в репозиторий на сервере (4-5 коммитов), и отправили свои изменения на ревью. Двое-трое неутомимых ревьюверов закидали вас крупными и мелкими рекомендациями правок, а то и вовсе нашли косяки (ещё 4-5 коммитов). Затем QA нашли несколько краевых случаев, тоже требующих исправлений (ещё 2-3 коммита). И наконец при интеграции выяснились какие-то несовместимости или попадали автотесты, которые тоже надо пофиксить.

Если теперь нажать, не глядя, кнопку Merge, то в главную ветку (у многих она по старинке называется master) вольются полтора десятка коммитов типа My feature, day 1, Day 2, Fix tests, Fix review и т.д. От этого, конечно, помогает режим squash, который сейчас есть и в GitHub, и в GitLab, но с ним надо быть осторожными: во-первых, он может заменить описание коммита на что-то непредсказуемое, а во-вторых заменить автора фичи на того, кто нажал кнопку Merge (у нас это вообще робот, помогающий релиз-инженеру собрать сегодняшний деплой). Поэтому самым простым будет перед окончательной интеграцией в релиз схлопнуть все коммиты ветки в один при помощи git rebase.

Но бывает также, что к код-ревью вы уже подошли с историей репо, напоминающей салат Оливье. Такое бывает, если фича пилилась несколько недель, ибо была плохо декомпозирована или, хотя за это в приличных коллективах бьют канделябром, требования изменились в процессе разработки. Вот, например, реальный merge request, который приехал ко мне на ревью две недели назад:



У меня рука машинально потянулась к кнопке Report abuse, потому что как ещё можно охарактеризовать реквест из 50 коммитов с почти 2000 изменённых строк? И как его, спрашивается, ревьюить?

Честно говоря, у меня ушло два дня просто на то, чтобы заставить себя приступить к этому ревью. И это нормальная реакция для инженера; кто-то в подобной ситуации, просто не глядя, жмёт Approve, понимая, что за разумное время всё равно не сможет сделать работу по обзору этого изменения с достаточным качеством.

Но есть способ облегчить жизнь товарищу. Помимо предварительной работы по лучшей декомпозиции задачи, уже после завершения написания основного кода можно привести историю его написания в более логичный вид, разбив на атомарные коммиты с зелёными тестами в каждом: создал новый сервис и транспортный уровень для него, построил модели и написал проверку инвариантов, добавил валидацию и обработку исключений, написал тесты.
Каждый из таких коммитов можно ревьюить по отдельности (и GitHub, и GitLab это умеют) и делать это набегами в моменты переключения между своими задачами или в перерывах.

Сделать это всё нам поможет всё тот же git rebase с ключом --interactive. В качестве параметра надо передать ему хэш коммита, начиная с которого нужно будет переписать историю. Если речь о последних 50 коммитах, как в примере на картинке, можно написать git rebase --interactive HEAD~50 (подставьте вместо 50 вашу цифру).

Кстати, если вы в процессе работы над задачей подливали к себе ветку master, то сначала надо будет сделать rebase на эту ветку, чтобы merge-коммиты и коммиты из мастера не путались у вас под ногами.

Вооружившись знаниями о внутреннем устройстве git-репозитория, понять принцип действия rebase на master будет несложно. Эта команда берёт все коммиты в нашей ветке и меняет родителя первого из них на последний коммит в ветке master. См. схему:




Иллюстрации взяты из книги Pro Git

Если изменения в C4 и C3 конфликтуют, то после разрешения конфликтов коммит C4 изменит своё содержание, поэтому он переименован на второй схеме в C4.

Таким образом, вы получите ветку, состоящую только из ваших изменений, и растущую из вершины master. Само собой, master должен быть актуальным. Можно просто использовать версию с сервера: git pull --rebase origin/master (как известно, git pull равносилен git fetch && git merge, а ключ --rebase заставит git сделать rebase вместо merge).

Вернёмся наконец к git rebase --interactive. Его делали программисты для программистов, и понимая, какой стресс люди будут испытывать в процессе, постарались максимально сохранить нервы пользователя и избавить его от необходимости чрезмерно напрягаться. Вот что вы увидите на экране:


Это репозиторий популярного пакета Guzzle. Похоже, что rebase ему не помешал бы

В текстовом редакторе открывается сформированный файл. Внизу вас ожидает подробная справка о том, что тут вообще делать. Далее в режиме простого редактирования вы решаете, что делать с коммитами в вашей ветке. Всё просто, как палка: pick оставить как есть, reword поменять описание коммита, squash слить воедино с предыдущим (процесс работает снизу вверх, то есть предыдущий это который строчкой ниже), drop вообще удалить, edit и это самое интересное остановиться и замереть. После того, как git встретит команду edit, он встанет в позицию, когда изменения в коммите уже добавлены в режим staged. Вы можете поменять всё, что угодно в этом коммите, добавить поверх него ещё несколько, и после этого скомандовать git rebase --continue, чтобы продолжить процесс rebase.

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

Если вы запутались и кажется, что всё пропало, у вас есть кнопка аварийного катапультирования git rebase --abort, которая немедленно вернёт всё как было.

Вы можете повторять rebase несколько раз, затрагивая только части истории, и оставляя остальные нетронутыми при помощи pick, придавая своей истории всё более и более законченный вид, как гончар кувшину. Хорошим тоном, как я уже написал выше, будет сделать так, что тесты в каждом коммите будут зелёными (для этого отлично помогает edit и на следующем проходе squash).

Ещё одна фигура высшего пилотажа, полезная в случае, если надо несколько изменений в одном и том же файле разложить по разным коммитам git add --patch. Она бывает полезна и сама по себе, но в сочетании с директивой edit она позволит вам разделить один коммит на несколько, причём сделать это на уровне отдельных строк, чего не позволяет, если я не ошибаюсь, ни один GUI-клиент и ни одна IDE.

Убедившись ещё раз, что всё в порядке, вы наконец можете со спокойной душой сделать то, с чего начался этот туториал: git push --force. Ой, то есть, разумеется, --force-with-lease!



Поначалу вы, скорее всего, будете тратить на этот процесс (включая первоначальный rebase на master) час, а то и два, если фича реально развесистая. Но даже это намного лучше, чем ждать два дня, когда ревьювер заставит себя наконец взяться за ваш реквест, и ещё пару дней, пока он сквозь него продерётся. В будущем же вы, скорее всего, будете укладываться в 30-40 минут. Особенно помогают в этом продукты линейки IntelliJ со встроенным инструментом разрешения конфликтов (full disclosure: компания FunCorp оплачивает эти продукяы своим сотрудникам).

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

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

Перевод Хватит копировать, пора сливаться. Часть 1. Конфликт слияний

15.02.2021 18:22:10 | Автор: admin

Несмотря на распространённость операции git cherry-pick (копирование коммитов) в Git, обычно это не самое лучшее решение. Иногда это меньшее из двух зол, но я ещё не видел ситуации, где оно было бы целесообразно.


Это первая часть из серии статей, которые начинаются объяснением почему копирование это плохо, продолжаются рассказом почему это ужасно, а затем описывают как получить тот же результат, применяя слияние (merge). Я покажу как применить эту технику в случае, когда вам нужно сделать слияние со старыми коммитами (retroactive merge) и когда вы хотите исправить копирование на слияние пока не случилось чего-нибудь плохого.


В копировании задействованы две ветки: донорская (откуда берётся коммит) и принимающая (куда он копируется). Давайте назовём их соответственно master и feature. Для простоты предположим, что копируемый коммит содержит изменение только в одной строке единственного файла. На данной диаграмме каждый коммит помечен содержанием этой строки, а штрихованная стрелка означает само копирование (операцию git cherry-pick).


Первая диаграмма


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


Есть какой-то общий предок А с коммитом строки "apple". Далее ветки расходятся, коммит F1 лежит в ветке feature, а M1 в master. Эти коммиты не затрагивают рассматриваемую строку, поэтому они всё ещё помечены "apple". Затем мы фиксируем F2 в ветку feature, меняющую содержимое нашей строки на "berry", а затем копируем F2 в ветку master с названием M2.


Пока ничего необычного не происходит.


Время идёт, дерево репозитория обогащается новыми коммитами:


Вторая


Мы зафиксировали М3 в ветку master и F3 в feature. Оба коммита обходят стороной нашу строку, поэтому она всё ещё равна "berry".


Пришло время слить feature в master. Так как строка одна и та же в обоих ветках, конфликтов не происходит, результат слияния всё ещё "berry".


После слияния 1


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


Давайте рассмотрим другой вариант. После копирования F2 мы фиксируем М3 в master и F3 в feature, но на этот раз F3 меняет нашу строку на "cherry". Такое может быть если программист, работающий над веткой feature, нашёл улучшение в коде, или менеджмент резко потребовал перевести весь проект на "cherry". Какова бы ни была причина, теперь дерево репозитория выглядит вот так:


A bomb!


На этот раз при слиянии feature в master происходит конфликт. Основание трехстороннего слияния (three-way merge) содержит "apple", входящая ветка feature содержит "cherry", а текущая "berry".


<<<<<<<<<< HEAD (master)berry||||||||| merged common ancestorsapple=========cherry>>>>>>>>>> feature

Конфликт возникает потому, что скопированные изменения затёрлись новыми, а сама информация о копировании не сохранилась. Вспомним, что штрихованная стрелка только у нас в голове.


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


В случае только двух веток шансы на корректное разрешение довольно хорошие (коллизий не так много, программист не сильно устал и т.п). Но что будет если взять три ветки?


Трёхветочный кошмар


Снова начинаем с коммита А, где наша строка равна "apple". Сразу создаём ветку victim на основе A и фиксируем изменения в V1, не охватывающие нашу строку. Из V1 создаём третью ветку feature с той же историей: коммит F1 не охватывает рассматриваемую строку, поэтому пока она везде равна "apple". Тем временем в ветке master появляется новый коммит М1, который тоже не трогает нашу строку.


Продолжаем веселье. В ветке feature фиксируем изменение нашей строки на "berry" как F2, и копируем его в master под именем M2. Затем снова меняем нашу строку уже на "cherry" в feature и фиксируем это как F3. В master появляется новый коммит М3, который не трогает нашу строку, поэтому в master она пока равна "berry".


Тем временем ветка victim и знать не знает про наши "шуры-муры" с копированием из feature в master. В ней фиксируются два новых изменения V2 и V3, оставляющих нашу строку девственно равной "apple".


Всё хорошее должно когда-то заканчиваться, ветка feature сливается в victim, производя на свет фиксацию V4 с нашей строкой, равной "cherry" благодаря наследию из feature.


Расплачиваться за "непотребство" с копированием придётся ветке victim, когда в неё сливают master. Бум! Впервые возникает конфликт: "благородное" изменение F2 встречает своего клонированного двойника M2. Бедняга программист, разрешающий эту коллизию, не имеет понятия о копировании, к тому же он скорее всего уже устал от других (обоснованных) конфликтов, поэтому вряд ли сможет корректно разрешить и этот.


Вкратце, проблема: при git cherry-pick в дереве появляются две копии одного коммита. Если хотя бы одна из его строк поменяется до слияния её копий, то возникнет невынужденная коллизия. Причём это может произойти и через неделю, и через год. Это значит, что тот, кто будет её разрешать, может попросту не иметь ресурсов для принятия правильного решения (не он копировал, команда полностью поменялась и проч).


Однако, вся эта Санта-Барбара может стать ещё хуже если конфликта не произойдёт!


Почему? Читайте в следующей части.

Подробнее..
Категории: Git , Git workflow

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru