6 best practices for teams using Git | Opensource.com

6 best practices for teams using Git

Work more effectively by using these Git collaboration strategies.

Women in tech boardroom

Subscribe now

Get the highlights in your inbox every week.

Git is very useful for helping small teams manage their software development processes, but there are ways you can make it even more effective. I've found a number of best practices that help my team, especially as new team members join with varying levels of Git expertise.

Formalize Git conventions for your team

Everyone should follow standard conventions for branch naming, tagging, and coding. Every organization has standards or best practices, and many recommendations are freely available on the internet. What's important is to pick a suitable convention early on and follow it as a team.

Also, different team members will have different levels of expertise with Git. You should create and maintain a basic set of instructions for performing common Git operations that follow the project's conventions.

Merge changes properly

Each team member should work on a separate feature branch. But even when separate branches are used, everyone eventually modifies some common files. When merging the changes back into the master branch, the merge typically will not be automatic. Human intervention may be needed to reconcile different changes made by two authors to the same file. This is where you have to learn to deal with Git merge techniques.

Modern editors have features to help with Git merge conflicts. They indicate various options for a merge in each part of a file, such as whether to keep your changes, the other branch's changes, or both. It may be time to pick a different code editor if yours doesn't support such capabilities.

Rebase your feature branch often

As you continue to develop your feature branch, rebase it against master often. This means executing the following steps regularly:

git checkout master
git pull
git checkout feature-xyz  # name of your hypothetical feature branch
git rebase master  # may need to fix merge conflicts in feature-xyz

These steps rewrite history in your feature branch (and that's not a bad thing). First, it makes your feature branch look like master with all the updates made to master up to that point. Then all your commits to the feature branch are replayed on top, so they appear sequentially in the Git log. You may get merge conflicts that you'll need to resolve along the way, which can be a challenge. However, this is the best point to deal with merge conflicts because it only impacts your feature branch.

After you fix any conflicts and perform regression testing, if you're ready to merge your feature back into master, do the above rebase steps one more time, then perform the merge:

git checkout master
git pull
git merge feature-xyz

In the interim, if someone else pushes changes to master that conflict with yours, the Git merge will have conflicts again. You'll need to resolve them and repeat the regression testing.

There are other merge philosophies (e.g., without rebasing and only using merge to avoid rewriting history), some of which may even be simpler to use. However, I've found the approach above to be a clean and reliable strategy. The commit history is stacked up as a meaningful sequence of features.

With "pure merge" strategies (without rebasing regularly, as suggested above), the history in the master branch will be interspersed with the commits from all the features being developed concurrently. Such a mixed-up history is harder to review. The exact commit times are usually not that important. It's better to have a history that's easier to review.

Squash commits before merging

When working on your feature branch, it's fine to add a commit for even minor changes. However, if every feature branch produced 50 commits, the resulting number of commits in the master branch could grow unnecessarily large as features are added. In general, there should only be one or a few commits added to master from each feature branch. To achieve this, squash multiple commits into one or a handful of commits with more elaborate messages for each one. This is typically done using a command such as:

git rebase -i HEAD~20  # look at up to 20 commits to consider squashing

When this is executed, an editor pops up with a list of commits that you can act upon in several ways, including pick or squash. Picking a commit means keeping that commit message. Squashing implies combining that commit's message into the previous commit. Using these and other options, you can combine commit messages into one and do some editing and cleanup. It's also an opportunity to get rid of the commit messages that aren't important (e.g., a commit message about fixing a typo).

In summary, keep all the actions associated with the commits, but combine and edit the associated message text for improved clarity before merging into master. Don't inadvertently drop a commit during the rebase process.

After performing such a rebase, I like to look at the git log one last time to make final edits:

git commit --amend

Finally, forcing an update to your remote feature branch is necessary, since the Git commit history for the branch has been rewritten:

git push -f

Use tags

After you have finished testing and are ready to deploy the software from the master branch, or if you want to preserve the current state as a significant milestone for any other reason, create a Git tag. While a branch accumulates a history of changes corresponding to commits, a tag is a snapshot of the branch's state at that instant. A tag can be thought of as a history-less branch or as a named pointer to a specific commit immediately before the tag was created.

Configuration control is about preserving the state of code at various milestones. Being able to reproduce software source code for any milestone so that it can be rebuilt when necessary is a requirement in most projects. A Git tag provides a unique identifier for such a code milestone. Tagging is straightforward:

git tag milestone-id -m "short message saying what this milestone is about"
git push --tags   # don't forget to explicitly push the tag to the remote

Consider a scenario where software corresponding to a given Git tag is distributed to a customer, and the customer reports an issue. While the code in the repository may continue to evolve, it's often necessary to go back to the state of the code corresponding to the Git tag to reproduce the customer issue precisely to create a bug fix. Sometimes newer code may have already fixed the issue but not always. Typically, you'd check out the specific tag and create a branch from that tag:

git checkout milestone-id        # checkout the tag that was distributed to the customer
git checkout -b new-branch-name  # create new branch to reproduce the bug

Beyond this, consider using annotated tags and signed tags if they may be beneficial to your project.

Make the software executable print the tag

In most embedded projects, the resulting binary file created from a software build has a fixed name. The Git tag corresponding to the software binary file cannot be inferred from its filename. It is useful to "embed the tag" into the software at build time to correlate any future issues precisely to a given build. Embedding the tag can be automated within the build process. Typically, the tag string git describe generates is inserted into the code before code compilation so that the resulting executable will print the tag string while booting up. When a customer reports an issue, they can be guided to send you a copy of the boot output.


Git is a sophisticated tool that takes time to master. Using these practices can help teams successfully collaborate using Git, regardless of their expertise level.

Hands programming

Make everyone think you write perfect code the first time (and make your patches easier to review and merge).
Person using a laptop

The Git Extras repo hosts more than 60 scripts that add to Git's basic functionality. Here's how to install, use, and contribute to it.


About the author

Ravi Chandran - Ravi is a software engineer in the UAV industry. He has worked on a variety of problems such as UAV sensor software integration, DevOps, digital signal processing and applying machine learning to problems. He's always on the lookout for new and interesting software tools for his work and hobbies. Some of his favorites topic areas include Python, C#, Unity and containers. He likes writing and sharing nuggets of what he has learned along the way with the open source community.