Git Advanced: Rebase, Cherry-Pick, Bisect, Submodules
Master Git advanced workflows: rebase, cherry-pick, bisect, and submodules.
20+ years shipping production systems from the metal up. Everything here is grounded in real deployments.
- ✓Basic Git knowledge: commit, push, pull, merge, branching.
- ✓Familiarity with command line interface.
- ✓Understanding of Git's object model (commits, trees, blobs) is helpful but not required.
- Rebase rewrites commit history by moving commits to a new base, creating a linear history.
- Cherry-pick applies specific commits from one branch to another without merging the whole branch.
- Bisect uses binary search to find the commit that introduced a bug.
- Submodules allow embedding one Git repository inside another as a dependency.
Think of Git commits like chapters in a book. Rebase is like rewriting the order of chapters to make the story flow better. Cherry-pick is like taking a single chapter from one book and inserting it into another. Bisect is like playing a guessing game where you split the book in half to find where a mistake was introduced. Submodules are like having a separate mini-book inside your main book that you can update independently.
You've mastered the basics of Git: commit, push, pull, merge. But as your projects grow, you'll encounter scenarios where simple merges create messy histories, or you need to selectively apply changes, or you're hunting down a bug that appeared weeks ago. This is where Git's advanced features come in. Rebase lets you craft a clean, linear history. Cherry-pick gives you surgical precision to apply only the commits you need. Bisect turns debugging into a binary search. Submodules help you manage dependencies without copying code. In this tutorial, you'll learn each of these tools through practical examples, see how they can go wrong, and discover best practices to avoid common pitfalls. By the end, you'll be able to maintain a pristine commit history, debug efficiently, and manage complex multi-repo projects with confidence.
1. Rebase: Rewriting History with Purpose
Rebase is one of Git's most powerful yet misunderstood features. Unlike merge, which creates a new commit that ties two branches together, rebase moves an entire branch to a new base commit, replaying each commit on top. This results in a linear history that is easier to read and bisect. However, rebasing rewrites history, which can cause problems if you've already shared your branch with others.
When to rebase: - To integrate changes from main into your feature branch before merging. - To clean up a series of commits before merging (using interactive rebase). - To maintain a linear project history.
When NOT to rebase: - On public branches that others have based work on. - On long-lived feature branches (prefer merge).
Let's see an example. Suppose you have a feature branch 'feature' that branched off 'main' at commit A. Meanwhile, main has moved forward with commits B and C. To rebase feature onto main:
``bash git checkout feature git rebase main ``
This will replay your feature commits on top of main's latest commit. If conflicts occur, Git pauses and lets you resolve them. After resolving, use git rebase --continue. To skip a commit, use git rebase --skip. To abort, use git rebase --abort.
Interactive rebase allows you to squash, reorder, or edit commits. For example, to squash the last three commits into one:
``bash git rebase -i HEAD~3 ``
This opens an editor where you can change 'pick' to 'squash' for the commits you want to combine.
Best practice: Always rebase onto a stable base. If you're rebasing onto a moving target (like a frequently updated main), you may need to rebase multiple times. Consider using git pull --rebase instead of git pull to keep your local commits on top of remote changes.
2. Cherry-Pick: Selective Commit Application
Cherry-pick allows you to apply a specific commit from one branch to another. This is useful when you need a bug fix that was committed on a different branch but you don't want to merge the entire branch. For example, if a hotfix was applied to main and you need it in your release branch, you can cherry-pick that commit.
Syntax: git cherry-pick <commit-hash>
You can cherry-pick multiple commits by listing them in order. If conflicts arise, resolve them and use git cherry-pick --continue. To abort, use git cherry-pick --abort.
Example: Suppose commit abc123 on main fixes a critical bug. To apply it to your release branch:
``bash git checkout release git cherry-pick abc123 ``
Important considerations: - Cherry-picking creates a new commit with a different hash, even if the changes are identical. - If the commit depends on previous commits that are not in the target branch, you may get conflicts or incomplete changes. - Cherry-picking can lead to duplicate commits if not tracked carefully.
Best practice: Use cherry-pick sparingly. If you find yourself cherry-picking many commits, consider merging the branch instead. Always test after cherry-picking, as the new commit may behave differently in a different context.
3. Bisect: Binary Search for Bugs
Git bisect is a powerful debugging tool that uses binary search to find the commit that introduced a bug. Instead of manually checking each commit, you tell Git a 'good' commit (where the bug didn't exist) and a 'bad' commit (where the bug exists). Git then checks out a commit halfway between them, and you test it. Based on your result, Git narrows down the range until it finds the first bad commit.
How to use: 1. Start bisect: git bisect start 2. Mark the current commit as bad: git bisect bad (or specify a commit) 3. Mark a known good commit: git bisect good <commit> 4. Git checks out a commit in the middle. Test it. 5. Mark it as good or bad: git bisect good or git bisect bad 6. Repeat until Git identifies the first bad commit. 7. Reset: git bisect reset
Automated bisect: You can automate testing with a script. For example, if you have a test script that returns 0 for success and non-zero for failure:
``bash git bisect start git bisect bad git bisect good v1.0 git bisect run ./test-script.sh ``
This will automatically run the script on each commit and mark accordingly.
Best practice: Ensure your test is reliable and fast. Bisect works best with a linear history; merge commits can complicate things. Use git bisect skip if a commit cannot be tested (e.g., build failure).
4. Submodules: Managing Dependencies
Submodules allow you to include one Git repository inside another as a subdirectory. This is useful when your project depends on an external library that you want to track at a specific version. Unlike copying the code, submodules maintain a link to the external repo, so you can update it independently.
Adding a submodule: ``bash git submodule add https://github.com/example/lib.git lib ` This clones the repo into the 'lib' directory and creates a .gitmodules` file.
Cloning a project with submodules: ``bash git clone --recurse-submodules <repo-url> ` If you already cloned without submodules, run: `bash git submodule update --init --recursive ``
Updating submodules: ``bash git submodule update --remote `` This pulls the latest commit from the submodule's remote. Then you need to commit the change in the parent repo to record the new submodule commit.
Common pitfalls: - Submodules are often left at a detached HEAD state. Always checkout a branch inside the submodule before making changes. - If you forget to push submodule changes, others will get a broken reference. - Submodules can be confusing for new contributors.
Best practice: Use submodules for stable dependencies that you don't frequently modify. For actively developed dependencies, consider using a package manager instead.
5. Interactive Rebase: Squashing and Rewording
Interactive rebase is a superpower for cleaning up commit history before merging. You can squash multiple commits into one, reword commit messages, reorder commits, or even drop commits entirely. This is especially useful for feature branches where you have many 'WIP' commits that you want to combine into a logical set.
Starting interactive rebase: ``bash git rebase -i HEAD~n ` where n is the number of commits to go back. Alternatively, you can rebase onto a specific commit: git rebase -i <commit-hash>`.
Common operations: - pick - use the commit as is. - reword - change the commit message. - squash - combine the commit with the previous one, merging messages. - fixup - like squash but discard the commit message. - drop - remove the commit.
Example: Suppose you have three commits: 'WIP', 'Add feature', 'Fix typo'. You want to combine them into one commit 'Add feature'.
``bash git rebase -i HEAD~3 `` In the editor, change the second and third lines from 'pick' to 'squash' or 'fixup'. Save and exit. Then edit the final commit message.
Best practice: Squash commits that are not meaningful individually. Keep the history clean but still logical for code review. Avoid squashing commits that have been reviewed separately.
6. Rebase vs Merge: Choosing the Right Strategy
One of the most debated topics in Git workflows is whether to rebase or merge. Both integrate changes from one branch into another, but they do so differently.
Merge: Creates a new commit that ties together the histories of both branches. It preserves the exact timeline of when commits were made. This is non-destructive and safe for shared branches.
Rebase: Rewrites the commit history of the feature branch to appear as if it was developed on top of the latest main. This results in a linear history but changes commit hashes.
When to use merge: - For public or shared branches. - When you want to preserve the context of when changes were made. - For long-lived feature branches.
When to use rebase: - For local, unpublished branches. - To maintain a clean, linear project history. - Before merging a short-lived feature branch into main.
Best practice: Many teams adopt a 'rebase before merge' policy: rebase your feature branch onto main to keep history linear, then merge (using --no-ff to create a merge commit for visibility). This combines the benefits of both.
Example workflow: ``bash git checkout feature git rebase main # Resolve conflicts if any git checkout main git merge --no-ff feature ``
The Rebase That Broke the Build
- Rebase is not a silver bullet; prefer merge for long-lived branches.
- Always rebase onto a stable base (e.g., main) frequently to avoid massive conflict resolution.
- Use interactive rebase to squash commits before rebasing to reduce conflict surface.
- After rebasing, force-push only if you are the sole contributor to the branch.
- Consider using 'git rerere' to remember conflict resolutions.
git rebase --abortgit merge <branch>| File | Command / Code | Purpose |
|---|---|---|
| rebase-example.sh | git checkout feature | 1. Rebase |
| cherry-pick-example.sh | git log --oneline main | 2. Cherry-Pick |
| bisect-example.sh | git bisect start | 3. Bisect |
| submodule-example.sh | git submodule add https://github.com/example/lib.git lib | 4. Submodules |
| interactive-rebase.sh | git rebase -i HEAD~3 | 5. Interactive Rebase |
| rebase-vs-merge.sh | git checkout main | 6. Rebase vs Merge |
Key takeaways
Common mistakes to avoid
3 patternsRebasing a public branch that others have based work on.
Cherry-picking a commit without checking its dependencies.
Forgetting to push submodule changes after updating them.
Interview Questions on This Topic
Explain the difference between rebase and merge. When would you use each?
Frequently Asked Questions
20+ years shipping production systems from the metal up. Everything here is grounded in real deployments.
That's Software Engineering. Mark it forged?
5 min read · try the examples if you haven't