DEV Community

Cover image for Git things right🟢✔️ using rebase workflow in a software team
Smitter
Smitter

Posted on

Git things right🟢✔️ using rebase workflow in a software team

Git has many powers and one of them is flexibility. Different approaches(workflows) can be leveraged to efficiently build software collaboratively, and teams can pick their best fit. Common in a Git team, branching is a godsent technique used to build new features in isolation without impacting stability in main branch. Later when a feature is complete, it's branch is merged into main, and that branch becomes obsolete and perhaps deleted.

It is odd how people treat git rebase like a plague to avoid when working on public branches i.e, branches other team members have pulled down from remote into their devices/local repos. That is okay. They are people. Coming from this article you shouldn't be one of them but a git user who doesn't have to sweat over using git rebase again.
We'll discuss why Git rebase workflow in a Git team, is an exciting thing you are missing while still clutched to the Git merge workflow.

Note: To better relate with the rest of the article, you need to be at least familiar with creating branches in Git and merging them. By the way, it is super easy.

To breakdown matters into chunks you can digest, let's assume of a company called "XYZ", that has a wonderful software product 😎️.

This company uses a Gitflow workflow to build its software. Ideally, its branching strategy comprises of the following:

  • main - This is the name given to a main branch that holds production ready code. Or call it stable version.
  • develop - This is also a main branch that contains newly integrated features (that are not yet ready to be released to production). The unstable version.
  • release- Branch that helps prepare a new stable release. Usually stemmed off the develop branch, for testing, documentation generation, bugfixes and other related preparations. It must be merged back to both develop and main.
  • hotfix- Branch that fixes bug discovered in production release(main branch). It must be fixed. And after being fixed, it is merged back to both main and develop.
  • feature - Branch that developers work on when building new features that come up. Collectively reffered to as feature/topic branch.

Note: You could choose your own name conventions. Importantly, the branches should still hold their purpose and usecase.

The main and develop branches are considered main branches, i.e they are long-lived branches, while the rest are supporting branches that aid parallel development, usually short-lived.

For elaborating the Git rebase workflow, we will focus on activity done on develop branch.

Scenario

Let's narrow down the persona to you, a developer working in this "XYZ" company .

Several features have come up, that need to be built and integrated into the company's software product. And you have been assigned one of them. The rest is distributed to other developers.

So for example, you need to build a new feature in the software that allows a user to locate gas stations nearby.

To go about this, you start off with git checkout develop. Then perform a git pull to integrate latest changes that may be in the remote but not in you local develop branch. And you branch off from develop. The new branch is feature branch, where you work on your assigned task.

After you are done, you are ready to merge your changes back into develop branch.

Please note that at this time, other developers who finished working on their assigned features earlier than you, have already integrated their work into develop branch. So, develop branch has evolved and after another git pull from it, commit history in your local repository can be visualized into this:

Fork-shape commit history

Merging policy

This company has its own take and philosophies about their project's commit history.

  • develop branch commit history should be linear, replaying thoughtful commits that happened in feature branch incorporated into it. No squash merging of feature branch. To reason out: In real world features can be complex, so it is important to maintain feature commits to explain how we arrived at point B coming from point A.
  • main branch should have one commit per each release. Practically, a squash merge. Meaning it will have a diminished project history. A good commit message should tell the features incorporated at the time of that release.
  • release and hotfix - These branches add improvements to codebase in main branches. After improvements are made, they are squash merged into main and develop. Good commit message is important too.

The problem with Git merge workflow

Now that we know how this company desires commit history of its project to be organized, let's look at one way one could decide to integrate work.

Suppose you use git merge to incorporate work from one branch into another, you do the following steps:

git checkout develop # Check out public branch
git pull origin develop # Pull in latest commits from the remote copy
git merge feature # Incorporate work from feature branch
git push # Share your changes to the remote
Enter fullscreen mode Exit fullscreen mode

That is the Git merge workflow. And it is legendary at being easy to pickup, that it builds a striking adoption by most Git users.

The branch's history tree looks like this:

Git merge non-linear hist shape

Tip: Commit history tree can be observed on the left edge when you run git log --graph --oneline.

A merge commit is generated which combines two branch histories. The commit history in develop becomes non-linear. That is to say, the commit history is not straight since it breaks from an evolving main line and later rejoins. Already, a non-linear project history is not what the company wants on this branch.

Aside from this company's preferences, the following are the cons of using a Git merge workflow:

  • Git merge workflow does not enforce an individual developer to build, test and work out code differences while at the feature branch level before merging to main.
  • This workflow generates merge commits that can really clutter the commit history. A disaster class history can happen especially if you team with committers that are relatively new to Git or are using GUI that hides the actual results from them.
  • If you need to review work for whatever reason, it is very difficult to do so with a cluttered history filled with noise jamming the radar.
  • Merge commits created, can result in duplication of old commits because someone merged in a branch that branched off at a far away older commit. This maybe an old branch or the developer did not branch off at the right spot.
  • A project's history can aid in debugging, e.g when finding a commit that introduced a bug. Git merge workflow can confuse things with merge commits, e.g when using git bisect.

An ideal linear project history desired on the develop branch looks like this:

Linear hist shape

All commits from feature branch are placed on top of develop branch and in order they occured. Also, there is no extraneous merge commit.

Git rebase workflow for a linear commit history

