← All blogs

Git Worktrees, Rebases, and Hotfixes: How to Handle Multiple Branches in a Real Team

A story-driven guide to handling feature work, urgent hotfixes, Git worktrees, changing main branches, rebasing, conflicts, pull requests, and cleanup in a real enterprise workflow.

Git Worktrees, Rebases, and Hotfixes: How to Handle Multiple Branches in a Real Team

You are in the middle of a new authentication feature, with several local changes in progress.

Then a message arrives:

“We have a production issue. Login is failing for some users. Can you fix it urgently?”

Some users cannot sign in, and your authentication work is half done on the same machine. You need to fix production without disturbing the feature you are building.

Knowing individual Git commands is the easy part. Choosing the right operation when the situation changes is what matters in a real development team.

Starting the Feature

To see how you got here, go back to the moment you receive the authentication task.

The team’s shared branch is main, so you fetch first to start from current code.

You run git fetch:

git fetch origin

Then create your feature branch with git switch:

git switch -c feat/005.authentication origin/main

The origin/main at the end tells Git to create your feature branch from the freshly fetched main.

A few commits later, your repository looks like this:

main
  │
  ├── A
  └── B
       \
        C ─── D ─── E
             ↑
      feat/005.authentication

Your authentication work now lives on feat/005.authentication, while main remains untouched.

main Moves While You Work

A few hours later, another developer finishes a bug fix and merges it into main.

You run:

git fetch origin

Git now knows about the new commit:

origin/main                A ── B ── F
feat/005.authentication    A ── B ── C ── D ── E

git fetch updates your local knowledge of the remote branches and leaves your current branch and working files alone.

main moving ahead is routine, so you keep developing authentication.

A Production Problem Appears

Back to the login failure.

You are still working on feat/005.authentication, and your working directory contains unfinished changes.

You could switch branches, but Git may refuse if your changes would be overwritten. You could temporarily put those changes aside with git stash:

git stash
git switch main

Your working directory is now clean, so you can switch branches without carrying the unfinished authentication changes with you.

When you return to the feature branch, restore the changes with:

git switch feat/005.authentication
git stash pop

git stash pop applies the stashed changes back to your working directory and removes that stash entry if the application succeeds.

If you have multiple stashes, you can inspect them with:

git stash list

For example:

stash@{0}: WIP on feat/005.authentication: E1a2b3c add authentication flow

Stashing is useful when you need to temporarily clear your working directory for a short operation.

You could also commit the unfinished work as a work-in-progress commit, but that puts incomplete changes into the feature history.

In this situation, however, you need to fix the production issue while keeping your authentication work available exactly as it is. A Git worktree gives you another working directory for the hotfix while your authentication work remains untouched.

You already fetched the latest remote state, so you create the hotfix worktree directly from origin/main:

git worktree add ../job-application-api-hotfix \
    -b hotfix/login-failure origin/main

The command creates a new directory beside your project, creates hotfix/login-failure in it with -b, and starts that branch from the latest remote main.

You now have:

job-application-api/
    feat/005.authentication

job-application-api-hotfix/
    hotfix/login-failure

The directory holds the files, the worktree is Git’s record of that directory, and the branch is what the worktree has checked out.

Your unfinished authentication work stays exactly where it was, while the hotfix has its own working directory.

Fixing the Production Issue

You investigate the login failure in the hotfix directory and find the problem.

You fix it and run the tests with the Maven Wrapper:

./mvnw test

Once the change is correct, stage the files that belong to the fix, create the commit, and push the hotfix branch:

git add <specific-files>
git commit -m "fix: resolve login failure"
git push -u origin hotfix/login-failure

The hotfix branch should hold the production fix and none of your unfinished authentication work.

The history now looks roughly like this:

main
  │
  ├── A
  ├── B
  └── F
       \
        H
        ↑
   hotfix/login-failure

Your authentication feature remains isolated in the other directory.

