Статьи

Управление версиями программного обеспечения, не думая об этом

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

К счастью, сообщество open source решило обе эти проблемы для нас. Во-первых, у нас есть  семантическое управление версиями , которое однозначно определяет, как обновлять вашу версию, когда вы делаете исправления и исправления, незначительные улучшения, совместимые с предыдущими версиями , или критические изменения. Они даже определяют, как вы должны после исправления номеров версий обозначать предварительные версии и номера сборки. Предполагая, что все приличные программные проекты используют  Git в  эти дни, тогда другая проблема решается, следуя   стратегии ветвления GitFlow , инициативе земляка  Винсента Дриссена . GitFlow подробно описывает, в какой отрасли вы выполняете основную работу по разработке, как вы стабилизируете грядущий выпуск и как вы отслеживаете производственные выпуски.

Поэтому я на минутку предположу, что вы собираетесь принять семантическую схему управления версиями, а также следовать стратегии ветвления, предписанной GitFlow. Не было бы здорово, если бы существовал какой-то инструмент, который использовал бы имена веток и теги на master (представляющие рабочие выпуски) для генерации правильных номеров версий для вас? Хорошо, еще раз, сообщество открытого источника приходит на помощь. Джейк Джинниван  и ребята из ParticularLabs, компании, которая стоит за  NServiceBus , создали именно это и назвали его  GitVersion . Итак, давайте посмотрим, как это работает.

Демонстрируя красоту автоматического управления версиями

Во-первых, нам нужно установить GitVersion, что довольно просто благодаря  Chocolatey .

> choco install gitversion.portable

Это позволяет вам запустить его из командной строки. Вы также можете подключить его к вашей системе сборки, скопировав один исполняемый файл в систему управления версиями или добавив его в виде  пакета NuGet . Он обнаружит движки сборки, такие как TeamCity и AppVeyor, и примет вывод. Теперь давайте предположим, что у вас есть новый проект Git с первым коммитом в ветке master:

> mkdir GitDemo
> git init
> Initialized empty Git repository in D:/Tmp/GitDemo/.git/

> echo "hello" demo.txt
> git add .
> git commit -m "My first commit"
[master (root-commit) 2a34238] First commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 demo.txt

Теперь запустите инструмент командной строки GitVersion

gitversion

Это приведет к следующему выводу JSON:

{
  "Major":0,
  "Minor":1,
  "Patch":0,
  "PreReleaseTag":"",
  "PreReleaseTagWithDash":"",
  "BuildMetaData":0,
  "FullBuildMetaData":"0.Branch.master.Sha.2a34238911a4b47ec4b8794e27cd69c5857cb12a",
  "MajorMinorPatch":"0.1.0",
  "SemVer":"0.1.0",
  "LegacySemVer":"0.1.0",
  "LegacySemVerPadded":"0.1.0",
  "AssemblySemVer":"0.1.0.0",
  "FullSemVer":"0.1.0+0",
  "InformationalVersion":"0.1.0+0.Branch.master.Sha.2a34238911a4b47ec4b8794e27cd69c5857cb12a",
  "ClassicVersion":"0.1.0.0",
  "ClassicVersionWithTag":"0.1.0.0",
  "BranchName":"master",
  "Sha":"2a34238911a4b47ec4b8794e27cd69c5857cb12a",
  "NuGetVersionV2":"0.1.0",
  "NuGetVersion":"0.1.0"
}

So, by default your version numbering starts with 0.1.0. Notice the many variables targeted to specific uses. For instance, we store the InformationalVersion that includes the hash of the commit in the AssemblyInformationalVersion attribute of the assembly. Similarly, we use the NuGetVersion for our NuGet packages. Finally, our build numbers are mapped to the FullSemVer variable. That last part is pretty important, because traditional build numbers don’t say much about the actual changes. With Git versioning, rebuilding a particular commit renders the exact same number. This creates a whole lot more tracability.

Let’s follow Gitflow and continue development on the develop branch and run GitVersion.

> git checkout -b develop
> gitversion

Ignoring the remainder of the variables for now, this is the result:

"FullSemVer":"0.2.0.0-unstable"

You’ll notice two things. First, the minor version number is automatically updated. Second, the version number is post-fixed to make it pretty clear your working a development branch. Now let’s add a couple of commits and see what happens.

