Introduction
Git has become ubiquitous in the world of software development. Whenever I think of Git, I still think of the old xkcd comic. It's funny because it's true. Most of us just memorize a set of Git commands, and if things get too crazy, we just re-clone the repo, copy our latest changes over, and try again.
This post will not only outline the set of Git commands I use the most, it will also cover a Gitflow branching and merging strategy that I've seen work in the real world (with up to 6 developers simultaneously working on the same repo).
If you're a solo developer on a project, feel free to branch and merge however you'd like. If you work with a team of developers, then Gitflow could possibly improve your quality of life. Gitflow obviously isn't for everyone, but for small teams with high velocities, I've seen it work quite well.
DevOps
Over the years different processes have been invented to help development teams improve their operations. This is where terms like "DevOps" and "CI/CD" got their origins. DevOps Engineers strive to help software development teams move fast and not break things. Or, to misquote Einstein, "everything should be made as fast as possible, but not faster".
What's at the heart of many of these DevOps CI/CD pipelines? That's right, Git!
- Pull/Merge requests.
- Commits.
- Merges.
These are the three most common CI/CD pipeline triggers I've seen in my career. All three are done with Git.
Git
Since Git is so important in the world of software development, let's take a few minutes to review some of the basics before we get into the real-world application of Gitflow.
This blog tries to use inclusive language, so when I refer to the main branch, I'm referring to what others might call the master branch.
One of the primary rules of any successful branching strategy is that the main branch should always be in a "shippable state". This means that the code at the head of the main branch should be able to be built and deployed at a moment's notice.
As a solo developer working on a hobby project, you might never branch from main. In this case, you should hold off committing your changes to the main branch until they've passed some kind of local test suite. By not committing untested changes, your main branch remains shippable even though no other branches ever get created.
Now instead of a solo developer, let's think about projects with small teams of two or more developers.
To get a step closer to the Gitflow process, the team should try to avoid ever committing directly to the main branch. Many teams implement branch protection rules that make it impossible to commit directly to the main branch (branch protection rules can be applied to any branch, not just the main branch).
Once direct commit access to the main branch has been shut off, the only option is to branch from main and then create a pull/merge request back to main. Sounds simple enough on the surface, but it's important to stay alert because avoidable merge conflicts can be extremely common.
What do I mean by avoidable merge conflicts? These are merge conflicts that arise because a developer didn't follow the proper process or "flow". Let's look at a common Git workflow - assume the Git repository is being hosted on GitHub and had been previously cloned to our machine.
# Workflow 1.
# Repo previously cloned from GitHub.
# Select the main branch.
git checkout main
# Create a branch off of main.
git checkout -b feature-branch-a
# Code code code.
# Add changes to feature-branch-a.
git add .
# Commit changes to feature-branch-a.
git commit -m "Feature A is ready to go."
# Push the branch to GitHub.
git push origin feature-branch-a
# Use GitHub to create a pull request.
In the GitHub UI, when you create a pull request from feature-branch-a to the main branch you get a merge conflict. Why? How could this have been avoided? Take a moment to think about what I should have done differently.
Here is a slightly extended Git workflow, but one that will dramatically reduce the number of merge conflicts.
# Workflow 2.
# Repo previously cloned from GitHub.
# Select the main branch.
git checkout main
# Update our local copy of main.
git pull
# Create a branch off of main.
git checkout -b feature-branch-a
# Code code code.
# Add changes to feature-branch-a.
git add .
# Commit changes to feature-branch-a.
git commit -m "Feature A is ready to go."
# Check if anything has been merged to main.
# Select the main branch.
git checkout main
# Check for updates to main.
git pull
# Select the feature branch.
git checkout feature-branch-a
# If changes were found in main,
# merge in the latest changes.
git merge main
# If new changes were merged in,
# re-test your code before creating a PR.
# Push the branch to GitHub.
git push origin feature-branch-a
# Use GitHub to create a pull request.
It's important to remember that Git is a distributed source code management system. There is no central repository from Git's perspective. What ends up happening is the copy of the Git repo hosted on a platform like GitHub, GitLab, or BitBucket will "act" as a central repository, because that's where the team does all of its branching and merging.
As soon as you clone a Git repository from a remote source like GitHub onto your local machine, your local machine's copy of the repo is free to diverge. And diverge it will. This is the nature of a distributed source code management system.
The important take away here is that when you "branch from main" on your local machine, you're branching from your machine's local copy of main - which may or may not be fully up-to-date with the copy of main that's on GitHub.
This is why the extra git pull commands are so important. If you create new branches from stale out-of-date branches, you're gonna have a bad time. If your feature branches tend to stick around for more than a few days, then I'd encourage you to merge in updates from the upstream branch at least once every other day.
Gitflow
If you're comfortable with the list of Git commands in the "Workflow 2" example above, then you're already comfortable with the Gitflow process. In practice, Gitflow just takes the list of Git commands from Workflow 2 and introduces a few new layers for non-production environments.
It's common for code merged to the main branch to get deployed to a production or staging environment. Team's can also introduce lower non-production environments like "dev" or "qa" (or the already mentioned "staging"), and setup corresponding CI/CD pipelines to deploy code to the lower environments.
Common branch -> environment mappings I've seen are:
- dev -> dev
- qa -> qa
- staging -> staging
- prod -> prod
Let's put it all together and track the full journey of a new code change, from initial development all the way to production.
# Gitflow.
# Repo previously cloned from GitHub.
# Branches: dev, qa, staging, prod
# Environments: dev, qa, staging, prod
# Start from the lowest branch (dev).
# Select the dev branch.
git checkout dev
# Update our local copy of dev.
git pull
# Create a branch off of dev.
git checkout -b feature-branch-a
# Code code code.
# Add changes to feature-branch-a.
git add .
# Commit changes to feature-branch-a.
git commit -m "Feature A is ready to go."
# Check if anything has been merged to dev.
# Select the dev branch.
git checkout dev
# Check for updates to dev.
git pull
# Select the feature branch.
git checkout feature-branch-a
# If changes were found in dev,
# merge in the latest changes.
git merge dev
# If new changes were merged in,
# re-test your code before creating a PR.
# Push the branch to GitHub.
git push origin feature-branch-a
# Use GitHub to create a pull request.
# PR: feature-branch-a -> dev.
# Merge feature-branch-a into dev.
# CI/CD pipeline deploys code to dev.
# Delete feature-branch-a from GitHub.
# Verify Feature A in dev.
# Use GitHub to create a pull request.
# PR: dev -> qa.
# Merge dev into qa.
# CI/CD pipeline deploys code to qa.
# Verify Feature A in QA.
# Use GitHub to create a pull request.
# PR: qa -> staging.
# Merge qa into staging.
# CI/CD pipeline deploys code to staging.
# Verify Feature A in staging.
# Use GitHub to create a pull request.
# PR: staging -> prod.
# Merge staging into prod.
# CI/CD pipeline deploys code to production.
# Verify Feature A in production.
# Repeat this process for every (non-hotfix) code change.
Hotfix
The Gitflow process listed above is for standard code/feature development. In cases where a P0 (highly critical) bug is found in production, a separate "hotfix" workflow is often used.
Hotfix workflows can be tricky because code changes can sit in a lower non-production environment like qa or staging for several days or weeks before they're officially deployed to prod. Ideally the code on the staging branch should match the prod branch, but in reality the two branches sometimes diverge.
Step 1 in any hotfix workflow is identifying what version of the code is currently on production. In theory that code should also be on the staging branch, so let's start there (if the staging branch is ahead of the prod branch then begin by branching from prod instead of staging - otherwise unwanted changes might get included in the hotfix when it gets deployed to production).
# Sample hotfix flow.
# Repo previously cloned from GitHub.
# Branches: dev, qa, staging, prod
# Environments: dev, qa, staging, prod
# staging and prod branches are in-sync.
# and it ain't no lie, baby bye bye bye.
# Branch from staging instead of dev.
# Select the staging branch.
git checkout staging
# Update our local copy of staging.
git pull
# Create a branch off of staging.
git checkout -b hotfix-branch-a
# Code code code.
# Add changes to hotfix-branch-a.
git add .
# Commit changes to hotfix-branch-a.
git commit -m "Hotfix A is ready to go."
# Check if anything has been merged to staging.
# Select the staging branch.
git checkout staging
# Check for updates to staging.
git pull
# Select the feature branch.
git checkout hotfix-branch-a
# If changes were found in staging,
# merge in the latest changes.
git merge staging
# If new changes were merged in,
# re-test your code before creating a PR.
# Push the branch to GitHub.
git push origin hotfix-branch-a
# Use GitHub to create a pull request.
# PR: hotfix-branch-a -> staging.
# Merge hotfix-branch-a into staging.
# CI/CD pipeline deploys code to staging.
# Verify Hotfix A in staging.
# Use GitHub to create a pull request.
# PR: staging -> prod.
# Merge staging into prod.
# CI/CD pipeline deploys code to production.
# Verify Hotfix A in production.
# Backport the hotfix to the dev branch.
# Select the dev branch.
git checkout dev
# Update our local copy of dev.
git pull
# Create a new branch from dev
git checkout -b backport-hotfix-a
# Merge in the hotfix.
# This will likely result in merge conflicts
# that will need to be resolved manually.
git merge hotfix-branch-a
# Resolve any merge conflicts.
# Add changes to backport-hotfix-a.
git add .
# Commit changes to backport-hotfix-a.
git commit -m "Backported Hotfix A to dev."
# Push the branch to GitHub.
git push origin backport-hotfix-a
# Use GitHub to create a pull request.
# PR: backport-hotfix-a -> dev.
# Merge backport-hotfix-a into dev.
# CI/CD pipeline deploys code to dev.
# Delete backport-hotfix-a from GitHub.
# Verify Backported Hotfix A in dev.
# Use GitHub to create a pull request.
# PR: dev -> qa.
# Merge dev into qa.
# CI/CD pipeline deploys code to qa.
# Verify Backported Hotfix A in QA.
# The hotfix has now been verified in all
# environments: dev, qa, staging, and prod.
# Repeat this process for every hotfix.
I've heard critics of this workflow complain that it's too "tedious and time consuming". To that I go back to my previous Einstein misquote: "everything should be made as fast as possible, but not faster".
Whenever corners are cut and shortcuts are taken, bugs, broken CI/CD pipelines, merge conflicts, and failed tests are sure to follow. Trust and follow the process and your pipeline status page will be a sea of green checkmarks.
Conclusion
This post outlines Git workflows I've seen work in the real world. My hope is that someone finds this information useful and it helps them create more reliable software deployments with fewer merge conflicts and broken pipelines.
If you would like to share your own opinions or personal anecdotes, you're invited to post your comments below.
📎 Click here to download a PDF copy of this post.
Comments
There are no public comments at this time.