git rebase can achieve the desired commit history like shown in the above image.

A gotcha about git rebase is that it does its work by rewriting history, which can cause chaos when performed on a public branch other developers have based their work on; like in our case, the develop branch.

To avoid rewriting history, we need to ensure that when work from a separate branch is incorporated into develop, it is fast-forwaded. That is to mean commits made on the separate branch, are appended on top of existing history of current branch. Thereby avoiding rewrite on existing history.

To make git perform a fast-forward when integrating work from separate branches, we have to prepare commit history between them, especially when they have both evolved.

For instance, when feature branch was first created:

git checkout develop
git checkout -b feature # Create feature branch and switch to it
Enter fullscreen mode Exit fullscreen mode

Commit history in feature started off on a base commit(c2) which is the latest commit on develop branch at that time. More work committed from the branch, evolved the history with new commits(c3...c5) on top of the base commit.

Now at this time, enough work has been done on feature branch and we are ready to integrate these changes back into develop. However, the starting base commit of the feature branch is no longer the latest commit/tip of the develop branch:

Feature branch history shape before rebase

In other words, develop branch has evolved with newer commits by other team members since the last time feature was created off from it.

Therefore if we need a fast-forward merge to be possible, we have to update the base of feature branch so it is anchored onto the latest commit(c7) that is the tip of develop branch. Then changes that were committed in feature are freshly committed onto the top of the updated base in the order they occured(c3'...c5'), hence the nomenclature and internal working of git rebase:

Feature branch history shape after rebase

Note: c3'...c5' are new commits with same changes like in c3...c5(before rebasing). Rebasing changes their commit IDs.

After we rebase feature branch, we head into develop branch and traffic lights should be "green 🟢" for us to integrate new work in which Git will fast-forward. This is so because the base commit in feature branch is reachable as most recent commit on current branch(develop).

Typically, these are the steps in a git rebase workflow in your local repository:

git checkout develop # Checkout public branch
git pull origin develop # Get and incorporate latest changes
git checkout -b feature # Create feature branch and switch to it
    # Work on the new feature, make commits, test... Until you are done!
git fetch origin # Refresh local repo store of remote copies
git rebase origin/develop # Rebase to update feature branch base with latest develop branch commits
git checkout develop # Head back to public branch
git pull origin develop # Integrate latest commits from remote copy
git rebase feature # Bring in commits of feature branch onto the top
git push # Share the changes to remote copy
Enter fullscreen mode Exit fullscreen mode

To reiterate, the 2nd last command: git rebase feature, does not rewrite history. Git checks and sees that history of the checked out branch(develop), forms the base/ancestor of feature branch. So to include work done in the separate branch, it just appends commits done in feature branch onto the top of develop's history. In git parlance, this is a fast-forward, where Git simplifies work and just moves the develop branch to point to the same commit the feature branch is pointing.

So develop branch and feature branch are pointing to the same commit.

The result is a linear history same for both branches. The project history is as illustrated:

Git rebase workflow linear history

Alternatively, at the 2nd last command, instead of git rebase feature, you could use git merge feature and still the result is a fast-forward; merge commit is not generated in a fast-forward. Also, you could use git merge feature --ff-only to merge two lines of work only if fast-forward is possible. This can help mitigate risk of rewriting history on public branch; when a step on the workflow is missed or out of order. But anyway, Git will warn if you try to push a rewritten history.

Should you use a Git rebase workflow?

The choice between git merge and git rebase workflows can seem so pointless 🤷‍♂️. Because, in a nutshell, they both introduce changes from one branch into another. At least, this is the case for a Git user solely concerned about the final work.

Beyond the surface, the choice between the two comes down to maintainance of a project's commit history. Decision on a project's history can be based on two philosophies:

  1. Commit history is a record of what actually transpired and should be preserved.
  2. Commit history is a publication and it should be presented in high quality.

If a project gravitates to keeping a record of what actually transpired in its doing(regardless of organization), Git merge workflow is better suited. On the other hand, if a project should maintain a chronological and accessible history to tell a coherent story of how a project evolved, Git rebase workflow should be the choice.
There are far more advantages to keeping a high quality history that is clean and linear, for example:

  • It helps understand the story of how a solution was implemented.
  • There is clear snapshots of development stages of a project that can be referenced or used as starting points in future.
  • Cherry-picks, rollbacks and other versioning intentions become easier when history is orderly.
  • It outlines the evolution of the project in an easy to follow manner.
  • It allows to make simple compares when regressions are detected.

Overally why a Git rebase workflow is better than Git merge workflow is: Git rebase workflow is an outright ambassador for a linear commit history. The way it does this is by avoiding a merge commit while replaying the commits of a feature branch onto the tip of main. Also, Git rebase workflow allows you to resolve conflicts from feature branch before merging to main.

Git rebase workflow can be ideal in open-source contributions where you rebase a feature branch and create a pull request.

Final words

Thanks for reading. I hope I cleared your doubts. And now you can make informed decision about history of your project.

Follow me here on Dev so you can be first to know of new nuggets that drop.

I have an X(formerly twitter) account. We can connect 🤝.

You also need to hear this🙂: I maintain an open source library. See it in Github. I appreciate your star⭐ should you like the work(DEMO). I use a rebase workflow with it. Finally, explore my creativity practiced on my personal website.

Top comments (0)