Как автоматизировать коммиты из Jira в GitLab

Как автоматизировать коммиты из Jira в GitLab

Анатолий Копыл Анатолий Копыл Фронтенд-разработчик 9 февраля 2024

Рассказываем, как с помощью плагина для Google Chrome и open source-решения упросить работу с коммитами в GitLab.

Изображение записи

Привет! Я Анатолий, фронтенд-разработчик сайта Selectel. Тот, кто работает с кодом, понимает, как часто приходится делать коммиты. Вроде бы «дело на пять минут», но это не так: процесс отнимает много времени, если нужно внести десятки изменений. Чтобы сэкономить драгоценные минуты и упростить работу себе и коллегам, я разработал расширение, которое автоматически переносит информацию из задачи в GitLab.  

Проблема потерянного времени 

Коммит изменения в GitLab — фоновый и рутинный процесс, на который никто не закладывает рабочего времени. Но в нем есть действия, которые съедают 18 секунд при каждом коммите. Десять коммитов — уже три минуты за день и 15 за неделю. Да, немного, но на это тратится внимание. К тому же за эти 15 минут можно сделать что-то полезное или просто выпить кофе и дать мозгу отдохнуть. 

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

В команде принято использовать ID задач из Jira в качестве названий для веток и Conventional Commits. Формат: type (compontent_from_Jira): subject TASK_ID.

Удобство от такого подхода я ощутил сразу: в Gitlab намного проще ориентироваться. Ветки, коммиты, мердж реквесты — все становится очевидным. Но есть и обратная сторона медали — приходится писать эти сообщения о коммите.

Процедура коммита выглядит так:

  1. Копируешь тип задачи в Jira, вставляешь в сообщение коммита.
  2. Копируешь компонент задачи в Jira, вставляешь в сообщение коммита.
  3. Копируешь название задачи в Jira, вставляешь в сообщение коммита.
  4. Копируешь ID задачи, вставляешь в сообщение коммита.
  5. Коммитишь.

Получаем сообщение типа «feat(common): Сделать кнопку чуть краснее или чуть больше WEB-1234».

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

Очевидное и простое решение

Зачем заставлять пользователя (разработчика) копировать эти поля и вручную форматировать сообщение коммита, когда можно отказаться от конвенции автоматизировать это с помощью расширения для браузера?

ТЗ: кнопка на странице Issue в Jira, которая будет класть в клипборд отформатированное сообщение.

Попытка №1

Используя crxjs, создаем расширение на Vite и Preact. Расширение состоит из манифеста, content script, popup и service worker. Если не вдаваться в подробности, то popup – это окошко, которое появляется при клике на иконку расширения. Там мы размещаем пользовательские настройки, а content script вставляется в страницу и позволяет манипулировать DOM-ом.

Внутри content script создаем кнопку в нужном месте:


    const issueHeader = document.getElementById("jira-issue-header")

const root = document.createElement('div')
root.id = "jira-helper-entry"
issueHeader.appendChild(root)

render(
  <div class="sch-buttons">
    <CopyCommitMessageButton />
  </div>,
  root
)

// Добавим на страницу тостер, в нем будут 
// спавниться тосты библиотеки react-hot-toast.
render(
  <Toaster position='bottom-right'/>,
  document.querySelector('#page')
)

По клику на нее соберем поля таски и составим из них сообщение:


    const name = document.querySelector("#summary-val").textContent.trim()
const component = document.querySelector("#components-val").textContent.trim()
const key = document.querySelector("#key-val").textContent.trim()
const type = document.querySelector("#type-val").textContent.trim()

const commitMessage = `${type}(${component}): ${name} ${key}`

Полученное сообщение кладем в клипборд и показываем тост, чтобы пользователь не сомневался, что ему в клипборд что-то попало и это не «каша»:


    function listener(event: ClipboardEvent) {
  event.clipboardData?.setData("text/plain", commitMessage)
  event.preventDefault()
}

document.addEventListener("copy", listener)
document.execCommand("copy")
document.removeEventListener("copy", listener)

// react-hot-toast
toast.success(
  () => (
    <div>
      <b>Скопировано!</b>
      <p>{commitMessage}</p>
    </div>
  ),
  {
    id: commitMessage
  }
)

Вуаля! Теперь можно наслаждаться новой кнопкой в Jira и экономить 18 секунд каждый раз, когда надо составить сообщение коммита.

Осознание и новое решение

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

А самое обидное, что бескомпромиссное решение было все это время под носом, а если точнее, над промптом. Мы же в консоли и IDE знаем, в какой ветке мы находимся, а значит знаем, над какой таской работаем! 

ТЗ: написать расширение для WebStorm, которое будет ходить в Jira, находить Issue, исходя из названия текущей ветки в IDE, составлять сообщение о коммите и сразу вставлять его в соответствующее окошко в IDE.