main Moves Again

While you finish the hotfix, another developer merges a second change into main.

You fetch again:

git fetch origin

Now you can see the two histories:

Before:

origin/main   A ── B ── F ── G
hotfix        A ── B ── F ── H

Your hotfix works, but it is based on an older version of main.

Teams commonly choose between rebase and merge here. Your team’s repository policy determines which one you should use.

If your team uses git rebase, you run:

git rebase origin/main

Rebase takes your hotfix commit and replays it on top of the current main:

After:

origin/main   A ── B ── F ── G
hotfix        A ── B ── F ── G ── H'

H' represents the same logical change as H, but it has been recreated on a new base, so it has a new commit identity.

If your team uses git merge instead, you might run:

git merge origin/main

The result can look like:

A ── B ── F ── G ── M
          \         /
           └─ H ───┘

Here M is a merge commit that joins the two histories.

Someone Already Fixed the Bug

Rebasing can also surface a harder case.

Suppose the change that arrived as G is another developer’s fix for the same login bug, and your hotfix fixes it too.

The rebase from the previous section is where this shows up.

If your change is textually identical to the change already present in main, Git can recognize that the patch is already there and skip the duplicate change during the rebase.

When Git skips your only commit, nothing remains. Close the hotfix branch, since there is nothing left to review.

If both developers solved the same problem differently, Git may be able to combine the changes automatically when they touch different parts of a file. That can leave both implementations in the result.

After the rebase, git diff origin/main...HEAD compares your branch with the point where it left main, so it shows only your own changes. You ask whether it still holds a production fix that main lacks. If one implementation is redundant, you remove it before opening the pull request.

When Git Cannot Combine the Changes

Sometimes both changes modify the same lines.

Git cannot decide which version represents the correct application behavior, so it stops the rebase and reports a conflict.

The affected file may contain conflict markers:

<<<<<<< HEAD
code from main
=======
code from your branch
>>>>>>> a1b2c3d (fix: resolve login failure)

During a rebase, the conflict can be confusing because HEAD represents the branch you are rebasing onto, while the lower section represents the commit currently being replayed.

For each conflict, ask what main changed, what your hotfix changed, and which behavior the application should have after the fix. Once the file is edited into its final state, git add <resolved-file> and git rebase --continue finish the rebase, and git rebase --abort returns you to where you started if you change your mind.

git add <resolved-file>
git rebase --continue

If you need to abandon the rebase:

git rebase --abort

The resulting code must still represent the correct application behavior.

Preparing the Pull Request

The hotfix is now based on the current main, and any conflicts have been resolved.

Before opening the pull request, inspect what your branch actually contains:

git diff origin/main...HEAD

Read this diff as if you were reviewing someone else’s pull request. You expect the production login fix and nothing else. Authentication feature changes, debugging code, unfinished work, unrelated formatting, and duplicate fixes all come out before review.

A final ./mvnw test run confirms that the rebased branch still passes.

You pushed the hotfix earlier, and the rebase rewrote that commit, so a plain git push is rejected. You run:

git push --force-with-lease

--force-with-lease overwrites the remote branch only if nobody else has pushed to it since your last fetch, which makes it safer than --force, which overwrites unconditionally.

A merge-based workflow does not rewrite commits and needs no force.

Now you open a pull request from hotfix/login-failure into main, and the branch is ready for review.

The Pull Request Is Merged

The pull request passes review and continuous integration, and the hotfix is merged into main.

The production issue is resolved.

You leave the hotfix directory, remove its worktree with:

git worktree remove ../job-application-api-hotfix

and delete the merged branch locally with:

git branch -d hotfix/login-failure

If your team’s GitHub workflow does not delete remote branches automatically, you can remove it there as well:

git push origin --delete hotfix/login-failure

Your feature directory is exactly as you left it. The hotfix lived in its own worktree, and the pull request carried only the fix.