You've successfully subscribed to developer.school
Great! Next, complete checkout for full access to developer.school
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info is updated.
Billing info update failed.

Using git-flow to Improve Software Delivery

Paul Halliday
Paul Halliday

We're extremely fortunate to have reliable version control systems in software development.  Whilst there are a variety of workflow, tools and GUIs available to assist with managing team delivery, we'll be investigating Git Flow by Vincent Driessen that was published in 2010.

Git Flow is predominately useful for versioned or projects not relying on continually deploying multiple times a day from the master branch.  This is because we're using the concept of a develop branch to release new versions of our application into master at a specified time, giving us an explicitly tagged release.

For a simpler approach that's effective in fast-paced projects that rely on several daily automated merges/deployments, the GitHub Flow will likely be an easy to implement alternative.

Let's dive in and investigate git-flow with a mobile application using Flutter. The actual code here is of little significance. It's all in the Git, baby!

Project Setup

Let's start by installing the git-flow CLI tool.

Installing the git-flow CLI tool

Although we can accomplish the same ideas here without using a CLI tool, the git-flow tool makes it as simple as typing commands like:

# Initialise Git Flow
$ git flow init

# New branch at feature/contacts
$ git flow feature start contacts

# feature/contacts merged into develop branch
$ git flow feature finish contacts

James M Greene has a great Markdown document which compares the git-flow commands to their underlying git calls if you're interested in knowing exactly what each command does. You can find that here.

To install git-flow on your respective platform, check out the installation instructions here. We're using git-flow-avh here as it seems to be better supported than the original git-flow repository.

New Project

To practice git-flow, go ahead and create a new project in a language/framework of your choice. I've elected to use Flutter for this, but the code is not of any relevance.

If you're following along, run the following in your terminal:

$ flutter create flutter_flow

$ cd flutter_flow

$ code . (or your favourite editor)

This will create a counter application where the number is increased every time a user taps a button.

Initialising a Git Repository

Flutter doesn't create a Git repository for us by default when we create a new project.  Let's initialise one now:

$ git init

Initialized empty Git repository in /flutter_flow/.git/

We can stage all files inside of the starter project by using:

$ git add -A

Finally, we can commit these as the "first commit" which will act as as the starting timeline for our project.

$ git commit -m "Initialise project"

Initialising git-flow

Now that we've got a base that we can compare our changes to, we can go ahead and initialise git-flow within this project:

$ git flow init
If you get a git-flow cannot be found error, then ensure you've installed the git-flow tool and restarted your terminal.

We're then asked a variety of questions which I've elected to keep as the default values:

Which branch should be used for bringing forth production releases?

Branch name for production releases: [master] 
Branch name for "next release" development: [develop] 

How to name your supporting branch prefixes?

Feature branches? [feature/] 
Release branches? [release/] 
Hotfix branches? [hotfix/] 
Support branches? [support/] 
Version tag prefix? [] 

The first thing that we'll notice is that we've been switched to the develop branch, and that branch matches the commit id of master (for now):

New Feature

Our stakeholders have came to us and said that our mobile application needs a new feature. They want to be able to:

  • List a varying number of Contacts

Apparently the counter application that we currently have has nothing to do with the ability to track Contacts. What do they know anyway? Pff.

We can use git-flow to create a new feature like so:

$ git flow feature start contacts

Switched to a new branch 'feature/contacts'

Summary of actions:
- A new branch 'feature/contacts' was created, based on 'develop'
- You are now on branch 'feature/contacts'

Now, start committing on your feature. When done, use:

     git flow feature finish contacts

We've now got three branches. Once again, they're all currently based off the latest commit ID, but that'll change in a second:

Contact

We'll start off by creating a very small Contact model, effectively consisting of a name:

@immutable
class Contact {
  final String name;

  const Contact({
    this.name,
  });
}

We can then use this Contact model to create a List<Contact> which will be used in our UI soon:

const List<Contact> contacts = [
  Contact(name: "Paul"),
  Contact(name: "Dave"),
  Contact(name: "Sarah"),
  Contact(name: "Catherine"),
  Contact(name: "Henry"),
];

