28 марта 2012 в 07:43

Полуавтоматическое выставление номера версии с помощью git tutorial

Гуляя по github'у я много раз видел в разных репозиториях одновременно и теги вида «v2.3.4» и коммиты с сообщениями типа «Bump version» и сменой номеров версии где-нибудь в lib/version.rb. И всегда мне казалось — что-то тут лишнее.

И когда пришло время задуматься и мне над расставлением номеров версий, я сказал: «Нет! Я не буду прописывать эти номера в файлы руками. Пусть это делает за меня моя система контроля версий!»

Итак, как вижу это я:


  1. Номер версии выглядит как тройка чисел: мажорная.минорная.патч-версия и увеличивается согласно рациональной политике назначений версий;
  2. При выпуске каждой новой минорной версии я для определённого коммита в истории создаю тег вида v<мажорная>.<минорная>;
  3. Git автоматически выдаёт мне номер версии, основываясь на последнем тэге (для этого коммита) и количестве коммитов, после него.


Как это работает:


В составе git есть много полезных команд, нас будут интересовать две:
  1. git describe — показывает информацию о самом последнем тэге, доступном из данного коммита;
  2. git show — отображает различные типы объектов (но нас будет интересовать только текущий коммит).


Вооружившись этими знаниями, я в свой rails проект в папку config/initializers поместил файл version.rb следующего содержания:

module AppName
  module Version
    Described = (IO::popen('git describe --long') { |gs| gs.read }).strip
    Number = Described.gsub('-', '.').gsub(/^v/, '').split('.')[0..-2].join('.')
    Date = Date.strptime( IO::popen('git show -s --format="%ct"') { |gs| gs.read } , "%s")
    Revision = (IO::popen('git show -s --format="%H"') { |gs| gs.read }).strip
  end
end


Здесь происходит следующее:

Команда git describe --long выводит сообщение примерно следующего вида: v0.8-3-g387f83f, где
  • v0.8 — имя тега
  • 3 — количество коммитов после этого тега (0 если тэг поставлен на текущий коммит)
  • 387f83f — короткий хэш текущего коммита.

Строчка Described.gsub('-', '.').gsub(/^v/, '').split('.')[0..-2].join('.') приводит это сообщение к виду 0.8.3

Командой git show -s --format="%ct" мы получаем дату создания текущего коммита в формате Unix.

А git show -s --format="%H" возвращает нам полный хэш коммита.

Результат


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

"Приложение AppName. Версия #{AppName::Version::Number} от #{I18n.l AppName::Version::Date, :format => :long} года."

И получить
"Приложение AppName. Версия 0.8.3 от 27 марта 2012 года."


Плюсы и минусы


Плюсы
  • Париться над номерами версий теперь приходиться меньше, да и случаи «А, блин, забыл увеличить номер версии!» теперь будут реже (всё-таки минорные и мажорные версии не каждый день выходят, правда?);
  • (Как результат предыдущего пункта) Устраняется потенциальная возможность несоответствия номеров версий в репозитории и в коде (в коде их больше нет);
  • Идеально подойдёт для компилируемых программ (С++), как часть процесса сборки.

Минусы
  • Вне репозитория работать не будет (а кто и зачем собрался держать код вне репозитория?);
  • Жёсткая привязка количества коммитов к патч-номеру версии (с другой стороны — не делайте коммит на каждую строчку кода);
  • Иногда номер версии всё-таки должен быть прописан жёстко;
  • Не подойдёт для, например, PHP, так как вызывать пару тройку shell-комманд на каждый запрос — накладно.


Что почитать?


Ну, во-первых, мануалы по git describe и git show.

А вообще, поиск по "<CVS> autoversioning" (где <CVS> — название вашей любимой системы контроля версий) выдаст вам немалое количество рецептов для любых случаев, программ и технологий.

