Git things right🟢✔️ using rebase workflow in a software team
Tags: git, webdev, opensource, programming
Published: 2023-08-21 11:19:24

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. <small>By the way, it is super easy</small>.

To breakdown matters into chunks you can digest, let's assume of a company called <i>"XYZ"</i>, that has a wonderful software product 😎️.<br />
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 <em>feature</em>/<em>topic branch</em>.

> **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 <i>"XYZ"</i> company .<br />
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.<br />
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.<br />
**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6yolbtaxol9x17pw50m.jpg) -->

![Fork-shape commit history](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5tfoudyg1ysja5d3yxvo.png)

## 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:

```zsh
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
```

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.<br />
The branch's history tree looks like this:

![Git merge non-linear hist shape](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bqb598dpk0wshyfzyv3d.png)

> **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.<br />
Aside from this company's preferences, the following are the **cons of using a Git merge workflow**:

<!-- ![Tricky merge history](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y1it10khk1znken6un5v.png) -->

-   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`.

<!-- ### Pros

-   It is simple with few steps.
-   It is non-destructive. What is already there is not changed. It just adds new commits on top, hence easy to undo. -->

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

![Linear hist shape](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ma4w1nof7ozzaba66j7q.png)

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.<br />
For instance, when `feature` branch was first created:

```zsh
git checkout develop
git checkout -b feature # Create feature branch and switch to it
```

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.<br />
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmd99gpdkit9yyca9xgs.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oibs8iptoujtm9voedpl.png)

<!-- ![Feature branch history shape after rebase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/964tr3mxlq7dpqhd4v5z.png) -->
<!-- ![Feature branch history shape after rebase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m7k3l1x8d4tbwatdrk9e.png) -->
<!-- ![Feature branch history after rebase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ldlyzfd5obw4meu33wg.png) -->

> **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:

```zsh
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
```

To reiterate, the 2<sup>nd</sup> 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.<br />
So `develop` branch and `feature` branch are pointing to the same commit.<br />
The result is a linear history same for both branches. The project history is as illustrated:

![Git rebase workflow linear history](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hfnjzpfup5y4v1q4fdrg.png)

<!-- ![Git rebase workflow linear history](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3t5hmt7qv70q9b90l9kr.png) -->
<!-- ![Git rebase worflow hist shape](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ma4w1nof7ozzaba66j7q.png) -->

Alternatively, at the 2<sup>nd</sup> 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. <small>But anyway, Git will warn if you try to push a rewritten history.</small>

## 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.
1.  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.<br />
Follow me here on Dev so you can be first to know of new nuggets that drop.
{% user smitterhane %}

I have an [**X**(formerly twitter)](https://twitter.com/intent/user?screen_name=smitterhane) account. We can connect 🤝.<br />
You also need to hear this🙂: I maintain an open source library. See it in [Github](https://github.com/hane-smitter/react-loading-indicator). I appreciate your star⭐ should you like the [work(DEMO)](https://react-loading-indicators.netlify.app). I use a rebase workflow with it. Finally, explore my creativity practiced on [my personal website](https://lookupzach.netlify.app).