Mastering Your GitLab Flow: The Power of Advanced Git Rebase
xanderekpl
In modern software development, a clean and logical Git history is not just a nicety; it’s a cornerstone of maintainability, efficient debugging, and team collaboration. While the GitLab Flow provides a simple and effective branching strategy, the real artistry lies in how we manage our feature branches before they merge. This is where git rebase
transcends its basic usage and becomes a powerful tool for crafting a narrative history.
This article dives deep into advanced git rebase
workflows, focusing on managing complex feature branches and stacked branches to elevate your team’s efficiency and codebase clarity.
Why Rebase? The Quest for a Linear History
By default, merging a feature branch in GitLab often creates a merge commit. This is safe and preserves the exact history, warts and all. However, it can lead to a “graph” history that’s difficult to follow.
* e3b8e71 (HEAD -> main) Merge branch 'feature/user-auth'
|\
| * 6a42d3c (feature/user-auth) Add token validation
| * 1b3d5a0 Add password hashing
| * a2c4f67 WIP login endpoint
* | c8a1b7e (origin/main) Update CI configuration
* | 2f9d8c0 Refactor database service
|/
* 0e5f7b1 Initial project setup
A rebased history, by contrast, reapplies your feature branch commits on top of the latest state of the target branch, creating a perfectly linear and sequential story.
* 9f2c1a3 (HEAD -> main) Add token validation
* 5e8b4d7 Add password hashing
* 3a1b9c2 Implement login endpoint
* c8a1b7e (origin/main) Update CI configuration
* 2f9d8c0 Refactor database service
* 0e5f7b1 Initial project setup
The benefits are immediate:
- Clarity: The history reads like a logical sequence of changes.
git bisect
: A linear history makes identifying regressions withgit bisect
trivial.- Atomic Commits: It encourages developers to clean up their work into small, atomic commits, each representing a single logical change.
The Interactive Rebase Workbench: Beyond the Basics
Your primary tool is interactive rebase: git rebase -i <base>
. While pick
, reword
, and squash
are well-known, let’s focus on two powerful, underutilized options: fixup
and autosquash
.
fixup
and autosquash
: The Ultimate Cleanup Crew
Often, you make a commit and then realize you missed something small—a typo, a forgotten file. Instead of creating a new commit “Fix typo” and then manually squashing it later, you can automate this.
-
Make your initial commit:
git commit -m "feat: Implement user authentication service" # Commit SHA: a1b2c3d
-
Realize you forgot to update a comment. Make the change and then commit with the
--fixup
flag, pointing to the commit you want to amend.git commit --fixup a1b2c3d
This creates a commit with the message
fixup! feat: Implement user authentication service
. -
When you’re ready to clean up your branch, run an interactive rebase with the
--autosquash
flag.# Rebase against the main branch git rebase -i --autosquash origin/main
Git will automatically open your editor with the fixup
commit placed correctly and marked for fixup
, requiring no manual reordering.
# Editor view during rebase
pick a1b2c3d feat: Implement user authentication service
fixup 4e5f6a7 fixup! feat: Implement user authentication service
pick b4c5d6e feat: Add JWT token generation
This workflow keeps your development process fluid while ensuring the final history is pristine, without the noise of corrective commits.
Managing Long-Lived Feature Branches
When a feature branch is active for days, main
will inevitably move forward. To keep your branch up-to-date and prevent a monstrous merge conflict later, you should rebase periodically.
# Get the latest changes from the remote
git fetch origin
# Rebase your feature branch on top of the latest main
git rebase origin/main
# You've rewritten your branch's history, so you must force-push.
# Use --force-with-lease as a safeguard against overwriting work.
git push --force-with-lease
Why --force-with-lease
?
This is a safer alternative to git push --force
. It will only force the push if your local copy of the remote branch is up-to-date with the actual remote branch. This prevents you from accidentally overwriting commits that a colleague might have pushed to the same feature branch. For a senior developer, this should be the default.
The Art of Stacked Branches with rebase --onto
For large, epic-sized features, breaking the work into a “stack” of dependent branches is a highly effective strategy. Each branch builds on the previous one, allowing for smaller, independent Merge Requests.
Imagine this stack:
main
← feature/api-layer
← feature/service-layer
← feature/ui-component
The problem arises when you need to make a change to a base branch, like feature/api-layer
, after the other branches have been created. Or, more commonly, when you need to rebase the entire stack onto a new main
. Merging or rebasing naively will create a tangled mess.
This is where the powerful git rebase --onto
command shines. It allows you to surgically move a set of commits from one base to another.
Scenario: The main
branch has new commits, and you need to update your entire stack.
Initial State:
* --- C (feature/ui-component)
|
* --- B (feature/service-layer)
|
* --- A (feature/api-layer)
|
* --- M2 (main)
|
* --- M1
Step 1: Rebase the first branch (feature/api-layer
) onto main
.
This is a standard rebase.
git checkout feature/api-layer
git rebase main
This creates a new commit, A'
. The old A
is now a dangling commit.
New State:
* --- A' (feature/api-layer)
|
| * --- C (feature/ui-component)
| |
| * --- B (feature/service-layer)
|/
| * --- A (OLD, dangling)
|/
* --- M3 (main)
|
* --- M2
|
* --- M1
Step 2: Rebase the second branch (feature/service-layer
) onto the new feature/api-layer
.
Here’s the magic. We want to take the commits that were on feature/service-layer
but not on the old feature/api-layer
and re-apply them onto the new feature/api-layer
.
The syntax is: git rebase --onto <new_base> <old_base> <branch_to_move>
git rebase --onto feature/api-layer feature/api-layer@{1} feature/service-layer
--onto feature/api-layer
: The new parent for our branch.feature/api-layer@{1}
: Areflog
entry pointing to wherefeature/api-layer
was before we rebased it (the<old_base>
). You could also use the old commit SHA.feature/service-layer
: The branch whose commits we want to move.
Step 3: Repeat for the entire stack.
git rebase --onto feature/service-layer feature/service-layer@{1} feature/ui-component
This process cleanly transplants each branch in the stack onto its updated parent, preserving the dependencies and commit history within each branch. While it can be scripted for very deep stacks, understanding the manual process is key.
Integrating with GitLab Merge Requests
- Stacked MRs: Create a separate Merge Request (MR) for each branch in your stack. Set the target branch appropriately (e.g., MR for
feature/service-layer
targetsfeature/api-layer
). - Review Process: Review and merge the base MR first (
feature/api-layer
intomain
). - Updating MRs: After the base is merged, you perform a final rebase of the next branch (
feature/service-layer
) ontomain
, update its MR to targetmain
, and merge it. This “domino” or “cascade” merging process keeps themain
branch history perfectly linear.
Conclusion
git rebase
is more than just a tool for avoiding merge commits; it’s a workflow for professional software craftsmanship. By mastering interactive rebasing with autosquash
, managing branch updates with --force-with-lease
, and surgically restructuring stacked branches with --onto
, you can transform a chaotic development history into a clean, compelling, and highly maintainable narrative. Adopting these advanced techniques within your GitLab Flow will not only improve your codebase but also elevate your entire team’s engineering discipline.