It happens on real teams. Someone merges the wrong thing, a dependency update silently breaks the build, or a hotfix lands without a proper review. Whatever the reason, master is broken and you still have work to ship.
The good news: if you have a working release tag, you have everything you need. This guide walks through the full recovery arc, branching from the tag, adding new code on top, and raising a clean pull request that merges back to master with no surprise conflicts.
Why branching from the tag is the right move
The instinct is often to "fix master directly." Resist it. You do not know the full extent of what broke, and working on master violates the PR policy anyway. The tag is a precise snapshot of code that worked. Treating it as your new baseline keeps every subsequent decision clean and reviewable.
The strategy has three phases: establish a branch at the good commit, build your new work on top of it, then reconcile the divergence before raising a PR so the reviewer sees nothing but your intentional changes.
Phase 1: Create a branch from the tag
This single command is the foundation of everything that follows. It tells Git to set your new branch's starting commit to exactly where v1.3 points, not to the current tip of master.
# Create the branch rooted at the v1.3 tag
git checkout -b fix/restore-from-v1.3 v1.3
# Push it to Bitbucket so the team can see it
git push -u origin fix/restore-from-v1.3
Before writing a single line of new code, take a moment to understand exactly what diverged. This pays off when you reconcile later.
# What commits exist on master that are not in v1.3?
git log v1.3..origin/master --oneline
# What do those changes actually look like?
git diff v1.3 origin/master
Key insight: The diff above tells you exactly which files the broken commits touched. Those are the files you will need to consciously handle when you reconcile later. Keep a mental note of them.
Phase 2: Add your new work on top
You are now on a stable foundation. Build your features or fixes as normal, with one discipline: commit small and commit often. This is not just good practice in general, it becomes essential when you need to explain your PR to a reviewer who is looking at a branch that diverges from a broken master.
# Work on your files normally
git add src/feature-x.js src/utils.js
# Write commit messages that will read well in the PR
git commit -m "feat: add validation layer for incoming payloads"
git add tests/feature-x.test.js
git commit -m "test: unit coverage for payload validator"
# Push regularly : this keeps Bitbucket in sync
git push origin fix/restore-from-v1.3
Phase 3: Reconcile before you raise the PR
This is the step most guides skip or handle lazily. If you raise a PR now, Bitbucket will show conflicts because your branch started at v1.3 and master has moved (to broken commits) since then. The reviewer gets a noisy, confusing diff.
The correct approach is to resolve all of this locally on your branch before the PR goes up. Bitbucket then shows a clean diff of exactly what you intended to change.
Step 1: Pull master's current state into your branch
# Make sure you have the latest remote state
git fetch origin
# While ON your fix branch, merge master in
git checkout fix/restore-from-v1.3
git merge origin/master
Step 2: Resolve conflicts, keeping your code
Git will flag every file where your branch and the broken master diverge. For each conflict, you decide: keep your code, keep master's code, or blend them. In a recovery scenario, you almost always want to keep your branch's version.
# Option A: Accept your branch's version for a specific file
git checkout --ours src/api/handler.js
git add src/api/handler.js
# Option B: Open the file manually, remove conflict markers,
# edit to the state you want, then stage it
# <<<<<<< HEAD (your branch)
# ======= (divider)
# >>>>>>> origin/master (master)
git add src/api/handler.js
# Once all conflicts are resolved, commit the merge
git commit -m "merge: absorb origin/master, keep v1.3 baseline + new features"
# Push the resolved branch
git push origin fix/restore-from-v1.3
Watch out with
--ours: During agit merge, "ours" refers to the branch you were on when you ran the merge your fix branch. "Theirs" refers to the branch being merged in master. This is the opposite of how it works during agit rebase. Keep this straight.
Step 3: Verify the diff before raising the PR
This is your sanity check. The three-dot diff shows only the commits that exist on your branch but not on master, exactly what the reviewer will see in the PR.
# Three-dot diff: "what does my branch add beyond master?"
git diff origin/master...fix/restore-from-v1.3
# Cross-check: should show ONLY your new intentional changes
git diff v1.3 HEAD
# If the output matches your intentions, you are ready to raise the PR
Raising the pull request in Bitbucket
With the branch pushed and conflicts pre-resolved, the PR creation is straightforward. In Bitbucket, navigate to your repository, go to Pull Requests, and create a new PR from fix/restore-from-v1.3 to master.
Source branch:
fix/restore-from-v1.3. Target branch:master. Bitbucket should now show no merge conflicts because you resolved them on your branch.Write a clear PR description. Mention that the branch was rooted at tag
v1.3due to the broken master state. Reviewers need this context to evaluate the diff correctly.Link the relevant issue or Jira ticket if your team uses one. This anchors the PR to business context and keeps the audit trail intact.
The reviewer sees a clean, intentional diff. No broken master code, no spurious conflict markers. Just your new work on top of the last stable release.
What happens to master after the merge
When the PR merges, master will contain: the complete history up to v1.3, the merge commit that absorbed the broken state, and your new feature commits on top. The broken commits are still in the history, they are not erased, but they are effectively neutralized because your branch's code takes precedence in the final snapshot.
This is the cleanest outcome achievable under a PR-only policy. You did not force-push, you did not rewrite shared history, and every change that landed in master went through a review.
A note on force-pushing master: Some guides suggest resetting master to the tag and force-pushing. That works in a solo repo but is destructive on a shared branch, it rewrites history that other developers may have already pulled. The approach in this guide avoids that entirely.
Quick reference: the full command sequence
# 1. Branch from the working tag
git checkout -b fix/restore-from-v1.3 v1.3
git push -u origin fix/restore-from-v1.3
# 2. Understand the divergence (read-only, safe to run anytime)
git log v1.3..origin/master --oneline
git diff v1.3 origin/master
# 3. Do your work, commit often
git add <files>
git commit -m "feat: your message here"
git push origin fix/restore-from-v1.3
# 4. Absorb master into your branch (pre-resolve before PR)
git fetch origin
git merge origin/master
# resolve conflicts, then:
git add .
git commit -m "merge: absorb master, keep v1.3 baseline + new features"
git push origin fix/restore-from-v1.3
# 5. Verify the diff is clean
git diff origin/master...fix/restore-from-v1.3
# 6. Raise the PR in Bitbucket: fix/restore-from-v1.3 -> master
The principle worth keeping
Tags are not just release markers. They are named, immutable recovery points. Any time master becomes unstable and a working tag exists, you have a clean path back, without rewriting shared history, without bypassing your PR policy, and without making your reviewer's job harder than it needs to be. The work is in the reconciliation step. Get that right, and the merge is uneventful.
Top comments (0)