Simplified Git Flow

Kevin Kuchta
GoodToGoat
Published in
8 min readJun 16, 2017

There are a few popular, published patterns for using git on small teams. GitFlow is famous for scaring off new git users with an avalanche of boxes and arrows, while Github Flow takes an almost painfully simple approach.

When we considered them for our use at Joyable, each had their flaws:

  • GitFlow was a bit heavyweight for us. We practice continuous deployment and release multiple times a day, which doesn’t lend itself to the release-based workflow.
  • Github Flow didn’t really address situations like multiple engineers working on a single feature.

So, we struck out on our own a bit.

Background

For the first few years of Joyable, we used a slightly extended version of Github’s flow with rebases and feature branches:

  • Developers make normal changes on small, personal branches off of master.
  • Developers pull-requested their changes to master and --ff-only merged them in when they were approved.
  • Larger or multi-developer features were done on long-lived “feature branches” that might live for weeks or even a month or two. Changes were made on personal branches off of these feature branches, then pull-requested back into the feature branch.

As we grew, we found ourselves increasingly relying on multi-week feature branches for most dev work. We’d find ourselves with these big, heavyweight deploys that required a day+ of manual testing. In the spirit of continuous deployment, we’d much prefer regular, incremental deploys (with incomplete features being hidden behind feature flags).

We briefly experimented with eliminating multi-week feature branches altogether. Each developer ended up deploying several times a day, which became onerous pretty quickly. Our deploy process is only one command, but it takes 15 minutes to complete. Since we require deploying to a staging server before deploying to prod, it takes at least 30 minutes from PR approval to your code being live.

We needed a process that minimized large, high-risk deploys while still preventing engineers from spending an hour or two a day deploying.

The Simplified Git Flow

We ended up with a version of GitFlow, but with a few steps removed and some guidance on when to rebase and when to merge. The high-level principles:

  • The master branch is always deployed.
  • The develop branch is always ready to deploy.
  • develop is merged to master and deployed every morning (and as-needed after that if someone’s willing to go through the 30 minute deploy process).
  • Small personal branches off of develop are merged back into it regularly, using feature flags to release larger features.
  • Long-lived feature branches are still possible for features that are impractical to release piecemeal, but are discouraged.

To illustrate this flow, let’s walk through three normal scenarios:

1. A Normal Change

Consider an average feature or bugfix. If your change is small enough to fit in a single pull request, do this once and you’re done. If you need to split it across multiple PRs, use this process repeatedly and hide your change behind a feature flag.

Say I’m adding lasers to our product (since what project doesn’t need more lasers?).

First, I’ll create a new branch off of develop and do some work. Our convention is that personal branch names start with your initials.

# Create a new branch off of develop
git checkout develop
git pull
git checkout -b kk_lasers
git push kk_lasers
# Add some commits
git commit -a -m "Add laser 1"
git commit -a -m "Add laser 2"
git push kk_lasers
# Get the latest changes off of develop whenever needed
git checkout develop
git pull
git checkout kk_lasers
git rebase develop
git push -f kk_lasers

You’ll notice we’re using rebasing here instead of merging. Our goal is to avoid merge commits that don’t contain useful information. “Kevin grabbed the latest changes into his personal branch” isn’t likely to be helpful for anyone in the future, so we rebase to avoid having a bunch of Merge branch 'develop' into kk_lasers commit messages cluttering up our history.

Because we rebased, though, we need to force-push our branch to github (hence the -f, which causes a force-push).

When I think my changes are ready, I create a pull request on Github to merge kk_lasers into develop and wait for at least one engineer to review it. If there are any changes I need to make, I add them to the same branch so the PR is updated.

git commit -a -m "Add a missing laser"
git push kk_lasers

Once the PR is approved, I rebase the changes from develop into my branch one more time (to be sure no new changes landed on develop while the PR was open) and then merge my changes back into develop.

git checkout develop
git pull
git checkout kk_lasers
git rebase develop
git push -f kk_lasers
git checkout develop
git merge --no-ff kk_lasers
git push develop
# Remember to delete your branch on github if you're done with it
git push origin :kk_lasers

I’m using no-ff for my merge here because I do want a merge commit. Being able to look at the git log in develop and see “Kevin merged his branch into develop here” is useful information to us.

When an engineer goes to deploy develop tomorrow morning, they’ll check the git log.

