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

Модель ветвления

Для Git существует несколько моделей ветвления или потоков. Некоторые люди не могут представить проект без полноценного GitFlow. Некоторые из них являются ярыми верующими только в master. Мы где-то посередине. Наш поток в основном основан на ветвях функций — у нас есть основная линия в master, и мы в основном работаем в краткосрочных ветвях, посвященных одной функции. Когда фича будет готова (в идеале уже после проверки), мы сжимаем ее коммиты настолько, насколько это имеет смысл, а затем перебазируем сжатый коммит (или коммиты) на master. Затем он объединяется с мастером.

Давайте разобьем этот поток на несколько более мелких частей.

Начальная работа

Нам назначается новый тикет/выпуск (в идеале, потому что мы выбрали его из запланированного списка). Предполагая, что мы в курсе удаленного master, мы начинаем с создания функциональной ветки для нашей задачи:

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

Затем начните работу. Каждый раз, когда мы достигаем стабильной точки в нашей реализации (но не обязательно полной), мы фиксируем наши изменения в нашей функциональной ветке. Коммитить рано, коммитить часто, — говорят они, и мы соглашаемся. Это может означать, что у нас есть несколько коммитов каждый час, и это совершенно нормально, если мы назовем коммиты достаточно хорошо, чтобы мы могли понять историю и иметь возможность вернуться в случае необходимости. Здесь нет строгих правил, поскольку этот филиал по-прежнему является нашей частной лужайкой за пределами master.

git add . git commit -m "WIP invoices listing; filtering UI done"

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

git push # or, for the first push of the new feature branch git push --set-upstream origin PROJ-123

Подготовка вещей

Когда наша работа завершена (или находится в состоянии, позволяющем ее рецензировать), ради любезности рецензентов мы должны немного очистить историю Git, раздавив ревизии и исправления контрольных точек. Мы делаем это с помощью интерактивной перебазировки. Это также хороший момент, чтобы фактически синхронизировать наш код с основной веткой, нацелив нашу перебазировку на текущую вершину master, которая, возможно, уже сдвинулась, пока мы работали над функциональной веткой:

git fetch # make sure we know the state of the origin git rebase -i origin/master

Перебазировка — сложный процесс. Рассуждать проще, если перед нами воображаемое или нарисованное дерево ветвей — очень рекомендую новичкам. С помощью rebase мы вырезаем ветку, над которой работали, из точки, к которой она была прикреплена, и пересаживаем ее в другое указанное место (в нашем случае это кончик master). Изменения применяются от фиксации к фиксации. В некотором смысле мы инструктируем Git переделать нашу работу из ветки функций снова поверх другой отправной точки. Таким образом, если мы изменили имя переменной в нашей функциональной ветке, которая возникла из фиксации A, когда она была вершиной master, Git попытается применить именно это изменение к текущей вершине master, каким бы оно ни было. Он применяет стандартные методы разрешения конфликтов, так что он может потерпеть неудачу. Нам нужно разрешать конфликты так же, как мы делаем это для классического слияния. Однако, в отличие от слияния, rebase создает новые коммиты, делая вид, что работа произошла позже по времени и на более поздней базовой линии, чем это было на самом деле. Это, пожалуй, самая спорная часть – обсудим ее позже.

После перебазирования наша работа сокращается до одной или нескольких значимых коммитов и применяется поверх самых последних доступных изменений. Нам нужно поделиться нашими перебазированными изменениями со всем миром. На этот раз простой git push терпит неудачу, потому что с помощью rebase мы переписали историю веток и не синхронизировались с тем, что уже было отправлено (мы удалили коммиты в стиле WIP, которые были отправлены ранее, и у нас есть несколько новых и вместо этого блестящий). Итак, мы делаем еще одну вещь, которую некоторые могут счесть спорной — мы принудительно отправляем нашу функциональную ветку, чтобы удаленный репозиторий также забыл об удаленных коммитах:

