Git is a powerful tool. It’s designed to enable multiple devs working on the same codebase at the same time. From big tech companies to scrappy startup environments, I’ve seen Git used in many ways with their own respective tradeoffs.

Being on a small team that has to do everything to run a startup (full stack development, on-call 24/7, quality assurance, program management, and product design), I’ve found a particular method of using Git that enables high-integrity program management, fast product iteration, and quickly root causing outages.

In short:

  • Enforce a linear history on the master branch. In other words, no merge commits because they create a tree history, which makes the git log difficult to read.
  • Devs write code on feature branches cut from master and Squash & Merge that feature branch into master.
  • A task is marked “Done” when its respective code is committed to master.
  • Development releases are tagged from commits on master. For end-to-end testing a feature branch, communicate that the service has code deployed that isn’t on master.
  • Production releases are tagged from release branches that are cut from master.

Why enforce a linear history?

A linear history maintains the git log as a descending timeline of events, where each commit is a new event marking a change in the codebase. A timeline is the simplest representation of what’s been happening in your codebase.

This enables your team to:

  • Easily know what the most recent changes are to the codebase because git log is by default a descending timeline of events.
  • Work directly on top of the commit history using git rebase, since each commit is appended to the end of the timeline. This helps with multiple devs editing the same file because the most recent changes to the file at the time of committing to master is at the end of the history.
  • Identify all the changes in the codebase between the commit of the previous healthy production release and the commit of the current faulty production release. A linear history enables the tool git bisect to effectively triage outages by identifying the commit that introduced a bug using binary search.
  • Effectively retrospect sprints by looking at all the commits within the sprint’s time window. This enables high-integrity project management so that whatever is landed on master in that time window can be matched up with your team’s sprint goals.

In a collaborative environment, the only alternative to a linear history would be a tree history (ie. merge commits), which is an on-call nightmare. Merge commits are effectively a child node of two parent nodes, where a node is a commit. By default, there is no squashing when doing a merge.

Therefore, it’s possible for old commits in a feature branch to sneak into the master branch when merged, meaning that commit can be buried deep into the git log by more recent commits that were merged into master. And then if a production release is tagged on top of that old commit, and that old commit contains a bug, we cannot use git bisect to identify the buggy commit because it could have snuck into the history before the previous healthy production release commit.

Sneaky commit in a merge commit
Here’s an example of an old commit sneaking into the history by being merged into master via merge commit. After the merge, the commit is now “behind” the previous healthy prod release commit. This breaks the timeline invariant of a linear history, and therefore git bisect is now not useful.

Regarding project management, if master has a tree history, we no longer can use the git log to figure out what was committed within the sprint’s time window. Because, put simply, the commits end up being all over the place and do not match up with the moment when a dev merged their changes into master.

Finally, when creating release notes for your customers to see what’s in the new version of your product, navigating a tree history is much more difficult than navigating a linear history to figure out what changed in between tagged releases.

A note on tagging releases

I’ve seen teams manually log in a public Slack channel the commit SHAs that were deployed to development or production environments for each service. While this is a great practice, it’s so easy to forget to do this. Especially when you’re woken up at 2am by an alert and you swiftly release a hot fix and head right back to bed.

A piece of infrastructure worth investing in is automatically logging what SHA has been deployed to whichever environment. It’s very important to have an accurate source of truth for what has been released in the development and production environments. This is to enable correct attribution of any bugs or outages that may occur. I’ve personally experienced taking way too long to triage an outage because of not having accurate critical pieces of information.

Push notification for deploys
Here’s an example of a tool we built that pings our Discord channel for prod releases. It contains all relevant information for when the deploy happened and what commit was deployed.

In summary

Git is a tool that enables collaboration on a codebase. Properly setting up an engineering process that benefits not only writing code but also testing, deploying, and monitoring applications can truly save tons of time. It’s a form of being resource efficient which ultimately enables the team to move faster.

And as a startup, it helps us sustainably go through that positive feedback cycle for iterating on our MVPs. We can trust this process to accurately tell us both what and when a commit was deployed. Therefore, we feel confident enough to quickly land and deploy changes to our web apps and backend services, because we have a process that can help us get out of bad situations. And we quickly skim through the git log to understand what’s changed in our codebase so we can effectively build on top of our teammates work.