Усилия и деньги, вкладываемые в продвижение языка Go, часто приносят пользу и другим разработчикам. В конце прошлого года на сайте gopheracademy была опубликована очень удачная статья о семантическом версионировании. Том самом, которое используется в npm, начинается с домика ^ и все ломает. Под катом спрятан перевод, который поможет вам быстро осмотреть сад граблей версионирования и как сейчас принято им пользоваться. И немного примеров на Go. Передаем слово автору!


Семантическое версионирование (также известное как SemVer) уже стало популярным подходом к работе с версиями программ и библиотек. Оно не только облегчает работу с последовательными релизами, но позволяет и людям, и автоматике понимать совместимость этих релизов друг с другом и с остальным миром. SemVer можно использовать много где, но больше всего этот подход известен в системах управления зависимостями.

Прежде чем рассказывать о специфичных для Go вещах, давайте посмотрим, что такое семантическое версионирование (примечание переводчика: ради этой части и был затеян перевод).



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

  • “Major” номер версии. Увеличивается (и не всегда на 1, вспомните PHP 5 -> 7), когда API библиотеки или приложения меняется обратно несовместимым образом.
  • “Minor” номер версии. Увеличивается, когда в API добавляются новые функции без нарушения обратной совместимости (примечание переводчика: вот здесь самое зло. К примеру, npm по умолчанию сохраняет версию с префиксом “^”, что означает “любая версия с такой же major”. Авторы npm резонно полагают, что если при смене minor обратная совместимость не теряется, то можно смело обновлять. Сюрприз: множество разработчиков библиотек просто не знают, что такое семантическое версионирование и ломают обратную совместимость, как получится. Результат – на компьютере автора все работает, а при установке “с чистого листа” на другом компьютере мы получаем новые версии зависимостей с поломанной обратной совместимостью). При увеличении номера “major” принято сбрасывать “minor” в 0, то есть за версией 4.27… последует версия 5.0… а не 5.28
  • “Patch” номер версии. Увеличивается при исправлении багов, рефакторинге и прочих изменениях, которые ничего не ломают, но и новых фичей не добавляют. При увеличении “Major” или “Minor” принято сбрасывать “Patch” в 0 (примечание переводчика: очень часто его называют “build number” и просто увеличивают на единицу с каждый билдом continuous integration, никогда не сбрасывая. Это позволяет легко отличать билды друг от друга во время тестирования и анализа сообщений ошибок от пользователей. Минус такого подхода в том, что довольно скоро цифра становится пятизначной. Но многих это не беспокоит).
  • Строка “pre-release”: необязательный, разделенный точками список, отделенный от трех номеров версии знаком минус. Например: “1.2.3-beta.2”. Используется вместо тегов, чтобы “помечать” определенные вехи в разработке. Обычно это “alpha”, “beta”, “release candidate” (“rc”) и производные от них.
  • Завершает строку семантической версии метаданные системы сборки. Так же, как и список “pre-release” тегов, он разделен точками ., но отделяет от номеров или тегов его не минус, а плюс. Данную информацию принято игнорировать (примечание переводчика: по феншую номер билда должен идти сюда – но мало кто так делает, очень длинные строки получаются).


Несмотря на то, что в спецификации ничего не говорится о префиксе “v”, он часто используется перед строкой семантической версии, например, “v.12.3”. Так же, как и метаданные, его принято игнорировать.

Все это и многое другое можно найти в официальной спецификации: http://semver.org/

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

Парсинг семантических версий в Go


Для Go доступно несколько пакетов для работы с семантическими версиями. В этой статье я рассмотрю вот этот: github.com/Masterminds/semver. Он соответствует спецификации, поддерживает необязательный префикс “v”, сортировку, работу с диапазонами и ограничениями. При этом с ограничениями этот пакет работает так же, как большинство решений для других языков программирования, таких как JavaScript, Rust и другие.
Пример ниже парсит строку семантической версии и выводит либо “major” версию, либо сообщение об ошибке:

v, err := semver.NewVersion("1.2.3-beta.1+build345")
if err != nil {
    fmt.Println(err)
} else {
    fmt.Println(v.Major())
}

Возвращаемое значение является экземпляром semver.Version, который содержит некоторое количество полезных методов. Если переданная строка не является семантической версией, то возвращается ошибка semver.ErrInvalidSemVer.

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