Чего ждать? Нет такой команды Git. Здесь мы используем пользовательский псевдоним для git push --force-with-lease. Он не такой агрессивный, как простой --force, потому что он может перезаписывать только те коммиты, которые мы уже знаем в нашей локальной копии — мы будем отклонены, если на удаленном компьютере есть еще какие-то коммиты, о которых мы не знаем. Обычный --force перезапишет их, даже не сообщив нам об этом, поэтому, если мы сотрудничаем в функциональной ветке с кем-то еще, мы на правильном пути, чтобы уничтожить их работу. На самом деле, я считаю, что --force вообще не должно быть доступно. Я не вижу законного случая принудительного нажатия, --force-with-lease должен быть единственным допустимым принудительным методом. И когда мы присваиваем ему псевдоним please (обратите внимание, что это сокращение от push+lease), мы также более добры и менее… напористы 😆.

Время проверки

Когда наши изменения находятся в удаленном репозитории, мы можем приступить к рецензированию. В Bright мы все являемся поклонниками JetBrains, поэтому, очевидно, мы используем Upsource для этой цели. Наш подход к обзорам был уже описан здесь, так что давайте пропустим его здесь. Однако важно то, что проверка кода, вероятно, приведет к большему количеству коммитов в теперь перебазированной ветке функций, и это нормально, включая больше коммитов в стиле WIP. Давайте продолжим работу над веткой столько, сколько потребуется для успешного завершения обзора. В идеале, мы должны часто выполнять перебазирование на master, чтобы убедиться, что наша функциональная ветка по-прежнему синхронизирована с основной веткой, но у этого есть серьезный недостаток в нашем потоке — Upsource имеет тенденцию терять комментарии к коду, когда коммиты, к которым он был прикреплен, перебазируются. Помня об этом, мы ищем правильный баланс между удобством и риском несовместимости.

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

git checkout master git merge --ff-only PROJ-123 git push

Флаг --ff-only технически не нужен, но это проверка работоспособности, мы действительно правильно выполнили перебазирование, и все слияние на самом деле будет операцией быстрой перемотки вперед на master вместо классического слияния двух строк коммита. Мы можем даже настроить это как поведение слияния по умолчанию.

Существуют альтернативные методы перебазирования. Если мы хотим объединить все коммиты нашей функциональной ветки в одну, мы можем запустить git merge --squash feature-branch вместо пары rebase+merge. Эффект концептуально тот же, поэтому используйте то, что вам удобно.

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

git push origin --delete PROJ-123

Теперь официально время обеда 😉.

Но

Но говорят ребейз — это плохо!

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

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

Однако здесь есть виновник. Rebase применяет каждую фиксацию отдельно к новой базе, так что это означает, что у нас может быть несколько конфликтов для разрешения. Более того, если нам нужно перебазировать несколько раз с течением времени (например, потому что master пошел дальше, пока наше изменение находилось на рассмотрении), нам нужно повторить этот процесс разрешения конфликта несколько раз. Это боль, с которой частично справляется техника rerere. Но лучшее смягчение — позаботиться об определении объема и размера задач. Хорошо разделенная задача достаточно мала, чтобы гарантировать, что реализация не продлится долго: менее дня может быть хорошим целевым порогом, но очевидно — это зависит. Хорошо разделенная задача (в хорошо спроектированной кодовой базе) также достаточно независима от других запланированных изменений, поэтому количество конфликтов достаточно мало для ее обработки. В некоторых случаях это непросто, но оно того стоит — эффект от хорошо спланированных задач огромен и распространяется практически на все аспекты процесса разработки программного обеспечения.

Но как сотрудничать в функциональной ветке?

Тот факт, что мы принудительно продвигаем нашу ветку функций, означает, что история меняется. Поэтому, если кто-то полагается на это, мы можем нарушить их рабочий процесс. Вопрос о том, как разделить работу, если ее должны выполнять несколько человек, актуален, хотя, учитывая, что мы уже установили важность небольших и хорошо разделенных задач, это не так распространено, как может показаться. В большинстве случаев необходимость совместной работы над функцией до того, как она будет готова к отправке на master, вероятно, означает, что она недостаточно детализирована. Обратите внимание, что готов к отправке на master не обязательно означает готов к отправке клиентам. Существует множество методов, позволяющих разделить эти две функции, Переключение функций — это тот, с которым мы больше всего разобрались в Bright.

Но если нам действительно нужно поделиться одной и той же веткой функций с другими, мы можем вернуться к слиянию и «классическим» методам совместной работы Git внутри ветки функций и избавиться от нее непосредственно перед перемещением на главную. Git — могущественный зверь. И, в конечном счете, слияние по-прежнему является вполне допустимой техникой, если мы убеждены, что нам нужно использовать ее даже на master, ни один котенок не умрет. Случайный нелинейный фрагмент истории в Git, вероятно, не сделает ее сразу нечитаемой.

