Всем привет! Меня зовут Роман Аймалетдинов, я андроид разработчик. В этой статье хочу рассказать вам как автоматизировать, хоть и простую, но рутинную работу по созданию и оформлению ваших git-hub pull request через bash скрипт. Это особенно полезно, если у вас монорепозиторий или несколько типов template. Например, отдельные template для bug и для feature или для разных команд. Но не все сразу, сначала разберемся с самими templates, поймем, почему это может быть неудобно и сделаем свой скрипт, который умеет парсить название в человеческое и создавать удобное описание для каждого типа задач. Теперь все по порядку.

Скрипт генерирует нам шаблон командой ./gradlew makepr
Скрипт генерирует нам шаблон командой ./gradlew makepr

Note:
Если вам нужен скрипт, но не хочется читать "воду", то вот ссылка на репозиторий. Остальным приятного чтения!

Про проект

Так уж вышло, что на моем проекте используется GitHub для ревью процесса, кроме того, у нас монорепа (т.е. это проект, в котором и iOS, и Android, и Backend и куча языков, общие скрипты и много всего разного). Эта информация нам еще пригодится позже.

GitHub, как и BitBucket, имеют функцию создания собственных template для ваших PR. Что это? Template - это некий шаблон вашего описания задачи. Вы нажимаете кнопку создания PR, а на открытой странице уже есть какая-то заготовка description поля.

Untitled
Так выглядел наш темплейт созданный скриптом некоторое время назад, сейчас он немного усовершенствован

Вообще, это удобно, но мое личное мнение, иногда лиды злоупотребляют разного рода темплейтами, армейскими правилами и т.п. И template, вместо оптимизации рутинной работы разработчика, превращается в раздражающую форму, из которой нужно удалять лишние поля и скролить страницу. А иногда еще заставляют какие-то чекбоксы проставлять, и об этом нужно помнить и держать в актуальном состоянии. Я ничего не имею против чекбоксов в description PR, но иногда это скатывается в микроменеджмент. Я отвлекся..

Как добавить template в GitHub ?

Все просто, заходим на официальную страницу с инструкцией от GitHub и видим приблизительно такой план:

  • Создаем папку .github в корне проекта, если ее еще нет;

  • Создаем файл с названием pull_request_template.md (и только таким названием!)

  • Внутри пишем наш шаблон

  • Мержим

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

А если я хочу несколько разных templates?

А что если нам нужно несколько шаблонов? На это есть несколько причин:

Во-первых, мы хотим иметь разные шаблоны для feature, fix и release PRs, ведь логично, что набор инструкций может быть разным. В feature мы хотим добавить напоминания о том, что нужно все закрыть фичатоглами и не забыть написать тесты, заголовок для скриншотов и видео UI изменений. В fix реквестах мы хотим видеть шаблон попроще. А вот в release мы хотим засунуть required чекбоксы со всеми проверками по процессу, чтобы автор реквеста точно не забыл сделать все самое важное до нажатия кнопки merge. Другой вариант, у вас могут быть стримы, в которых существуют свои лиды и свои правила.

Во-вторых, у нас и, возможно, у вас - монорепа, это значит что у нас android, backend, iOS и все другие - в одном репозитории и, естественно, у всех разные запросы на template. Много команд, а разные команды могут захотеть иметь свои собственные шаблоны, т.к наши могут кого-то раздражать или быть вовсе нерелевантными. Как, например, заголовок для видео и скриншоты UI изменений для backend разработчиков или ребят, которые занимаются голосовым ассистентом.

GitHub и BitBucket предлагают возможность создания нескольких разных templates и выбирать необходимый. Давайте посмотрим как именно?

BitBucket

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

Untitled

А вот и инструкция с официального сайта как это сделать. Очень удобно я считаю, команда BitBucket получает, как минимум, мое сердечко.

GitHub

А как на счет GitHub? GitHub тоже имеет функциональность выбирать шаблон.

  • Необходимо создать папку PULL_REQUEST_TEMPLATE в .github папке

  • Создаем сколько угодно шаблонов с любым удобным названием

  • Мержим