Попытка №2

Для основы я нашел существующий open source-проект, который добавляет окно для создания сообщений коммита по шаблону. Для работы с ним достаточно изучить Java, потратить день, чтобы понять, как ставить зависимости, сбилдить проект и пофиксить LinkageError. Это совсем не то, что нам надо, но это намного лучше, чем начинать с нуля!

Интерфейс плагина.
Интерфейс плагина.

Например, в этом плагине уже добавлена кнопка в интерфейс, по нажатию на которую появляется окно выше:


    <idea-plugin>
  <actions>
    <action id="Commit.Button" class="com.kopyl.commit.CreateCommitAction"
      text="Create Commit Message"
      description="Create commit message"
      icon="//6f3bf64a-14d1-4b68-9202-2a000ca072b9.selcdn.net/icons/generate.svg">
      <add-to-group group-id="Vcs.MessageActionGroup" anchor="first"/>
    </action>
  </actions>
</idea-plugin>

И реализована функция получения названия ветки для использования в качестве Jira ID:


    private String getJiraIdFromBranchName(Project project) {
  RepositoryManager<GitRepository> repositoryManager = GitUtil.getRepositoryManager(project);
  GitLocalBranch branch = repositoryManager.getRepositories().get(0).getCurrentBranch();
  
  if (branch == null) return "";

  return branch.getName();
}

Напишем класс для взаимодействия с API Jira и будем передавать туда этот ID.


    // src/main/java/com/kopyl/commit/JiraClient.java

public class JiraClient {
  private final String token;
  private final String jiraUrl;
  private final JiraRestClient restClient;

  public JiraClient(String token, String jiraUrl) {
    this.token = token;
    this.jiraUrl = jiraUrl;
    this.restClient = getJiraRestClient();
  }

  private JiraRestClient getJiraRestClient() {
    BearerHttpAuthenticationHandler handler = new BearerHttpAuthenticationHandler(this.token);
    JiraRestClientFactory factory = new AsynchronousJiraRestClientFactory();
    return factory.create(getJiraUri(), handler);
  }

  private URI getJiraUri() {
    return URI.create(this.jiraUrl);
  }

  public Issue getIssue(String issueKey) {
    return restClient.getIssueClient()
      .getIssue(issueKey)
      .claim();
  }
}

Напишем класс CommitMessage, который будет принимать Issue и иметь метод для превращения этого Issue в строку подходящую под наш формат. Везде для взаимодействия с Jira используется пакет Atlassian Jira REST Client API.


    // src/main/java/com/kopyl/commit/CommitMessage.java

public class CommitMessage {
  private ChangeType changeType = ChangeType.FEAT;
  private String changeScope = "";
  private String shortDescription = "";
  private String jiraId = null;

  public CommitMessage(Issue issue) {
    if (Objects.equals(issue.getIssueType().getName(), "Bug")) {
      this.changeType = ChangeType.FIX;
    }

    this.changeScope = issue.getComponents().iterator().next().getName();
    this.shortDescription = issue.getSummary();
    this.jiraId = issue.getKey();
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append(changeType.label());

    if (isNotBlank(changeScope)) {
      builder.append('(').append(changeScope).append(')');
    }
    builder.append(": ").append(shortDescription);

    if (isNotBlank(jiraId)) {
      builder.append(' ').append(jiraId);
    }

    return builder.toString();
  }
}

В плагине уже существовал класс CreateCommitAction, который соответствовал <action> из plugin.xml, он обрабатывает нажатие на добавленную в интерфейс кнопку. 

Перепишем его, чтобы он не открывал окно, а обращался в API и создавал сообщение о коммите из полей таски:


    // src/main/java/com/kopyl/commit/CreateCommitAction.java

public class CreateCommitAction extends AnAction {
  // ...

  @Override
  public void actionPerformed(@NotNull AnActionEvent actionEvent) {
    CommitMessageI commitPanel = getCommitPanel(actionEvent);
    if (commitPanel == null) return;

    String personalAccessToken = AppSettingsState.getInstance().personalAccessToken;
    String jiraUrl = AppSettingsState.getInstance().jiraUrl;

    JiraClient jiraClient = new JiraClient(personalAccessToken, jiraUrl);

    String jiraId = getJiraIdFromBranchName(actionEvent.getProject());
    Issue issue = jiraClient.getIssue(jiraId);

    CommitMessage commitMessage = new CommitMessage(issue);

    commitPanel.setCommitMessage(commitMessage.toString());
  }
}

Код выше использует класс AppSettingsState. С помощью него мы получаем параметры, заданные пользователем в настройках. Взаимодействие с настройками реализованы в соответствии с официальным туториалом.

Билдим проект через TasksintellijbuildPlugin. Ставим его с диска — и получаем желаемый результат!

Заключение

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