Three complete FormDialog implementations, Clean, Redesigned, and Simplified, produced a final integration commit that carried none of them forward, yet it still added 380 lines across six files. The surviving code was a 266 line PaymentInfo component, a getSessionFormPaymentInfo service function, 82 lines of QuickAddFormModal work, and consistent form naming across the GlobalForms pages. I had built the three rewrites in parallel because the modal had stopped responding to small edits, and a single branch of further patches could not have told me which changes were actually worth keeping.
That commit was not a retreat to the old modal. It changed six files, added 380 lines, and removed 30. It introduced a 266 line PaymentInfo component, a getSessionFormPaymentInfo service function, 82 lines of QuickAddFormModal work, and consistent form naming across the GlobalForms pages. The one thing it refused to carry forward was any of the three complete dialog rewrites.
I had run the rewrites in parallel because the modal had stopped responding to small edits. A single branch with another round of props, conditions, and markup changes would have told me only whether the latest patch happened to look better. It would not have separated a better layout from a better data boundary, or a useful naming cleanup from a new dialog structure I did not want to own.
The branches gave each attempt permission to be complete. They also gave each one permission to die.
The diff looked like a failed experiment
The diagnostic path started at the final diff, not at the first branch. I saw a commit explicitly excluding all FormDialog attempts while carrying a substantial amount of related code. At first glance, that shape looks contradictory. Three implementations had been built, then none of them shipped.
The file list made the actual decision legible. The surviving changes were not random lines lifted out of a wreck. They formed a small set of boundaries around the dialog problem:
src/components/PaymentInfo.jsx +266src/services/sessionForms.js +10GlobalFormsPage.jsx +40FormsDataView.jsx +6GlobalFormsList.jsx +6QuickAddFormModal.jsx +82PaymentInfo was the largest surviving piece. That matters more than the fact that it had 266 lines. Payment details became a reusable component instead of remaining folded into a particular FormDialog design. The service layer received getSessionFormPaymentInfo, so the payment lookup had a named access point distinct from whatever the modal happened to render. The quick-add modal kept its UI improvements without becoming the foundation for the broader dialog rewrite.
The GlobalForms changes were smaller, 40 lines in the page and 6 lines in each supporting component, but they had the same character. Consistent form naming could survive any future dialog decision. It was a local correction with a stable scope, not an argument for one branch’s whole composition.
Once I read the commit that way, the three discarded dialogs stopped looking like wasted work. They were a set of implementation probes. Their job was to expose which pieces were structural and which pieces only made sense inside one attempt.
Why I did not keep iterating on one branch
The tempting alternative was a single long-lived branch. Start with Clean, patch the rough edges, borrow a few ideas from Redesigned, then simplify whatever got too ambitious. That feels efficient because every change remains in one place.
It also destroys the comparison.
After enough iterative edits, a branch no longer tells you which decision caused the improvement. A better payment display, a renamed form field, and a different modal lifecycle arrive in the same working tree. If the result is awkward, you cannot remove the lifecycle choice without also backing out the display work that came with it. The branch becomes a history of compromises rather than a candidate you can evaluate cleanly.
Choosing one of the three complete rewrites would have been simpler in a different way. It would have given us a decisive merge and a clear story. It lost because the useful work did not align with a single complete FormDialog implementation. The evidence was in the final commit: every full dialog attempt was excluded, while the payment component, service function, quick-add work, and naming consistency were retained.
A fourth option was to copy changes manually until the result looked right. I rejected that too. Manual reconstruction hides provenance. Later, it is hard to know whether a line came from Clean, Redesigned, or Simplified, and harder still to compare the retained code against its original context. Cherry-picking creates a deliberate selection boundary. The discarded branch remains available as evidence, while the integration commit states exactly what we chose to own.
The integration commit was the real design artifact
I think of the three branches as scaffolding now. They were not product architecture. They were temporary structures that made the product architecture easier to see.
That framing changes the success condition. A branch does not need to be mergeable to be valuable. It needs to answer something clearly enough that we can preserve the answer in a smaller, better-placed change. In this case, the answer was not a winning FormDialog. It was that payment information deserved its own component and service call, quick-add had improvements worth carrying forward, and the GlobalForms names needed to line up.
The final commit was therefore not a salvage job. It was a filter. It accepted 380 additions and rejected three complete implementations because the reusable seams mattered more than a branch narrative.
I keep disposable branches for this class of problem because they reset the argument. Each attempt can make a strong case. The final integration does not have to declare a winner. It only has to keep the code that still makes sense after the branch that produced it is gone.