Сортировка семантических версий


С помощью библиотеки semver вы можете сортировать семантические версии средствами стандартной библиотеки. Например:

raw := []string{"1.2.3", "1.0", "1.0.0-alpha.1" "1.3", "2", "0.4.2",}
vs := make([]*semver.Version, len(raw))
for i, r := range raw {
    v, err := semver.NewVersion(r)
    if err != nil {
        t.Errorf("Error parsing version: %s", err)
    }

    vs[i] = v
}
sort.Sort(semver.Collection(vs))

В этом примере набор семантических версий преобразуется в экземпляры semver.Version, которые затем складываются в semver.Collection. А у semver.Collection есть все необходимое для использования со стандартной библиотекой sort. Это очень удобно для правильной сортировки pre-release информации и игнорирования мета-тегов.

Диапазоны, Ограничения и Wildcards


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

c, err := semver.NewConstraint(">= 1.2.3, < 2.0.0, != 1.4.5")
if err != nil {
    fmt.Println("Error parsing constraint:", err)
    return
}

v, err := semver.NewVersion("1.3")
if err != nil {
    fmt.Println("Error parsing version:", err)
    return
}

a := c.Check(v)
fmt.Println("Version within constraint:", a)

Для тех, кто знаком с диапазонами версий по другим языкам и тулзам, библиотека предлагает хорошо знакомую нотацию:

  • ^1.2.3 обозначает совместимость на уровне “major” версии. Это синтаксический сахар для записи “>= 1.2.3, < 2.0.0”. Используется для указания “самой свежей версии зависимости, все еще совместимой по API”.
  • ~1.2.3 обозначает совместимость на уровне “patch” версии. Это синтаксический сахар для “>= 1.2.3, < 1.3.0”. Используется для указания нужной версии с последними багфиксами, но без серьезных изменений.
  • “1.2.3 — 3.4.5” обозначает точный диапазон версий, включая начальную и конечную. Это синтаксический сахар для “>= 1.2.3, <= 3.4.5”.
  • Также библиотека поддерживает “wildcards” с помощью символов “x”, ”X” или “*”. Вы можете указать версию как “2.x”, “1.2.x” или даже как “*”. И все эти нотации можно спокойно комбинировать друг с другом.


Начните использовать семантические версии прямо сейчас


