Автоматическое генерирование версии ПО с номером сборки через хук Git

В этой статье описан второй способ использования увеличения номера софта с использованием хуков гит. В первой статье о версионировании программного обеспечения я использовал подход с помощью файла проекта qmake. Данный подход содержит в себе серьёзные минусы:

  • привязка к библиотеки Qt;
  • жесткий формат номера версии;
  • путаница в версиях при использовании параллельных веток (отличаться будет только хеш комммита);
  • при генерации новой версии надо пересобирать всё ПО (возможно это сказано громко но чаще всего неохота искать объектник который надо удалить и делается qmake, make clean, make);

Всех этих минусов лишён новый подход с помощью хуков гита. Сразу перечислю плюсы:

  • независимость типа проекта;
  • строгое соблюдение workflow;
  • формирование какого угодно формата версии;
  • небольшой плюсик — соблюдение данного workflow поможет легко генерить changelog;

Самое главное четко разделять назначение веток и использовать их! Сливать изменения в ветку develop должен только один разработчик. Перед слиянием неплохо бы провести тестирование. Тут немного спорный вопрос, ведь можно накидать в ветку develop новых фич и исправленных багов, а  например, jenkins сам всё протестирует. Но до такой автоматики руки не доходят. В моём случае было необходимо, чтобы каждое увеличение номера версии означало, что в ПО добавилась новая фича или исправленный баг. Поэтому при использовании гит я пользуюсь следующими правилами:

  • Каждая фича это обязательно отдельная ветка;
  • Каждый баг это обязательно отдельная ветка;
  • Слияние веток в ветку develop происходит с опциями —no-ff —no-commit (git merge —no-ff —no-commit FEATURE|BUG), тут происходит увеличение номера версии;
  • Все фичи и баги объединяются в ветку develop;
  • В ветку master объединяется только из ветки develop;
  • В ветку master объединяемся только с опцией —ff;

Теперь про сам хук

Код хука не большой, просто читает файлы, инкрементирует и записывает новую версию софта в файл. Главное не забыть поставить права на выполнения файла хука, когда его положите в папку .git/hooks!

Хук использует файлы для получения информации о формате версии софта, о числе в этом формате, которое необходимо увеличить и название ветки в которой необходимо выполнять инкремент. Все эти файлы должны находиться в корне исходников. (Да… пока файлов много, руки не доходят чтобы сделать все настройки в один файл)

  • version-branch_build.txt — содержит имя ветки в которой делается инкремент («develop»);
  • version-build.txt — содержит текущий номер («1»);
  • version-template.txt — содержит шаблон для формата версии («0.%build%»);
  • version.txt — версия софта записывается в этот файл («0.1»);

Вообщем-то сам хук, ничкго кроме той работы с файлами что описана выше не делает. Вот его код:

#!/bin/bash

file_build_number="version-build.txt"
file_template_version="version-template.txt"
file_full_version="version.txt"
file_up_build_number_branch="version-branch_build.txt"

current_branch=`git symbolic-ref --short HEAD`
up_build_number_branch=`cat $file_up_build_number_branch`

if [ ! -f $file_build_number ] || [ ! -f $file_template_version ] || [ ! -f $file_full_version ] || [ ! -f $file_up_build_number_branch ]; then 
	echo "Warning: can't increment build version! Check files: \"$file_build_number\", \"$file_template_version\", \"$file_full_version\" and \"$file_up_build_number_branch\" in root dir git storage."
	exit 1
fi


echo "Current branch: $current_branch"
echo "Up build number branch: $up_build_number_branch"
if [ "$current_branch" != "$up_build_number_branch" ]; then
	exit 0
fi


number_build=`cat $file_build_number`
number_build=$(( number_build+1 ))
echo -ne $number_build > $file_build_number

cat $file_template_version | sed "s/%build%/$number_build/g" > $file_full_version