Как пользоваться. Вот тут, к сожалению, есть большая досада. GitHub не даст вам выбрать шаблон в web-морде, как это сделано у BitBucket, у вас есть всего 1 способ открыть шаблон: необходимо добавить query параметр в адресную строку браузера.

Я серьезно, вот так:

Untitled

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

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

Скрипт

Мы можем написать скрипт, который будет открывать нам страницу создания PR. Мы вызовем его из терминала, и он подставит нам нужный template.

Отлично, можно пробовать. Надо выбрать язык. iOS разработчики скорее всего сразу ринутся писать на Ruby, так как они и так часто с ним взаимодействуют и неплохо его знают. Некоторые другие могут написать на Python, но все это требует каких-то дополнительных движений. Нужно поставить Ruby или Python себе на машину, на CI, всем членам команды. А если мы захотим пошарить скрипт другой платформе, они либо перепишут на свой язык, либо просто забьют и не будут использовать. Поэтому лучший выбор, на мой взгляд, это bash скрипт. Местами жутковатый (любители bash, прошу не бейте меня.. пишу на нем едва осознанно и не регулярно), зато работает из коробки из любого чайника. Естественно, умного.

GitHub API

Отлично. Bash - лучший выбор. Давайте ознакомимся, а что вообще мы можем сделать, используя GitHub API и их query параметры? Идем на официальную страницу и видим:

quick_pull

https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1 creates a pull request that compares the base branch main and head branch my-branch. The quick_pull=1 query brings you directly to the "Open a pull request" page.

title

https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&labels=bug&title=Bug+fix+report creates a pull request with the label "bug" and title "Bug fix."

body

https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&title=Bug+fix&body=Describe+the+fix. creates a pull request with the title "Bug fix" and the comment "Describe the fix" in the pull request body.

labels

https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&labels=help+wanted,bug creates a pull request with the labels "help wanted" and "bug".

milestone

https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&milestone=testing+milestones creates a pull request with the milestone "testing milestones."

assignees

https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&assignees=octocat creates a pull request and assigns it to @octocat.

projects

https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&title=Bug+fix&projects=octo-org/1 creates a pull request with the title "Bug fix" and adds it to the organization's project board 1.

template

https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&template=issue_template.md creates a pull request with a template in the pull request body. The template query parameter works with templates stored in a PULL_REQUEST_TEMPLATE subdirectory within the root, docs/ or .github/ directory in a repository. For more information, see "https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests."

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

Таким образом, если мы, например, подставим в ссылку создания PR параметр assignees и укажем GitHub login юзера, то, при открытии в браузере, PR уже будет назначен на этого пользователя. Давайте покажу на примере:

Untitled

Тут я подставил &assignees=Evleaps и GitHub автоматически назначил на меня PR. Тут же нас интересует уже известный нам параметр - template, и если мы подставим нечто вроде ?template=android_release_template.md , то вот теперь то у нас и откроется нужный нам template.

Осталось написать скрипт.

Версия скрипта 1

Создадим файл make_pr.md где-нибудь в нашем проекте и поместим в него следующий код:

#!/usr/bin/env bash
base_url="<https://github.com>"
current_branch="current_branch"
#query params
expand="1"
query="expand=base_url/query"

Теперь можно вызывать скрипт в консоли командой bash make_pr.md

На android имеет смысл сделать gradle таску, чтобы было проще запускать скрипт из любого подкаталога в терминале или с помощью IDE.

Все просто, поместим в наш build.gradle следующий код:

task makepr(type: Exec) {
    commandLine "$rootDir/scripts/make_pr.sh"
}

Теперь мы можем запускать наш скрипт командой ./gradlew makepr

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

Требования к улучшенному скрипту

Мы видим:

  • Скрипт определял родительскую ветку и самостоятельно решал куда нужно направить PR

  • PR имел читабельное название с номером задачи (Title)

  • В PR была ссылка на задачу Jira

  • Был template описания

Реализация

Родительская ветка

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