И ваши волосы станут мягкими и шелковистыми! Если в вашем проекте есть работа с версиями, то предлагаю не терять время и начать пользоваться стандартом семантического версионирования. Для Go есть описанная выше библиотека github.com/Masterminds/semver, большинству современных языков и тулчейнов тоже есть, что предложить. Особенно Node.js с npm.

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


  1. steff
    14.04.2016 15:21
    +2

    Разработчики Хрома и Firefox, похоже, не в курсе :)


    1. rumkin
      14.04.2016 15:29
      +1

      У них гонка версий, а не версионирование. Скоро можно будет топ делать за сколько до сотни доберутся.


      1. Shultc
        14.04.2016 15:43
        +2

        Когда начнут сразу по 10 за раз прибавлять, так всё ещё быстрее пойдёт!


      1. berezuev
        14.04.2016 15:57
        +4

        хм… Зашел посмотреть, какая у меня сейчас версия хрома… Так хром взял, и с 49 обновил до 50-й...)


        1. AleksDesker
          19.04.2016 02:47

          Прочитал ваш коммент, пошел посмотреть, а какая у меня… обновил хром. Походу вы запустили цепную реакцию :)


          1. khim
            19.04.2016 13:13
            +1

            Хех. Обычно Хром «пропихивается» несколько дней, иногда — неделю или две и при этом смотрится — не началась ли «цепная реакция» (резко увеличилось количество крешей, например).

            Так как речь идёт об ошибках проявляющихся в количества «штук на миллион», то «на кошках» такое не отловишь, увы… А проблемы могут быть реальными, так что сразу на всех выкатывать новую версию опасно…

            Так и получается, что новая версия уже есть, но у вас её нет, так как вы стоите где-то в хвосте «очереди». Но если человек полез-таки смотреть какая у него версия, то, стало быть, ему это отчего-то важно. В этом случае его переставляют в начало оной очереди.

            Так что эффект «пошел посмотреть, а какая у меня… обновил хром» — это не «совпадение», а «фича».


    1. Kain_Haart
      15.04.2016 08:20

      Semver предназначен для версионирования продукта, предоставляющего внешний api. Chrome и Firefox скорее являются продуктом для пользователей.


      1. vlivyur
        15.04.2016 10:23
        +3

        Разработчики расширений согласны?


    1. encyclopedist
      15.04.2016 18:05

      В курсе.


      На самом деле они используют семантическое версионирование. Просто у таких проектов, которые состоят из очень большого числа компонентов и очень большого числа разных API каждая версия является мажорной. Потому что в каждой версии будет содержаться новое мажорное несовместимое изменение какого-нибудь компонента (то рендеринг HTML, то свойства CSS, то Javascript, то интерфейс, то ещё что-то).


      То же самое касается операционных систем, (и их ядер, оттого и смена версионирования Линукса).


      Семантическая версия плохо с


  1. rumkin
    14.04.2016 15:24
    +1

    Хорошая статья, но я бы не "предлагал", а "настоятельно рекомендовал" использовать semver.


  1. amarao
    14.04.2016 17:06
    +5

    Простите, а как это соотносится с debian'овской системой нумерации пакетов? И где эпоха в версии? Почему «semver» следует предпочесть существующей модели именования версий, в которой учтено всё на свете — от взбрыков разработчиков по переименованию системы нумерации версии (windows 95 и windows 10), до особенностей системы сборки?

    Дборотная версия выглядит примерно так:

    2:34.5.6-nmu7+8~20160309142337.19~1.gbp08c0c0

    2 — номер эпохи. Означает, что до «34» была какая-то другая версия. Например, «2000», и это «починили».
    34.5.6 — что хотите. Версия и версия.
    nmu — собиралось не мейнтейнером. Убунтововды там «ubuntu» пишут.
    7 — версия сбоки
    + и далее — версия из гита (что именно собиралось). Год, дата, время, потом после тильды — очередная локальная эпоха и gbp (кто собирал) и первые цифры коммита из которого собиралось.

    Это я не фантазирую, это индустриальный стандарт.


    1. kronoskib
      14.04.2016 17:40
      +2

      Предполагаю, что чем проще — тем лучше. И потом, это же для мейнстрима — node.js, go, python, ruby, c#, java итд. Для энтерпрайзов с чпуксом наперевес вполне можно использовать и что-то более «тяжеловесное».


      1. amarao
        14.04.2016 18:03

        Это версия питоновской приложухи. Мейнстримовая. Без энтерпрайза.


        1. khim
          14.04.2016 18:41
          +1

          По моему — это бессмысленный спор о терминах. Есть symver. Фигня вопрос: поддерживается (с некоторыми вариациями) для библиотек (но не дла названий сами приложений!) в GCC и libtool, для пакетов в NPM и в Gentoo, а также в куче разных других мест.

          Есть нумерация пакетов в Debian'а. Применяется, вы не поверите, для нумерации пакетов в Debian'е. Причём для библиотек, вы не поверите, Debian использует как раз symver тоже. Правда в извращённом «ручном» виде, но использует (именно поэтому там есть пакеты libc6 и liblzma5, а не просто libc и liblzma).

          Что считать мейнстримом — дело ваше. Но если вы собираете библиотеку — то лучше, наверное, использовать систему, предназначенную для работы с библиотеками, а не что-то что используется в совсем других случаях и совсем для другого.


    1. safinaskar
      14.04.2016 17:45
      +2

      semver — это один из способов нумеровать версии у одного данного конкретного проекта. Система Debian'а — это способ нумеровать версии у пакетов в дистрибутиве, где почти каждый пакет имеет апстрим. Т. е. апстрим выпустил новую версию, стоИт задача правильно дать номер версии для соответствующего пакета Debian. Т. е. задачи разные у этих двух нумераций.

      Упомянутый номер эпохи ярко это демонстрирует. Зачем нужен номер эпохи в Debian'е? На случай, если апстрим тупанул и сменил систему нумерации. Допустим, апстим выпускал версии под номерами 10.0, 20.0, 30.0 (ну мало ли), а потом зачем-то решил убрать ноль и выпустил версию номер 4.0. Тогда мейнтейнеры пакета Debian увеличивают на единицу номер эпохи, чтобы не сломалось сравнение номеров версий.

      от взбрыков разработчиков по переименованию системы нумерации версии


      Ну вот я про это и говорю. Semver не для задачи подготовки пакетов некоего апстримного пакета. Там эти взбрыки учитывать не надо.

      Далее, semver на то и называется semver, что в нём учитывается semantic, семантика, смысл. Major, minor и patch выбираются исходя из смысла нового релиза, исходя из его предназначения, исходя из того, насколько сильно он отличается от предыдущих, насколько там важные фичи и так далее, и тому подобное. В Debian'е главный компонент номера версии (34.5.6 в вашем примере) не имеет никакого смысла в рамках всего проекта Debian в целом. Потому что он просто повторяет номер версии апстрима, а тот уже вкладывает в него смысл по своему усмотрению, например, с помощью того же semver.

      до особенностей системы сборки?


      Эту инфу можно занести в meta semver. Никакого стандарта на устройство этого meta не нужно, meta — это просто for your information, meta игнорируется при сравнении версий

      nmu — собиралось не мейнтейнером


      Это особенности чисто дебианого процесса разработки. Это не нужно в semver, в системе нумерации для всех и вся.

      7 — версия сбоки


      Этот номер означает, каким по счёту является данный пакет Debian на основе одного и того же данного upstream пакета. В semver это не нужно, там нет upstream'а и downstream'а.

      + и далее — версия из гита (что именно собиралось). Год, дата, время, потом после тильды — очередная локальная эпоха и gbp (кто собирал) и первые цифры коммита из которого собиралось.


      Всё это можно в meta, без стандарта на точное устройство meta, я уже говорил


    1. Kain_Haart
      15.04.2016 08:26
      +3

      34.5.6 — что хотите. Версия и версия.
      Вот здесь и предлагается использовать semver


      1. amarao
        15.04.2016 14:42
        +1

        А, ок.


  1. safinaskar
    14.04.2016 17:43

    deleted


  1. Oxoron
    14.04.2016 22:17
    +5

    Поправьте название, пожалуйста.
    Правильное: v3.14.1592-beta6:…


  1. prefrontalCortex
    15.04.2016 15:36

    Спецификация, безусловно, прекрасная, но вот как номер версии, удовлетворяющий ей, сгенерировать автоматически из git — я, например, не представляю :(
    Если делать, «как все», добавляя в репозиторий теги с minor.major (например, «v1.0») и вызывать git describe, то patch version будет добавляться через тире, вкупе с хэшем коммита с префиксом g, например, 1.0-42-gdeadbeef.


    1. khim
      15.04.2016 16:06
      +2

      Либо я ничего не понимаю, либо одно из двух. Сгенерировать номер версии автоматически нельзя. Какой смысл в автоматически сгенерированной версии? Кому и о чём она будет говорить? О том конкретно из каких исходников вы собрали ту или иную библиотеку? Ну так это номер билда, а не номер версии.

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

      Откуда git сможет получить информацию о том — как влияют на код ваши изменения и, соответственно, какую цифирьку увеличивать? Это только вы можете сделать…


      1. prefrontalCortex
        16.04.2016 18:22

        Сгенерировать номер версии автоматически
        Я не совсем правильно выразился, стоило написать «полуавтоматически». Major/minor в любом случае нужно выбирать самому вручную,
        добавляя в репозиторий теги с minor.major (например, «v1.0»)


        1. Zapped
          21.04.2016 23:09

          А по-моему, номер версии должен задаваться сервером сборок и проставляться уже в репозитории тегом. А git describe
          лишь служит тому, чтобы узнать в какую версию вошёл тот или иной коммит


  1. LexxFedoroff
    15.04.2016 19:15
    +1

    Мы для автоматического генерирования версий используем GitVersion.
    Очень удобная штука.


  1. nskazki
    18.04.2016 10:27

    Небольшая ремарка: до первого мажорного релиза ^ работает так же как ~
    Например: "^0.2.0" == ">= 0.2.0, < 0.3.0", но "^1.2.0" == ">= 1.2.0, < 2.0.0"
    Поэкспериментировать можно здесь: semver.npmjs.com