← Writing

Un-merging a feature without rewriting shared history

A tester needed four features tested — without the unfinished fifth that was already merged into the same dev branch. Force-push was off the table. Here's the surgical git recipe that worked, and when each alternative is the right call.

On this page

Here’s a situation that comes up more often than any git tutorial admits: a shared dev branch contains five features. Four are finished and the tester needs them now. The fifth is half-done, already merged, and absolutely not ready for testing.

This happened on a client e-commerce project. Credit notes, bulk quotations, a category-hierarchy rework, and import sheets — all ready. An unfinished “inventory intelligence” feature — merged into the same branch, not ready, and liable to make the tester’s report useless with noise from a feature nobody should be looking at yet.

The question: how do you produce a branch containing everything except one feature, when that feature is already woven into shared history?

The options, and why the obvious ones lose

We talked through three approaches before touching anything. The tour is worth taking, because choosing the right one is most of the skill.

Option 1: force-push dev back to before the feature. Instant disqualification. dev is a shared branch — other people have it checked out, have built on it, will pull it. Rewriting its history means every collaborator’s next pull either resurrects the commits you deleted or tangles their local history. Force-push is a fine tool on a branch with exactly one writer. On a shared branch it’s how you spend a day untangling three people’s repos.

Option 2: git revert the feature’s merge commit(s). Legitimate, and often the right answer. Revert adds new commits that undo old ones — history is appended to, never rewritten, so collaborators are safe. Two things made it messy here. First, the feature hadn’t landed as one tidy merge; its commits were interleaved with other work, so a clean revert set was hard to identify with confidence. Second, this feature was coming back once finished — and re-merging previously-reverted work means reverting the revert, a workflow that’s technically fine and practically a famous source of confusion.

Option 3: cherry-pick the four good features onto a fresh branch. The mirror image. Works, but now you’re hand-selecting the majority of the history and hoping you didn’t miss a commit. When you want “everything except X,” building “only A, B, C, D by hand” inverts the problem into its harder form.

The surgical answer

What we actually did combines a reset-point with selective file restoration — no shared history touched:

  1. Create a new branch from dev — the shared branch itself is never modified. All surgery happens on a branch whose history nobody else depends on.

  2. Rewind the new branch to the last commit before the unwanted feature arrived. Find that point in the log; the new branch gets reset to it. Now you have a branch that predates the feature — but it also predates some of the wanted work.

  3. Selectively pull the current tree back in, minus the feature. This is the key move:

    git checkout <sha>^ -- <path>
    

    git checkout <commit> -- <path> takes file contents from a commit without touching history. Using it against the relevant commit’s parent (<sha>^) for the unwanted feature’s directory restores that path to its pre-feature state — while the rest of the tree is brought up to match current dev. Because the unfinished feature lived in an identifiable directory, “the tree minus one feature” was expressible as path operations rather than commit archaeology.

  4. Commit the result and raise a single PR. The output is one clean, reviewable branch: four features in, one feature absent, dev untouched, nobody’s local repo disturbed. The tester tests; the unfinished feature continues on dev unbothered and merges normally when it’s done.

The general recipe

When you need “the branch, minus one thing”:

  • Never operate on the shared branch. Make a new branch; do all surgery there. This single rule removes the entire force-push category of disaster.
  • Prefer content operations over history operations when the feature is path-separable. If the unwanted work lives in its own directories, git checkout <commit> -- <path> reshapes the tree directly and you skip commit archaeology entirely. This is the most underused tool in the box.
  • Use revert when the unwanted change is a small number of clean merge commits and it won’t be re-merged soon.
  • Use cherry-pick when the wanted set is small — a couple of features onto a release branch — not when it’s the majority.
  • Ship the result as one PR. The person consuming the branch gets one thing to pull and a description of exactly what’s in and what’s out.

The deeper lesson: git surgery under pressure is a decision problem before it’s a command problem. The commands are three lines. Knowing which three — and which branch is allowed to feel the knife — is what keeps a testing-day problem from becoming a whole-team problem.

← All writing Book a call →
Book a call → WhatsApp