Using git rebase and squash is the secret to turning a chaotic commit history into a clean, understandable story. It’s all about taking a series of small, work-in-progress commits—you know the ones, like ‘WIP,’ ‘fix typo,’ and ‘another small update’—and bundling them into a single, well-described commit before you merge your branch.
Have you ever tried to figure out what a feature branch was really about, only to be met with a Git log that reads like someone’s chaotic diary? We’ve all been there. Seeing a dozen tiny commits for one simple change isn’t just messy; it’s a real drag on your team’s productivity. A cluttered history makes debugging a nightmare, complicates code reviews, and makes it nearly impossible to understand how a project has evolved.
This is the exact problem that git rebase and its squash command were made to solve. Think of them less as commands and more as storytelling tools for your code.
Let’s say you’re building a new feature. Over a couple of days, you might rack up 15 small commits. Before squashing, your history is a long, noisy list of minor tweaks that hides the bigger picture. After squashing, it’s one beautiful, clear commit: “Feat: Implement user authentication with OAuth2.”
This single change brings some huge wins:
git bisect incredibly effective. When every commit is a complete, self-contained piece of work, you can zero in on the exact commit that introduced a bug in minutes.As a bonus, this cleanliness makes it incredibly easy to generate meaningful changelogs, giving everyone a clear, documented timeline of progress.
In many development teams, a rebase-and-squash workflow is no longer just a nice-to-have; it’s the professional standard. In fact, analysis of repositories that enforce squashing commits on merge shows a 50-60% reduction in commit noise. This makes digging through a project’s history to understand old code (a process sometimes called “code archaeology”) far more efficient.
This isn’t about being picky—it’s a deliberate strategy to improve how your team works together and maintain a codebase that doesn’t become a tangled mess over time. By getting comfortable with git rebase and squash, you’re moving beyond just using Git and starting to master it for a more effective team.
Alright, let’s get into the nuts and bolts of your first interactive rebase. This is where you roll up your sleeves and transform a messy commit history into something clean and professional. It all starts with one command, but it opens up a powerful editing session where you’re in complete control.
To kick things off, you’ll use git rebase -i. The -i stands for interactive, and that’s the whole point. You also need to tell Git how far back you want to look. The most common way to do this is to count back from your current position, which Git calls HEAD.
Let’s say you’ve made 5 commits on your feature branch that are a mix of work-in-progress, fixes, and actual features. To start cleaning them up, you’d run this:
git rebase -i HEAD~5
This command tells Git, “Open an interactive editor for the last 5 commits from where I am right now.”
After you hit Enter, Git will drop you into your default command-line text editor (like Vim or Nano). You’ll see a file that lists the commits you selected for the rebase.
By default, every commit is marked with pick. It looks something like this:
pick 1a2b3c4 feat: add initial login form structure pick 5d6e7f8 fix: correct input field typo pick 9g8h1i2 style: adjust button padding pick a3b4c5d wip pick f6e7d8c feat: add form validation logic
Each line represents a single commit, showing the command, the commit hash, and the message. To squash them, you’ll just change the commands on each line.
Here are the commands you’ll use most often:
pick: This is the default. It just means, “Keep this commit as is.”squash: This is what we came for. It takes the commit and merges it into the one directly above it. Git will then ask you to combine their commit messages.fixup: This is a leaner version of squash. It also merges the commit into the one above it but completely discards the commit message. It’s perfect for those little “oops” commits like “fix typo” or “wip” where the message is just noise.To actually squash your commits, you just edit this file. The key is to leave the first commit in the list as pick and change the ones you want to merge into it to either squash or fixup.
Think of the top
pickcommit as your base. Everysquashandfixupbelow it will be folded up into that base, creating one solid, logical commit.
This process is what turns a tangled history into a clean, linear one, making it much easier for others to review.

