> Often there is a main trunk, and then branches that were made for particular product variants (like piece of hardware or whatever)
I worked at a place that did this.
The code was written in C, and I always thought the better solution would have been to use #define/#ifdef to flag certain blocks of code out of the compilation.
A branch for each product was a nightmare when there were 10+ products, some with multiple variations, each on its own branch. Backporting a bug fix meant cherry-picking into 20+ branches. What made it especially stupid was office politics from each product having its own PM, and then the PM for one of the products would decide the bug wasn't significant enough to spend the time doing the cherry-pick and testing. This happened too often when it came to security fixes when a PM didn't understand the issue.
Yes; #ifdef and #endif is basically branching, but it's in one branch of the CM system.
The benefit that everything is integrated, so there are no games with having to cherry pick things this way and that and losing fixes.
The apparent downside is that there is no isolation. Inside some of those #ifdefs is code that you are not building. But changes you are making can break that code for someone else.
While this may seem risky, it's actually better. Breaking something now is better than someone cherry picking your fix 8 months later into their branch and then dealing with the breakage.
Fixes to common code never get left behind; everyone working off the trunk instantly gets them.
The #ifdefs are immediately and constantly visible, telling you where the code is that is or is not part of what you are doing, and reminding you of its existence. They greatly discourage refactoring parts that you cannot test.
There is pressure to keep those #ifdefs clean, whereas people go hog wild when they have their own branch, thinking they can rewrite whatever they want to suit what they are doing.