Contact List Page

We can display the contacts inside of a ListView like so:

class ContactListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Contact List"),
      ),
      body: ListView.builder(
        itemCount: contacts.length,
        itemBuilder: (BuildContext context, int index) => ListTile(
          title: Text(
            contacts[index].name,
          ),
        ),
      ),
    );
  }
}

Finally, we'll need to update main.dart to set the home to our ContactListPage:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Contact List',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.indigo,
      ),
      home: ContactListPage(),
    );
  }
}

Committing Changes

We can commit the changes to our feature branch by using:

$ git add .

$ git commit -m "Added contact list and mock data"

[feature/contacts 4ee7abd] Added contact list and mock data

 4 files changed, 60 insertions(+), 113 deletions(-)
 rewrite lib/main.dart (94%)
 create mode 100644 lib/src/contacts/application/constants/contact_constants.dart
 create mode 100644 lib/src/contacts/domain/models/contact_model.dart
 create mode 100644 lib/src/contacts/presentation/pages/contact_list_page.dart

Finalising Contacts Feature

We've shown our stakeholders the new Contact List feature and they're ecstatic. They want this in production as soon as possible. Steady on. Before we do that we must "finish" this feature on our end and merge it into the develop branch.

If we look at the commit ids of our branches, now our feature/contacts is ahead of everything else:

Let's run the following git-flow command:

$ git flow feature finish contacts