git add $file_up_build_number_branch
git add $file_build_number
git add $file_template_version
git add $file_full_version

echo "Current soft version: $file_full_version"

exit 0

Пример использования

Соблюдая выше описанные правила создаю репозиторий, программирую и дохожу до состояния версии v0.1. Дальше предположим мне ставят задачу реализовать новую функциональность и допусим называется она «фича #1». Делаю следующее, создаю для фичи отдельную ветку f_1, создаю новый функционал, например в три коммита и теперь мне необходимо его добавить в тестовую сборку которая создается из ветки develop. Делаю всё это так:

git init
# ...
# пишем код в девелоп
# ...
git checkout -b f_1
# ...
# пишем фичу 1
# ...
git checkout develop
git merge --no-ff --no-commit

Самое главное здесь это последняя строка. Тут две небольшие изюминки для workflow:

  • запретить фаст-форвард необходимо для создания аккуратной истории: первое — один коммит в develop это один инкремент; один инкремент — это одна фича или исправленный баг; одна фича или исправленный баг — это одна запись в changelog;
  • запретить коммит требуется для самостоятельного создания коммита чтобы отработал хук. Возможно и коммит в git gui писать приятней…

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

После git merge

После git merge

Пишем наш коммит

Пишем наш коммит

Состояние после нашего коммита

Состояние после нашего коммита

 

 

 

 

Если таким образом создавать остальные фичи и исправленные баги, то история ветки develop останется чистой. Зачем мне чистая ветка develop без лишних левых коммитов? — для красивого и автоматизированого changelog!

Генерим changelog

При выходе новой версии очень хочеться видеть лог изменений от предыдущей версии. При использовании гит это достигается следующей командой:

$ git log --first-parent --oneline v0.1..
7504a2d Исправлен баг №1
2d21ff0 Добавлена фича №2
6c9e518 Добавлена фича №1

Вкратце опишу опции:

  • first-parent — родительская ветка при слиянии;
  • oneline — получение лога в одну строку. Быстро набирать, легко запомнить
  • v0.1.. — тег или хеш, двоеточия после означают вывести лог от v0.1 до текущего состояния

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

$git log --first-parent --format="%s (%ci)" v0.1..
Исправлен баг №1 (2015-08-20 20:34:40 +0300)
Добавлена фича №2 (2015-08-20 20:34:03 +0300)
Добавлена фича №1 (2015-08-19 22:53:40 +0300)

Плюшка

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

#!/usr/bin/env python

# -*- coding: utf-8 -*-


def test_develop_environment():
    path_git_hook = ".git/hooks/pre-commit"
    cmd_git_branch = "git rev-parse --abbrev-ref HEAD"
    file = "version-branch_build.txt"
    hash_hook = "fd97bf9a8fdd047292bacf0ce611b704"

    try:
        if os.environ["DEBUG"] != "1":
            return True
    except:
        # Если текущий переменной нет в среде, значит нет необходимости проверять среду рарзработки
        return True

    cmd_branch_current=subprocess.Popen(cmd_git_branch, shell=True, stdout=subprocess.PIPE)
    git_branch_current=cmd_branch_current.stdout.read()
    git_branch_current=git_branch_current.strip()

    file_branch_version=open(file)
    git_branch_version=file_branch_version.read()
    git_branch_version=git_branch_version.strip()

    if git_branch_current != git_branch_version:
        return True

    if os.path.isfile(path_git_hook):

        md5hash=hashlib.md5()
        md5hash.update( open(path_git_hook).read() )
        if hash_hook == md5hash.hexdigest():
            return True

        print u"Хеш сумма хука неверная, возможна эта версия устарела, скачайте требуемую версию."
        return False

    print u"При разработке для этой ветки необходимо использовать git hook для автоматического увеличения версии ПО"
    return False


if __name__ == '__main__':

    if not test_develop_environment():
        exit(1)

Вот и всё, сам хук и файлики для версии закинул сюда

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*