git checkout develop
git pull
git log --oneline master..develop
# Example output:
1bc35e9 Merge branch 'ek_bike_shedding' into develop
3c8969f Adjust bike shed color
8d8187a Merge branch 'kk_lasers' into develop
07fd180 Add laser 2
1cbcbbc Add laser 1
44e901a Merge branch 'nt_yak_shaving' into develop
07f0182 Trim yaks

They can get an overview of what changes have been made by looking only at the commit messages starting with Merge branch… before deploying.

git checkout master
git merge --no-ff develop
git push master
git push production master # <- triggers a deploy

Again we use --no-ff to force a merge commit. Knowing which points in our git history were deployed (by looking for Merge branch 'develop' into master) is useful.

2. A Hotfix

Need to bypass everything to fix an urgent bug on production now? It’s hotfix time.

You can just create a branch off of master, then merge it back in (and to develop) when you’re done.

git checkout master
git pull
git checkout -b kk_hotfix
git commit -a -m "Urgent temperature-related fix"
git push kk_hotfix

At this point, unless it’s the dead of night and everyone’s offline, we still try to get a quick code review. If we don’t want to go through a whole github pull request, sometimes we’ll just paste the output of git diff master into Slack and ask for someone to approve it.

When we’re ready to ship:

git checkout master
git pull
git merge --no-ff kk_hotfix
git push master
git push production master # <- Triggers a deploy

As before, we use --no-ff to force a merge commit. That’ll let someone reviewing the history of master to see that we merged a branch in that wasn’t develop, and they’ll know it was a hotfix.

Finally, we want to get develop up to date:

git checkout develop
git pull
git merge master
git push develop

Ideally we’d use rebase here, but rebasing a shared branch is a bad time: every other developer is going to run into headaches if you do. Instead, we just use a simplemerge. If possible, git will make it a fast-forward (ff) merge. If not, we’ll get a low-information merge commit like Merge branch 'master' into develop.

3. A Long-lived feature branch

Although we discourage long-lived feature branches, sometimes it’s unavoidable. If we’re working on a feature that will take a few weeks to build and relies on a number of large database schema changes, it could be prohibitively difficult to make those schema changes without impacting existing code. “Possible”, sure—but more expensive in terms of engineering time than we’d like.

In cases like those, we use a long-lived feature branch off of develop. Engineers make their personal branches off of, and merge back into, that feature branch.

It looks complex, but this is really just two engineers using the “Normal Change” process off of the lasers feature branch.

To start, someone will create a new shared branch that’s expected to live a few weeks (or however long this feature project lasts).

git checkout develop
git pull
git checkout -b lasers
git push lasers

Then, when a developer starts work on this feature, the flow looks very similar to the “Normal Change” process above, except off of the feature branch instead of develop.

# Create my new personal branch
git checkout lasers
git pull
git checkout -b kk_lasers
git commit -a -m "Add the first laser"
git commit -a -m "Add the second laser"
git push kk_lasers
# Grab the latest changes from the feature branch as needed
git checkout lasers
git pull
git checkout kk_lasers
git rebase lasers
git push -f kk_lasers
# PR when my code is ready
# Merge it back into the feature branch when the PR's approved
git checkout lasers
git pull
git merge --no-ff kk_lasers
git push lasers

Multiple developers can be following this flow at once if they’re all working on the same feature.

Occasionally, if there are changes in develop that the feature branch needs, we merge them in.

git checkout develop
git pull
git checkout lasers
git merge develop
git push develop

Finally, when the feature branch is all finished, we merge it back into develop.

git checkout develop
git pull
git merge --no-ff lasers
git push develop

This step doesn’t require a pull request, since all new code was already reviewed before it got into the lasers branch.

Benefits

This process handles the four main use case we run into:

  1. Small changes
  2. Large changes that can be locked behind a feature branch
  3. Large changes that cannot
  4. Urgent hot fixes

We’ve found it satisfies most of our requirements as well:

  • It avoids large, high-risk deploys
  • It avoids constant, time-consuming deploys
  • It keeps the git log relatively clean- keeping merge commits where they convey useful information and omitting them when they don’t.

To be fair, this flow might not be appropriate for your team if your deploy process is heavyweight enough that you can’t deploy every day. It also might not scale without modification to a team larger than a few dozen devs.

For small teams with frequent deploys, though, you might want to consider it. It’s worked well for us.

--

--