This gives us the following messages (I've edited it a little to be more readable):

Switched to branch 'develop'
Updating 31ee053..4ee7abd
Fast-forward

4 files changed, 45 insertions(+), 98 deletions(-)
 
- create mode 100644 application/constants/contact_constants.dart
- create mode 100644 domain/models/contact_model.dart
- create mode 100644 presentation/pages/contact_list_page.dart

Deleted branch feature/contacts (was 4ee7abd).

Summary of actions:

- The feature branch 'feature/contacts' was merged into 'develop'
- Feature branch 'feature/contacts' has been removed
- You are now on branch 'develop'

We now don't have the feature/contacts branch anymore. The develop branch has the latest 4ee7abd commit id as seen in our feature/contacts branch.

If we were working with others on our team, or wanted it available on our origin, we could've called:

$ git flow feature publish contacts

This would've pushed the feature/contacts branch to our origin.

Starting a Release

So far we've achieved the following:

  • Created the feature/contacts branch and surrounding feature
  • Finalised the feature/contacts branch and updated the develop branch to match.

The last thing outstanding to satisfy our stakeholder is:

  • Release the new feature to production.

In order to do this, we'll start a release build using git-flow:

$ git flow release start 0.1.0

This has the following side effects:

Switched to a new branch 'release/0.1.0'

Summary of actions:

- A new branch 'release/0.1.0' was created, based on 'develop'
- You are now on branch 'release/0.1.0'

Follow-up actions:

- Bump the version number now!
- Start committing last-minute fixes in preparing your release
- When done, run:

     git flow release finish '0.1.0'

We once again have three branches now, but this time instead of a feature it's a release:

At this point, no more feature/X branches should be merged into the release/0.1.0 branch. Any late bug fixes or documentation oriented changes can happen here, but not much else should happen on this branch.

As we have no further changes to add at this point, let's finalise the release branch by using:

$ git flow release finish '0.1.0'

We'll have to set a commit message for our merge and tag. After doing so, we get:

Summary of actions:
- Latest objects have been fetched from 'origin'
- Release branch has been merged into 'master'
- The release was tagged '0.1.0'
- Release branch has been back-merged into 'develop'
- Release branch 'release/0.1.0' has been deleted

We can see that our tag was created and the release was assigned like so:

$ git show 0.1.0

tag 0.1.0
Tagger: Paul Halliday
Date:   Sun Dec 6 15:36:21 2020 +0000

Release 0.1.0

commit 77682d1e9865b78c3761fe7e5ec056bd4b783b6d (HEAD -> master, tag: 0.1.0)
Merge: 31ee053 4ee7abd
Author: Paul Halliday
Date:   Sun Dec 6 15:34:36 2020 +0000

    Merge branch 'release/0.1.0'

If we wanted the tags to appear on our origin, we'd have to enable the auto pushing of annotated tags inside of our git config like so:

git config --global push.followTags true

If we didn't want to push every tag then we can do this manually instead:

# 0.1.0 tag only
$ git push origin 0.1.0

# All tags in project
$ git push --tags origin

Our final branch structure now looks like this:

Hotfixes

Uh oh. We just had a conversation with one of our stakeholders. They're a little upset that our Contacts list doesn't contain the head of the board. They want this to be updated, today.

Our Contact list isn't based off any database and instead is just a file inside of our repo, so this just needs updating.  The problem is, we've already deployed our release to production.

So, we think to ourselves... is this a new feature? No. It's a hotfix!

New Hotfix

Hotfixes can be used to patch production (master) releases and we can create a new one like so:

$ git flow hotfix start 0.1.1

This has the following side effects:

Switched to a new branch 'hotfix/0.1.1'

Summary of actions:
- A new branch 'hotfix/0.1.1' was created, based on 'master'
- You are now on branch 'hotfix/0.1.1'

Follow-up actions:
- Bump the version number now!
- Start committing your hot fixes
- When done, run:

     git flow hotfix finish '0.1.1'

Notice the key branch difference here. This hotfix branch is based off master and not the current develop branch.

In our scenario the develop branch doesn't have any extra merged features, but imagine a larger team where this branch could've changed a myriad of ways prior to creating this hotfix.

We now have four branches as expected:

We can add the stakeholder to the list like so:

const List<Contact> contacts = [
  Contact(name: "Paul"),
  Contact(name: "Dave"),
  Contact(name: "Sarah"),
  Contact(name: "Catherine"),
  Contact(name: "Henry"),

  // New
  Contact(name: "Giles")
];

We'll then need to commit this to our hotfix/contact-names branch:

$ git commit -am "Updated contact list"

[hotfix/0.1.1 3063eaa] Updated contact list
 1 file changed, 3 insertions(+)

We're ready to have this merged back into master and develop.

Finalise Hotfix

This can be finalised in a similar way to every other git-flow feature. By running the following:

$ git flow hotfix finish 0.1.1

This has the following side effects:

Switched to branch 'master'

Merge made by the 'recursive' strategy.
 
- lib/src/contacts/application/constants/contact_constants.dart | 3 +++
 1 file changed, 3 insertions(+)

Switched to branch 'develop'

Merge made by the 'recursive' strategy.

- lib/src/contacts/application/constants/contact_constants.dart | 3 +++
 1 file changed, 3 insertions(+)

Deleted branch hotfix/0.1.1 (was 3063eaa).

Summary of actions:
- Latest objects have been fetched from 'origin'
- Hotfix branch has been merged into 'master'
- The hotfix was tagged '0.1.1'
- Hotfix branch has been back-merged into 'develop'
- Hotfix branch 'hotfix/0.1.1' has been deleted

We now have two tags 0.1.0 and 0.1.1:

If we take a look at our git log for both the master and develop branch, we can see that our hotfix has been applied:

$ git log

commit afb78ac902e183fd059d25532aead01d0daabb92 (HEAD -> develop)
Merge: 4ee7abd 3063eaa
Author: Paul Halliday
Date:   Sun Dec 6 16:11:34 2020 +0000

    Merge branch 'hotfix/0.1.1' into develop

commit 3063eaa986ccfda676fd8d363876d4659b6b51c5
Author: Paul Halliday
Date:   Sun Dec 6 16:10:42 2020 +0000

    Updated contact list

commit 77682d1e9865b78c3761fe7e5ec056bd4b783b6d (tag: 0.1.0)
Merge: 31ee053 4ee7abd
Author: Paul Halliday
Date:   Sun Dec 6 15:34:36 2020 +0000

    Merge branch 'release/0.1.0'

Summary

In this article we looked at how to use git-flow in the majority of use-cases. It may not always be right for your project and requirements, but it does offer a workflow to work with release-based repositories.

Fluttergit

Paul Halliday

👋 Want to see more content? Head over to the YouTube channel: https://youtube.com/c/paulhalliday!