На самом деле, это не самая простая задача в git, так как понятия parent branch в git нет вовсе. Git - это направленный ациклический граф. Объекты коммитов образуют DAG, потому что у них есть родители (направленные), а граф объектов коммитов ациклический и это значит, что нет цепочки, которая начинается и заканчивается одним и тем же объектом.

Так как найти родительскую ветку в git? Конечно, гуглим и находим stackoverflow тред аж с 2012 года, описывающий разные подходы к поиску косвенного родителя. Именно косвенного, так как ни один из этих способов не даст вам 100% надежного результата определения родителя.

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

git log --pretty=format:'%D' HEAD^ | grep 'origin/' | head -n1 | sed 's@origin/@@' | sed 's@,.*@@'

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

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

getTargetBranch() {
    # Тут могла бы быть логика определения родительской ветки, но ее нет.
    echo "main"
}

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

Title

Затем нам нужно спарсить title для нашего PR. Тут все намного более однозначно, к счастью.

function getTitle() {
    local current_branch current_branch_number current_branch_text

    current_branch=$(git rev-parse --abbrev-ref HEAD)
    current_branch_number=$(awk -F '[^0-9]+' '{ print $2 }' <<<"$current_branch")
    current_branch_text=${current_branch#*$current_branch_number}
    trimmed_current_branch_text=$(
        echo "$current_branch_text" |
            sed -r 's/[-_]+/ /g' |                                     # replace '-' and '_' to ' '
            awk '{gsub(/^[ \t]+/,""); print $0 }' |                    # trim spaces
            awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}' # Uppercase first character
    )

    echo "JIRAPREF-$current_branch_number: $trimmed_current_branch_text"
}

Наши ветки выглядят так: JIRAPREF-1916-fix-crash-on-main-screen И метод парсит название в JIRAPREF-1916: Fix crash on main screen

Description

Напоследок осталось создать описание. Наш программируемый template c ссылками на Jira задачу в красивом и отформатированном виде.

function getPrDescription() {
    local current_branch_number description
    current_branch_number="$(git rev-parse --abbrev-ref HEAD | awk -F '[^0-9]+' '{ print $2 }')"

    description="
[$(getTitle)](https://globalradio.atlassian.net/browse/GPLAY-$current_branch_number)
---
<h3>Description:</h3> \n
\n
<h3>What was changed:</h3> \n
\n
<h3>Screenshots/videos/gif:</h3>
\n
\n
<details>
  <summary>PR Tips</summary>

  - How to build the template script: ./gradlew makepr
  - The bash script name is: make_pr.sh
  - The table example for copy paste:

  | Before | After |
  |------|------|
  |Your image before|Your image after|

</details>
"
    echo -e "$description"
}

Вызов

base_url="https://github.com"
target_branch=$(getTargetBranch)
current_branch="$(git rev-parse --abbrev-ref HEAD)"
path="/my_company_repo/my_project_name/compare/$target_branch...$current_branch"

#query params
title=$(getTitle)
body=$(getPrDescription)
assignees="$(git config user.name)"
expand="1"

query="expand=$expand&title=$title&assignees=$assignees&body=$body"
open "$base_url/$path?$query"

Прошу заметить, я не использую &template=android_release_template.md , так как вместо этого вызывается &body=$body c нашим описанием. Это намного более гибкий подход и может позволить сделать больше полезных оптимизаций в угоду вашей фантазии.

Весь скрипт целиком:

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

Заключение:

Таким образом, сама фича templates.md у GitHub мне показалось неудобной для использования и, честно говоря, я не вижу в ней особого смысла вне скрипта. А вот скриптом пользуемся, команда подхватила, это удобно.

Скрипт можно написать и более сложный и умный, все зависит от вашей фантазии, специфики и поставленных задач. Я рекомендую скопировать мой пример из репозитория и запустить. А после подумать, как бы вы его улучшили. Однако не забудьте поменять бранч is_updated_from_main в скрипте на dev/develop/main или другое название, главное ветки, куда вы мержите свои фичи. А также ссылки на JIRA, и укажите ваши префиксы. Просто внимательно прочтите скрипт, там все просто.

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

Спасибо, что прочли, надеюсь это кому ни будь пригодится. Удачи!

Комментарии (0)