> echo "test" > demo2.txt
> git add .
> git commit -m "another commit"
> gitversion

This will result in the last digit representing the number of commits since we branched of from master.

"FullSemVer":"0.2.0.1-unstable"

So let’s work on a little feature through a feature branch.

> git checkout -b feature1
> echo "test2" > demo2.txt
> git add .
> git commit -m "another commit"

Running Gitversion again will give you:

"FullSemVer":"0.2.0-Feature.1+1"

And again it’s crystal clear what version you’re working on. To simulate working on some feature, I’ll assume a couple of more commits without repeating myself here. Running GitVersion after those changes results in:

"FullSemVer":"0.2.0-Feature.1+4"

Now it’s time to integrate those changes back into the develop branch.

> git checkout develop
Switched to branch 'develop'

> git merge feature1
Updating eb79902..8c374d1
Fast-forward
 demo.txt  | Bin 0 -> 16 bytes
 demo3.txt | Bin 0 -> 14 bytes
 demo4.txt | Bin 0 -> 14 bytes
 3 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 demo3.txt
 create mode 100644 demo4.txt

> gitversion
"FullSemVer":"0.2.0.5-unstable"

So, whatever you do, the version is always in sync with the changes without any manual tasks.

Now suppose principal development has completed and it’s time to stabilize the code base. Let’s make that explicit by starting a release branch.

> git checkout -b release-1.0.0
Switched to a new branch 'release-1.0.0'

> gitversion
"FullSemVer":"1.0.0-beta.1+0"

So it seems release branches are not for shipping release versions. Instead, they are used to ship beta versions of your system. Again, the +0 is used to denote the number of commits since the last time you shipped a beta package. Consequently, if you do ship such a package, you’re supposed to tag that commit with something like 1.0.0-beta.1. When you do, and you add any additional commits to that branch, the following happens.

> git tag 1.0.0-beta.1

> ..make some changes

> git commit -m "some more changes"
[release-1.0.0 76682ac] some more changes
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 demo5.txt

> gitversion
"FullSemVer":"1.0.0-beta.2+1"

As long as you’re stabalizing a system or shipping intermediate versions for acceptance testing or similar situations, stay on the release branch. By doing so, you can support both the upcoming version as well as the current production version. Now, when you’re ready to go into production it’s time to merge to master.

> git checkout master
Switched to branch 'master'

> git merge release-1.0.0
Updating 2a34238..76682ac
Fast-forward
 demo.txt  | Bin 0 -> 16 bytes
 demo2.txt | Bin 0 -> 14 bytes
 demo3.txt | Bin 0 -> 14 bytes
 demo4.txt | Bin 0 -> 14 bytes
 demo5.txt | Bin 0 -> 14 bytes
 5 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 demo2.txt
 create mode 100644 demo3.txt
 create mode 100644 demo4.txt
 create mode 100644 demo5.txt

What’s important to remember is that you should tag the commit you ship, so in this case we’re going to tag the merge result with the final release number. Let’s see what GitVersion will do with that.

> git tag 1.0.0
> gitversion
"FullSemVer":"1.0.0"

This marks the end of the development cycle. Any hot-fixes should end up on master as well, and will have their last digit incremented, e.g. 1.0.1, 1.0.2. Any successive development should continue on develop again, so as a last step, let’s switch back to that branch and see what happens.

> git checkout develop
Switched to branch 'develop'

> gitversion
"FullSemVer":"1.1.0.0-unstable"

Nice isn’t it? GitVersion understands the concept of that tag on master and will assume you’ll continue with the next minor version on the develop branch. If you really want to continue with a different version, there are ways to make that happen. And I’ve been just showing you the most common flow. I highly recommend checking out the examples in theGitVersion wiki.

In Summary

In short, you just need to remember a couple of things:

  • Development happens on the develop branch
  • Stabilizing upcoming releases and shipping beta packages happens from release- branches
  • The master branch tracks production releases and hot-fixes.
  • Anytime you ship something, you must tag that commit. No need for tracking releases anywhere else.

So what happens if you don’t have regular releases and your project needs to deliver continuously? Well, the guys at Github had the same questions and came up with an alternative to GitFlow called GitHubFlow. Fortunately, GitVersion supports this flow out-of-box as well.

So what branching strategy do you use? And how do you track your releases? Let me know by commenting below or tweeting me at @ddoomen.