As you can see, what started as a messy series of changes gets simplified into a single, straightforward path. That’s the goal.
Using our example, let’s combine all 5 commits. I’d edit the file to look like this, using fixup for the purely corrective commits that don’t add valuable information.
pick 1a2b3c4 feat: add initial login form structure squash 5d6e7f8 fix: correct input field typo fixup 9g8h1i2 style: adjust button padding fixup a3b4c5d wip squash f6e7d8c feat: add form validation logic
Once you save and close that file, Git immediately opens another editor window. This new file contains all the commit messages from your pick and squash commands (remember, fixup messages are thrown away).
This is your moment to write a great commit message. Delete all the old, messy lines. Write a single, clear message that explains the what and the why of the combined change.
For our example, a good final message would be:
feat: Implement user login with email and password
Adds a complete login form with client-side validation for email format and password length. The form is wired to the authentication endpoint and provides user feedback on success or failure.
Save that new message, and you’re done. Git completes the rebase, and your 5 messy commits are gone, replaced by a single, descriptive one. You’ve successfully cleaned up your history.
Once you get the hang of interactive rebase, you can start automating the tedious parts. The real goal is to stop manually cleaning up your commit lists and create a workflow where your history practically organizes itself. This is exactly what autosquash was built for.
This clever feature lets you mark commits for squashing or fixing up right when you create them. By using a couple of simple flags, you essentially pre-program your rebase session. This saves a ton of time, especially when you’re getting a feature branch ready for a pull request.
The whole trick relies on two flags you can add to your git commit command: --fixup and --squash.
Let’s say you’re working on a feature and spot a typo or a small bug in a commit you just made. Instead of creating another commit with a message like “oops, fix typo,” you can target the original commit directly.
If the original commit’s hash is 1a2b3c4, you’d just run this:
git commit --fixup 1a2b3c4
This creates a new commit but with a special message: fixup! feat: add initial login form structure. When you kick off an interactive rebase later, Git sees this message. It automatically moves the fixup commit right after the one it’s supposed to fix and marks it for you. You don’t have to reorder or edit a thing.
With this, your development process feels much more fluid. You can make your main commits as you build out a feature. When a small, related change is needed, you just create a fixup or squash commit that points to the right parent.
When you’re ready to clean everything up for review, all you need is one command:
git rebase -i --autosquash main
The interactive editor will pop up, but the fixup and squash commits will already be in the perfect order and marked correctly. All you have to do is save and close the file. What used to be a manual, error-prone editing session is now a single, satisfying confirmation step.
This isn’t just a minor improvement; it’s a huge time-saver. Teams that adopt these techniques often see their commit history shrink by 60-70%. A messy feature branch with 5-15 “work-in-progress” commits can be condensed into just 1-3 clean, logical commits for the final PR. You can learn more about how to refine your Git history on thoughtbot.com.
If you’re more of a visual person and prefer a GUI, the built-in Git tools in VS Code make this process even simpler, especially with an extension like GitLens.
Instead of the command line, you can manage the entire rebase from the “Commits” view. You can right-click the commit you want to rebase onto and select Rebase current Branch onto Commit. From there, an interactive editor opens up where you can drag-and-drop to reorder commits or just click to change an action to pick, squash, or fixup.
It’s a great way to understand what’s happening without needing to memorize all the commands. For many developers, mastering these visual and command-line techniques is a key part of creating an efficient software development workflow.
And when you combine Git mastery with project management tools, your team’s process gets even better. We’ve written more about the synergy between Jira and Git if you’re interested in connecting your commits to your tasks.
Let’s be honest—the moment a rebase stops and flashes a CONFLICT message is when a lot of developers feel a spike of panic. But it really doesn’t have to be a disaster. Once you understand why a conflict happens during a rebase, the whole process becomes predictable and much less scary.

A rebase conflict simply means Git tried to apply one of your commits onto its new base, but the same lines of code had already been changed. When this happens, Git doesn’t crash or ruin your work. It just pauses the rebase and politely hands you the controls.
When Git pauses, it tells you exactly which files have conflicts. Your job is to open those files and sort out the differences, which is the same process you’d use for a standard merge conflict.
Inside a conflicted file, you’ll find Git’s conflict markers (<<<<<<<, =======, >>>>>>>) showing you the competing changes:
<<<<<<< HEAD and ======= is the version from the branch you’re rebasing onto (like main).======= and >>>>>>> is the version from the commit Git is currently trying to apply.Your task is to edit the file until it looks exactly the way you want it, making sure to remove all the conflict markers. Once a file is fixed, you have to tell Git you’re done.
First, you stage the file you just fixed:
git add <conflicted-file-name>
After you’ve staged all the files you fixed, you just tell Git to get back to work:
git rebase --continue
Git will then pick up right where it left off, moving on to the next commit in the sequence.
What if you get stuck in a particularly nasty conflict or feel like you’ve made a mess? This is where knowing your escape plan gives you real confidence. You never have to force your way through a confusing rebase.
Knowing you can undo anything turns a stressful rebase into a simple, solvable problem. The goal isn’t to avoid conflicts entirely, but to know exactly what to do when they pop up.
Here are the commands that will be your safety net:
git rebase --abort: This is your big red “undo” button. It instantly cancels the entire rebase and puts your branch back exactly as it was before you started. No harm done. If you’re ever in doubt, this is your go-to command.git rebase --skip: This one is a bit more situational. It tells Git, “Hey, forget about this specific commit, just skip it and move on to the next one.” It’s useful if a conflict is happening in a commit that you realize you don’t even need anymore.With these tools in your back pocket, git rebase stops being a high-stakes operation. Conflicts become a minor detour, not a major roadblock, freeing you up to use git rebase and squash to their full potential.
We’ve all been there. You’re deep in a rebase, you squash the wrong commits, or a merge conflict goes sideways. Suddenly, your branch is a mess, and that sinking feeling hits: “Did I just lose all my work?”
This fear is exactly why many developers avoid git rebase altogether. But what if I told you there’s a nearly foolproof way to undo any mistake?