Успехов вам в наведении порядка (хотя бы в версиях).
Новиков Андрей @Envek
карма
83,3
рейтинг 0,3
Похожие публикации
Самое читаемое Разработка

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

  • 0
    А как потом можно найти коммит, имея номер тега и количество коммитов после этого тэга?
    • 0
      Скорее всего никак (или очень затруднительно). История в git — однонаправленный граф, и просто идти «вперёд» по истории не получится.
      А зачем? Если так нужно для последующей возможной ловли багов, то проще хранить сразу хэш коммита для этой цели (и показывать его где-нибудь в «О программе»).
      • 0
        Да, для багов и тестирования. Хэш храню и показываю, но числовая версия для пользователей проще и человечнее, чем хэш :)
        Вообще, весь список коммитов после тэга можно посмотреть через git log v0.8..master (и git describe именно так высчитывает количество коммитов), но я не уверен, что там не возникнет какая-нибудь неопределенность с порядком коммитов из-за merge команд.
    • +1
      В выводе git describe есть первые несколько символов идентификатора коммита. Собственно, этого будет достаточно.
  • +2
    Идея достаточно хорошая. И всё же, её можно немного расширить для масштабируемости следующим образом:

    1. Делим всю работу на бранчи master/develop/feature/hotfix и далее по вкусу, как описано в куче трудов.
    2. Если вы привязываетесь к количеству коммитов, то логичным будет всю работу делать в feature-бранчах, и по окончанию этой работы делать git merge --no-ff в develop-ветку.

    Результат: это даст увеличение количества коммитов в develop-ветке ровно на один, что будет соотвествовать единице добавленного функционала. То есть в dev-бранче +1 версия = +1 реализованный функционал. Удобно.
    В feature-бранче же версия, посчитанная вашим образом, имеет конечный смысл для разработчика. И поэтому даст свободу по количеству коммитов в feature-бранче. Я например часто коммичу, тк стараюсь делать это достаточно атомарно.
    Но, к сожалению, неизбежно возникает конфликт версий между dev-веткой и feature-веткой, тк версии считаются одинаково. Поэтому, я бы предложил форматировать версию для feature-ветки как-нибудь так:

    Версия 0.8.2-<имя feature-ветки>-<количество коммитов в ней>, где 0.8.2 — версия из dev, от которой пошла feature-ветка. Конечный результат:

    0.8.2-new_ui-9

    Схема:

    Версия 0.8.2|...........-> 0.8.3
    Ветка dev -----------------------
    Ветка new-ui \........./


    Имхо, так лучше.
    • 0
      Не сработает:
      mkdir tmp && cd tmp
      git init .
      # Корневой коммит, он же v1.0
      touch a && git add a && git commit -m "0"
      git tag -a -m "version 1.0" v1.0
      # Переключаемся в ветку devделаем работу в ней
      git checkout -b dev
      touch b && git add b && git commit -m "1"
      # Обратно в мастер
      git checkout master
      touch c && git add c && git commit -m "2"
      # И снова в dev
      git checkout dev
      touch d && git add d && git commit -m "3"
      # А теперь merge
      git checkout master
      git merge --no-ff dev
      # И что в итоге?
      git describe
      

      И…

      v1.0-4-geafc8e0

      Упс, а посчитались-то все коммиты.

      После merge'а git уже не может сказать, какой же ветке принадлежал коммит, поэтому считает все. И я не знаю, как git describe'у сказать «а в мёрдж-коммитах следуй по первой ссылке на родителя».
      • 0
        Сорри за некропостинг, но сейчас есть ключик --first-parent.
        • 0
          Наоборот, спасибо вам большое.
      • 0

        Немного покопавшись я нашел ключик --merges у git log. В общем это выглядит так:


        #/bin/bash
        tag=`git describe --abbrev=0`
        rev=`git log $tag.. --oneline --merges | wc -l `
        echo "$tag-$rev"
  • +3
    а кто и зачем собрался держать код вне репозитория?

    Например, в src-пакетах всех ведущих дистрибутивов Linux.
    • 0
      Хм, ну там да… Но у меня не модуль, не библиотека и не компонент. Так что исходный код всегда в репозитории. На машине ли разработчика или на сервере, но из под надзора CVS он не уходит ни на минуту.
  • +1
    Еще полезная ссылка на почитать: semver.org
  • 0
    Вот чем хорош был svn, так вот этим :)
    • +1
      Если вы имеете в виду $Revision, то он бесполезен, так как относится к конкретному файлу, а не ко всему проекту.
      • –1
        Или я вас не понял, или вы что-то путаете.
        В svn сквозная нумерация ревизий. Ревизия файла и проекта по сути одно и тоже.
        • 0
          svnbook.red-bean.com/en/1.4/svn.advanced.props.special.keywords.html
          Revision
          This keyword describes the last known revision in which this file changed in the repository, and looks something like $Revision: 144 $. It may also be specified as LastChangedRevision or Rev.
          • –1
            При чем тут svn keywords вообще?
            • +1
              Так чем же по вашему хорош svn в контексте топика? Если не фичей автоматической подстановки ревизии в файлы, тогда чем?
  • +3
    >(а кто и зачем собрался держать код вне репозитория?)

    Ну вот тут товарисчи держали код в репозитории...
  • +1
    >Не подойдёт для, например, PHP, так как вызывать пару тройку shell-комманд на каждый запрос — накладно.

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

    В гите есть замечательный .git/hooks/post-merge хук, в котором можно всю эту инфу единоразово положить в файл version.php (.rb, .py...) и подключать его.

    Итого в коде всегда будет актуальная переменная (константа, класс) которая будет содержать эту информацию.

    Спасибо за git describe!
    • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Ну да, как вариант — использовать хуки… Вот только я не знаю — передаются ли хуки по pull/push, или для каждого клона их придётся прописывать заново?
      Так как я всё ещё не определился, хранение номера версии в коде — это зло или благо? Может быть post-merge (или лучше pre-commit) хук, вычисляющий и записывающий номер версии в файл (и добавляющий его в коммит).
      Я думаю, что такой вариант будет золотым компромиссом: и версии автоматом ставятся и в коде есть.
      Но здесь есть одно НО: при создании тэга потребуется переписать этот и все последующие коммиты (а изменение истории — это совсем нехорошо, если эти коммиты уже ушли на гитхаб или ещё куда в паблик). Но это касается только случая, когда этот файл version.ext включен в репозиторий (не игнорируется).
      • 0
        >Так как я всё ещё не определился, хранение номера версии в коде — это зло или благо?
        Так ничто не мешает этот файл добавить в .gitignore ) и не будет лишней мороки с коммитами

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

        А вообще, зачем эта информация нужна на каждой странице? Можно сделать special-url для тестеров… заметят баг — откроют, скопипастят номер версии в тикет и всего то и делов.
  • 0
    Не подойдёт для, например, PHP, так как вызывать пару тройку shell-комманд на каждый запрос — накладно.

    Кроме этого, держать директорию .git доступную через веб сервер тоже как то не хорошо.
  • +1
    Мне хватает github.com/svenfuchs/gem-release
  • 0
    $ hg archive ../foo
    $ cat ../foo/.hg_archival.txt 
    repo: 6c3930f0d96db9a2e9a1bd217d82c5e2f97f8528
    node: 482828da4b9e3eb06cbbcf172a2c32a85767f805
    branch: default
    latesttag: null
    latesttagdistance: 3
  • 0
    В Qt проекте можно написать так:

    GIT_VERSION = $$system($$quote(git describe))
    GIT_TIMESTAMP = $$system($$quote(git log -n 1 --format=format:"%at"))
     
    QMAKE_SUBSTITUTES += $$PWD/version.h.in
    


    А уже в version.h.in написать что-то вроде этого:

    #define APP_TIME         $$GIT_TIMESTAMP
    #define APP_VERSION      $$replace(GIT_VERSION, "^v(\\d+)\\.(\\d+)\\.(\\d+)-(\\d+)-(.*)$", "\"\\1.\\2.\\3.\\4\"")
    #define APP_VERSION_RC   $$replace(GIT_VERSION, "^v(\\d+)\\.(\\d+)\\.(\\d+)-(\\d+)-(.*)$", "\\1,\\2,\\3,\\4")
    #define APP_REVISION     $$replace(GIT_VERSION, "^v(\\d+)\\.(\\d+)\\.(\\d+)-(\\d+)-(.*)$", "\"\\5\"")
    


    Это перед сборкой сгенерит адекватный version.h файл.

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