If you've been programming for a while or worked in a shared codebase, you've probably seen or been involved with long-lived feature branches; branches that live on for a longer time without being merged, perhaps touching hundreds of files and thousands of lines.
Long-lived feature branches can be a result of bigger features being built in one go, and can be problematic because they have to be maintained side-by-side with the main branch. They can also be the result of stalling dependencies. For example, a front end feature might depend on a new back end endpoint that for some reason, mid-development, gets delayed.
Maintaining the branch side-by-side with the main branch either means rebasing it often (or merging main into it) to avoid merge conflicts, or dealing with a bigger merge conflict when the feature branch is finally ready to get merged. In either case, the main branch is a moving target. Doing the former means you might need to deal with unwelcome disturbances along the way. Doing the latter can be scary because the risk of failing to integrate the new feature properly increases. Not only that, long-lived feature branches, even if communicated beforehand can come as an unnecessary surprise to the rest of the team when they are merged.
Personally, I don't need long-lived feature branches, and I see them as a symptom of poor development practices by my own hand. I try to merge daily, because when I fail to do so, it's often a result of:
- Failing to distinguish between (a) building preparations for the new feature, (b) building the new feature itself and (c) integrating the new feature with the existing system.
- Not implementing changes in a backwards compatible way.
- Not using atomic commits and instead saving many or all changes in non-descriptive, incoherent commits.
Avoiding long-lived feature branches all comes down to doing a bit of planning, working structured with git and commits, and keeping the main branch clean. When I succeed, it becomes natural to continuously split up my work into smaller pull requests that can be merged separately.
For example, building a new feature might require that I extend the design system with new colours or make some changes to shared UI components to support new use cases. Maybe I also find and fix a bug in an existing function that I need to reuse. These changes happen as I work on the new feature, but they are not a direct part of it. And because they are applied in distinct commits, they can easily be cherry-picked to a new branch that can be merged without friction. All there's left to do is rebase my old branch on main.
Even if I'm building a big new feature over several days, I can keep finding ways of splitting it up and build on top of the current state of main. Until the inevitable time comes when I need to merge the feature itself. This is often the step of “integrating the new feature into the existing system”. Remember that back end endpoint that might be delayed? Well, as long as I put my feature behind a feature flag then there's no problem. A feature flag can come in many forms. Maybe we ping the new endpoint, and if it responds with an error, then we don't enable the new feature? Maybe I built a whole new page that can just be hidden by not including a link to it anywhere? Or maybe the feature can be toggled by one or more if statements that look for a query param in the URL?
The point is, it doesn't have to be that complicated to avoid long-lived branches and keep the main branch clean at the same time. Keeping the main branch clean means, even if someone decides to cut a release on main today, then my half-baked feature can safely go out without revealing itself or jeopardising the system. In fact, as of writing, I have an unfinished feature in production right now, that I've just hidden behind what is equivalent to an if statement. I didn't plan for it to go out, but I don't mind either.
There might be a few good cases for long-lived feature branches, but I'm sure there are plenty more reasons to want to merge continuously to main:
- Merging often helps you avoid those merge conflicts.
- It avoids the need of making a “big bang” merge which can be risky or come as an unnecessary surprise to team members (perhaps even breaking their code in their local branch?)
- You make progress even if your feature gets delayed.
- You are able to continuously merge preparations that are tangential to the new feature.
- You can share progress with the rest of the team.
- You are able to test early.
And most importantly: It feels good and it feels right. Instead of loathing git, I embrace it. Sure, it's not the easiest tool in our toolbox, but it's definitely not the hardest to learn either. I love that I can work in a structured way, and I love that it's not hard for me to continuously commit, merge and ship.
I keep seeing long-lived feature branches, and I always think, what a drag it must be to be the driver behind it.
I keep seeing developers hate on git, and I always think, what a drag it must be to not have this tool in your toolbox.