Meet git reflog. It’s your personal time machine for your local repository.
The secret is that Git rarely throws anything away for good. The reflog (short for reference log) keeps a private record of every single move the HEAD of your branch makes. Think of it as a journal of your actions—including the exact state of your branch right before you started that risky rebase.
Let’s say you just finished that rebase and immediately realized something went wrong. Don’t panic. Just pop open your terminal and run:
git reflog
You’ll see a detailed history of your recent activity, with each step marked by a pointer like HEAD@{n}. It’s like a breadcrumb trail of your every move.
a1b2c3d HEAD@{0}: rebase -i (finish): returning to refs/heads/my-feature a1b2c3d HEAD@{1}: rebase -i (squash): feat: implement new login flow e4f5g6h HEAD@{2}: rebase -i (squash): # This is a combination of 3 commits. … z9y8x7w HEAD@{5}: checkout: moving from main to my-feature
In this output, HEAD@{0} represents the current, messy state after the rebase. The gold is finding the state right before you started. Here, that’s HEAD@{5}, logged when you first checked out the branch. That pointer is your escape hatch.
To get back to that exact moment, use a hard reset.
git reset --hard HEAD@{5}
And just like that, the entire rebase is undone. Your branch is instantly restored to its original state, with all your old commits perfectly intact. No harm done. Nothing lost.
Knowing this command exists is a complete game-changer. It turns a high-stakes, nerve-wracking operation into a safe, sandboxed experiment. If you get lost, you’re only one command away from starting over.
Once you’re comfortable rewinding your work, you can focus on other ways to keep your workspace clean. For example, learning how to delete a local Git branch is a great next step for maintaining a tidy repo.
This safety net gives you the freedom to practice and truly master git rebase and squash without ever worrying about breaking things permanently.
Once you get the hang of the commands, you start running into the real-world questions, especially when working on a team. Knowing the “why” and “when” is just as important as the “how.” Let’s clear up a few of the most common things that trip developers up.
If you remember only one thing, make it this: never rebase a public or shared branch. I’m talking about branches like main, develop, or any other branch that your teammates have pulled and are actively working on.
Rebasing literally rewrites history. When you rebase a shared branch, your version of history suddenly looks completely different from everyone else’s. This is a recipe for disaster, leading to a tangled mess of conflicting merges and duplicated commits that pollute the repository. It creates a massive headache for the whole team.
Think of it this way: Your local feature branch is your private workspace. It’s a rough draft. You can rewrite, reorganize, and clean it up as much as you need. But once you merge it into a shared branch like
main, it’s considered published. From that point on, you only add new chapters (commits); you don’t go back and change what’s already been written.
So, keep your rebasing to your own local feature branches before you merge them. Once a commit history is shared, you should stick with git merge to bring in new updates.
Both squash and fixup let you combine commits during an interactive rebase, but they handle the commit messages differently. Choosing the right one makes cleaning up your history much faster.
Here’s my rule of thumb:
Use squash when the individual commit messages have value. When you squash a commit, Git combines its changes with the commit above it and then opens your editor with both commit messages. This is your chance to craft a single, comprehensive message from the raw material. It’s perfect when your intermediate commits explain why a change was made.
Use fixup for commits with throwaway messages. A fixup does the same thing as a squash—it combines the changes—but it completely discards the commit message. This is exactly what you want for all those noisy little commits like “fix typo,” “wip,” or “forgot another file.” The code change matters, but the message itself adds zero value to the final history.
Getting this right saves you from manually deleting a bunch of useless commit messages in the final step.
After you’ve rebased a branch that’s already on a remote server (like for a pull request), your local history is now out of sync with the remote. A regular git push won’t work; Git will reject it because the histories have diverged. You have to force the update, but there’s a safe way and a dangerous way to do this.
The command you should always reach for is git push --force-with-lease.
This command is a game-changer. It adds a crucial safety net by checking one thing before it overwrites the remote branch: has anyone else pushed new commits since I last pulled? If someone has pushed new work, your push will fail, preventing you from accidentally wiping out their changes.
The alternative, git push --force, is a blunt instrument. It overwrites the remote branch no matter what, which is how teammates’ work gets lost. Using git push --force-with-lease is the professional standard for updating a rebased branch. It’s just better for collaboration.
Use these principles to turn a messy feature branch into a clean, linear history that accelerates code reviews and simplifies debugging.
--abort.main or develop.git commit --fixup with git rebase -i --autosquash for effortless cleanup.git rebase -i main).git push --force-with-lease.Gallery
FAQ
Haven't found what you're looking for?
Connecting your GitHub org gives you team-level AI adoption signals out of the box — PR throughput, review patterns, and Acceleration Reports — without anyone installing anything else.
No. GitHub alone unlocks the team dashboard. For richer per-engineer insights (AI Stack, AI Skills, agent activity), engineers can install Zest in Cursor or Claude Code at any time.
Zest tracks blockers across tasks and prompts. By spotting weak spots and highlighting working strategies, your team keeps momentum and ships code faster.
Copilot metrics tell you usage; Zest tells you outcomes. We measure AI compute impact on PR throughput, code review quality, and agent autonomy levels — across Cursor, Claude Code, Codex, and any model your team runs.
Adoption charts show who's using AI, how often, and where value is gained. Teams and enterprises can drive toward 100% usage — ensuring everyone benefits.
Yes. Connect your GitHub org and you'll get the L2 dashboard — leaderboards, AI metrics, and team insights — with no credit card needed.