Skip to content

Git Strategies for Working on Teams

Background

Every team I’ve worked on has had different expectations and rules around git. Some had strict guidelines that created developer experience issues. Others had rules that were so loose that there was no consistency on the team.

These are my thoughts on what I’ve discovered to be a healthy balance between the strict and loose rules on teams. I hope to make suggestions for your team, and process that don’t sound dogmatic, and eventually help.

Always Branch and Pull Request to Main

This may seem like a no brainer to some, but no one should ever be interacting on your repo’s main branch directly. All changes to upstream main should be handled via pull requests (PR) with the exception of the initial repository commit. This will make all the suggestions throughout the article function properly. But it should also make it so your main branch will remain healthy assuming you have CI/CD working on PR. It also enables peer review on work which is a good habit. I still PR on my personal projects to make so I understand a set of changes in context as well.

Merges to main via Squash and Merge

All git hosting services have a “Squash and Merge” button on PRs which is an alias for:

git checkout main
git merge --squash <branch>
git commit

This takes all the commits on the specified branch and reduces them into a single atomic commit that gets inserted into the main branch. This is great for a few reasons:

  1. Keeps your git history clean
    1. omits merge commits
    2. keeps merge historically fully sequential
  2. Creates a single commit describing a bulk of changes but still makes the history of those changes available via a link to the original PR for full context
  3. Helps using bisect strategies to identify when a change was introduced, and reference the PR in which that change was introduced

The following is an example output of the squash and merge strategy:

git history example with squash merge

Individual Branching

I have two rules I suggest for individual branches:

  1. conform to some naming convention the team sets
  2. use a non-destructive strategy for commits and updates once a review has occurred

Rule 1 exists for a variety of reasons but primarily to avoid naming collisions. Rule 2, on the other hand, exists for a reviewer’s sake. All net new changes should be done via commits once a review has be initiated. Otherwise, it’s hard for a reviewer to track what they’ve already looked at versus what’s changed. Be nice to your reviewers. Once that PR is open, you may not use --amend or rebase on existing commits moving forward.

Otherwise, how you keep your branch up-to-date with the main branch is entirely your choice. Merge is typically the easiest if there are conflicts, especially when trying to resolve, and is usually best for novice developers. Rebase is great if you don’t have any merge conflicts or merge commits, and lets you get all the latest changes in the main brach. At the end of the day, do what is best for you.

The squash and merge strategy makes it so your branch can be whatever you want it to be. Do you commit every 5 minutes? Fine! Have 15 merge commits from main? That’s also fine! All these actions tell a story but squash and merge to main keeps that history on your branch, and PR and does not impact upstream main.

Long Running Epic Branches

All my rules above are great for single changes applied directly to main. However, some teams work on long running epic branches to avoid introducing changes to main that are not ready for release. The best way to avoid long running epic branches is feature flags, but some teams aren’t able to use these for one reason or another.

That’s ok! For this situation, I recommend the following:

  1. Create a base integration branch that is kept up with main using the merge strategy
  2. Create individual branches off the integration branch and follow my instructions on individual branching from above
  3. Squash merge branches back into the integration
  4. Squash merge the integration back into main when ready

I’ve been on teams that try to do fancy rebase --onto strategies with the integration branch, and then the following individual branches. At the end of the day, this creates a lot of unnecessary git work for teams, and wastes time that is better spent working on features, bugs, and tech debt. This is because branches have to be updated in a particular way, and sometimes the conflicts that arise lead to lost changes and duplicated effort and work. Simplify your process and simplify your teams’ lives.

Conclusion

This only covers a few of the most common situations, but these general rules should help for teams and individuals in the way that is best for them while keeping the upstream main branch healthy and easier to debug when critical issues are identified.

I hope this helps you and your team on your next project and allows more time for technical debt and other important work that may have been lost to git related issues.