How would you migrate a repository from Git Flow to a single long-lived main branch?
answer
- Converge before you delete anything
- Two commands tell you what each line uniquely has
- Old branches still point at the old base
- Rewrite private branches, merge shared ones
- Hooks, refspecs and remote HEAD still name it
basics
~20 sConverge the two lines first: ship or merge everything on develop into main so nothing is stranded, re-point in-flight branches onto main, then delete develop locally and on the remote and update every ref, hook and upstream that named it.
solid answer
~40 sTreat it as a convergence problem, not a deletion. Start by measuring: `git rev-list --left-right --count main...develop` and `git log --oneline main..develop` show exactly what each line has that the other lacks. Close the gap deliberately — either ship one final Git Flow release so `main` gains everything, or merge `develop` into `main` once — until `main..develop` is empty. Then move in-flight work: `git rebase --onto main develop feature/x` replays a feature's own commits onto `main`, or merge `main` into the branch if it is already shared and rewriting would disrupt others. Only then delete `develop` locally and on the remote. Finish by fixing everything that named it: upstreams shown by `git branch -vv`, hook scripts and refspecs that hardcode `develop`, and the remote HEAD that new clones follow.
code
bash · 9 lines# 1. how far apart are the two permanent branches?
git rev-list --left-right --count main...develop
git log --oneline main..develop # unreleased work
git log --oneline develop..main # hotfixes never merged back
# 2. converge, then verify nothing is stranded
git switch main
git merge --no-ff develop
git rev-list --count main..develop # must print 0go deeper
Know that develop and main are separate histories, so the migration is about merging one into the other before any branch is deleted, not about deleting a branch.
Be able to name the commands that measure and close the divergence, and explain why an open feature branch needs its base moved rather than just being merged later.
Show the operational side: sequencing with the team, choosing rebase versus merge per branch, and hunting down the hooks, refspecs and upstreams that still name the retired branch.
Own the reasoning for migrating at all and the definition of done — the benefit is a short merge base, so a migration that leaves branches long-lived has changed the diagram and nothing else.
## The state you are starting from A Git Flow repository has two permanent branches that are genuinely different histories. `main` holds released commits; `develop` holds everything merged since the last release. Feature branches descend from `develop`, tooling and hooks may name `develop` explicitly, and everyone's clone checked out `develop` by default. A migration touches all of that, and the failure mode is not dramatic — it is a commit quietly left on a branch nobody looks at any more. ## Step 1 — measure the divergence Before changing anything, quantify it. `git rev-list --left-right --count main...develop` (three dots) prints two numbers: commits reachable only from `main`, and only from `develop`. `git log --oneline main..develop` lists the second set concretely, and `git log --oneline develop..main` the first — that second list is the interesting one, because anything on `main` that never made it back to `develop` is usually a hotfix that was merged in one direction only, exactly the Git Flow bug the migration is meant to end. ## Step 2 — converge the two lines There are two honest ways to close the gap. Run one final Git Flow release: cut a release branch from `develop`, merge it into `main`, tag it, and merge back. This is the least surprising option for a team mid-cycle, because the last act under the old model is the model working normally. Or, if there is no release ceremony worth preserving, merge `develop` into `main` directly with `git merge --no-ff develop` and tag the result. Either way the finishing condition is the same and is checkable: `git rev-list --count main..develop` returns 0, meaning `main` now contains everything `develop` had. Do not delete `develop` before this holds. Deleting a branch removes only the pointer, but once the remote branch is gone the commits it uniquely held may be unreachable on the server, and recovering them depends on someone's local clone still having them. ## Step 3 — move in-flight work Every open feature branch descends from `develop`, so merging one into `main` afterwards would drag along whatever `develop` had that `main` did not — which after step 2 is nothing, but the branch's merge base is still an old point. For a branch that only its author has, `git rebase --onto main develop feature/checkout` replays exactly the commits in `develop..feature/checkout` on top of `main`, giving a clean, recent merge base. For a branch several people already have, prefer merging `main` into it: rebasing rewrites commits that others may hold, and the recovery from that is its own subject. Deciding per branch — rather than mandating one — is the judgment part of this migration. ## Step 4 — retire develop Once `main` contains everything and open branches are re-pointed, delete the branch: `git branch -d develop` locally (the `-d` form refuses if it is not merged, which is a useful last check) and `git push origin --delete develop` on the remote. Expect a long tail: every clone still has a stale `origin/develop`, cleared with `git fetch --prune`. Local `develop` branches with an upstream that no longer exists show up in `git branch -vv` as `[origin/develop: gone]`. ## Step 5 — fix everything that named develop This is where migrations actually go wrong. Search for the name: hook scripts that check the pushed ref, any configured refspec that mentions `develop`, and aliases in config. Update the remote's HEAD so new clones check out the right branch — `git remote set-head origin -a` updates `refs/remotes/origin/HEAD` in your clone from what the remote reports, and `git clone` follows the remote's HEAD when deciding what to check out. Conventions change too. Tagging now happens on `main` rather than at a release-branch merge, and `main` receives every change rather than only releases, so its `--first-parent` log becomes a change-level log instead of a release-level one. Anything that read version history from that log needs revisiting. ## Step 6 — sequence it with the team Pick a low-traffic window, ask everyone to push or merge outstanding work first, and announce the exact moment `develop` disappears. The technical steps take minutes; the coordination is the whole risk. A single person who was on holiday and returns to a rebased world is the normal source of post-migration mess. ## What goes wrong Deleting `develop` before folding it into `main` strands commits. Leaving it in place "just for now" is worse than not migrating: people keep merging into it, and you now maintain two models. Forgetting the hooks and refspecs produces failures that look random. And the subtlest one: if the team keeps branches long-lived after the migration, they have paid the cost of moving without gaining the benefit, because the benefit was never the branch count — it was the short merge base that the new model makes possible.
- Why prefer merging main into a shared feature branch instead of rebasing it onto main?Rebasing rewrites the branch's commits into new ones with new hashes. Anyone who already fetched the old commits then has a branch that has diverged from the new one, and reconciling that is manual and error-prone. Merging leaves existing commits untouched, at the cost of a merge commit — the right trade for a branch other people hold.
- What is the checkable condition that develop is safe to delete?`git rev-list --count main..develop` returns 0, meaning every commit reachable from develop is reachable from main. `git branch -d develop` enforces essentially the same check and refuses otherwise; using `-d` rather than `-D` keeps that safety net in place.
- After the migration, what changes about reading main's history?main now receives every change rather than only release and hotfix merges, so `git log --first-parent main` becomes a change-level log instead of a version-level one. Anything that derived the release list from that log must switch to reading annotated tags, for example via `git tag` or `git describe`.
- How do you find clones and branches still tied to the deleted branch?`git fetch --prune` removes stale remote-tracking refs, and `git branch -vv` then shows any local branch whose upstream is gone. Grepping hook scripts and CI-facing config for the literal name catches the rest; the failures those cause otherwise look intermittent rather than obviously related.
saying these in an interview costs you the question
- Deletes develop first and reconciles history afterwards
- Rebases shared branches onto main without warning anyone
- Assumes main already contains everything develop has
- Forgets hooks and refspecs that hardcode the old branch
- Keeps develop around indefinitely alongside the new model