Выпуск модели

Мы верим в принцип Разворачивай раньше, развертывай часто настолько, насколько это возможно для нас и наших клиентов. Вот почему мы всегда инвестируем в процессы автоматизации и непрерывной доставки — нам никогда не придется запускать какую-либо сборку вручную или с компьютера разработчика. Обычно это означает, что мы можем развернуть каждую фиксацию, приземляющуюся на master, в промежуточную среду наших приложений. Мы просим наших клиентов как можно чаще использовать эти промежуточные среды, чтобы мы могли обнаруживать нерешенные ошибки до того, как они попадут в рабочую среду. В большинстве случаев мы также используем промежуточные сборки для наших внутренних нужд — т. е. мобильные приложения, которые мы в настоящее время разрабатываем, подключаются к промежуточным бэкэнд-средам, мы запускаем там демоверсии и т. д.

Эти факты требуют от нас максимально возможного поддержания работоспособного состояния ветки master, потому что, если master выйдет из строя, это может повлиять на работу многих людей. Мы считаем, что это хорошо. Чтобы помочь с этим заказом, у нас есть серверы непрерывной интеграции, на которых работает TeamCity, настроенные для каждой ветки, поэтому тесты в наших функциональных ветках выполняются постоянно, в том числе до того, как изменения попадут на master. Это включает в себя интеграционные тесты, которые работают с реальным ядром базы данных или реальными HTTP-вызовами к серверной части. Docker делает эти вещи намного проще, чем раньше — но это тема для отдельной статьи.

Мы всегда выпускаем каждый коммит для серверных и веб-проектов, где он никому не вредит (при условии, что у нас нет серьезных простоев при развертывании). Для мобильных приложений в большинстве случаев мы воздерживаемся от этого по прагматическим причинам — циклы сборки и выпуска длиннее из-за участия третьих сторон (Google Play для Android и TestFlight для iOS), а также из-за раздражения клиентов, когда они получать уведомления обо всех обновлениях. А для активного проекта нет ничего необычного в том, чтобы делать дюжину коммитов каждый день. Поэтому для проектов iOS и Android мы, как правило, устанавливаем запланированный триггер в TeamCity, который выпускает изменения каждую ночь, и в случае, если нам нужно сделать это быстрее, мы всегда можем нажать кнопку на сервере сборки.

Обслуживание производства

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

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

Для серверных или веб-решений нам не нужно заботиться о предыдущих версиях — все, что нам нужно, это то, что выпущено в настоящее время. Мы используем ветку production (это единственная долгоживущая ветка, кроме master), которую мы принудительно сбрасываем до выпущенной точки master после завершения развертывания:

git push --force-with-lease origin master:production

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

git tag release1.3 git push --tags

Оба решения дают нам простой способ сбросить нашу среду разработки до точки на временной шкале Git, которая актуальна для работы над исправлениями ошибок, которые необходимо выпустить «как можно скорее».

Git-инструменты

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

Лично я предпочитаю сочетание сырого Git и встроенной поддержки WebStorm/IntelliJ. Я делаю коммит из IDE, потому что это просто, но перебазирую из CLI, потому что мне нужно чувствовать контроль. Но это субъективно, и каждому нужно найти свой инструментарий.

Если вы решите использовать CLI до некоторой степени, вот набор полезных псевдонимов, которые мы часто используем для удобства. Вы можете вставить его в свой файл .gitconfig, который, скорее всего, находится в вашем «домашнем» каталоге.

[alias] st = status ci = commit ciam = commit -am br = branch co = checkout lg = log -p lol = log --color --graph --decorate --pretty=format:'%C(red)%h %Cgreen%cr %Cblue(%an)%C(yellow)%d%Creset %s' --abbrev-commit --all lola = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --all unstage = reset HEAD reseth = reset --hard please = push --force-with-lease halp = reflog --date=iso # halp is more than help

Удачного толчка!

Первоначально опубликовано на brightinventions.pl

Адам Бар, веб-парень @ Bright Inventions

Личный блог Электронная почта Twitter Github Stackoverflow