81 comments

[ 11.5 ms ] story [ 2219 ms ] thread
This is the answer for anyone who isn't maintaining something like Windows, with multiple versions of product simultaneously for years. If you are cutting releases of a web service, cherry pick into short lived release branches.
(comment deleted)
I agree.

If you need to move a fix into multiple branches, isolate that fix in its own branch to begin with. The entire branch is the cherry. Each target branch deserves its own merge because chances are, seeing as the thing is so critical, the merge needs adequate attention each time to get done correctly (causing more work and friction is sometimes healthy).

If you're reaching the end of your sprint and your branch isn't ready to merge, chances are the entire branch could use another sprint in the oven. You've completely run out of time and you want to isolate a few changes, that's great if you want to move fast and break things.

In TFVC (and SVN, CVS, Perforce, etc.), branches are expensive. It has taken me months to "unteach" people at work antipatterns that arise due to that. Management still doesn't get it - there is this holdout perspective that branches have to be micromanaged. This was the only sensible way to manage the mess caused by TFVC+co.

If you're finding unusual degrees of friction with Git (Mercurial, Bitkeeper, etc.), chances are that you're artificially introducing issues caused by your legacy version control and trying to solve them. Start forgetting and unlearning.

SVN branches are not expensive. They use a copy-on-write.

http://svnbook.red-bean.com/en/1.8/svn.branchmerge.using.htm...

Good luck using hard links on Windows without accidents.

Apart from sarcasm, SVN branches use suffer-on-merge: creating a branch means immediate technical debt of the most useless kind, which is more expensive than any inefficient file copying.

From the same manual:

"To perform a sync merge, first make sure your working copy of the branch is “clean”—that it has no local modifications reported by svn status."

"One special kind of flexibility is the ability to have a working copy containing files and directories with a mix of different working revision numbers. Subversion working copies do not always correspond to any single revision in the repository; they may contain files from several different revisions."

(comment deleted)
Every SVN branch costs money, any system that cares about branch ancestry is expensive.

If me and Jim are working on two features on the same release and then I find that I need to integrate Jim's work in order to continue mine, Jim will need to merge his [incomplete] work into a common ancestor. If I am working on a feature in a completely different release, this "merge path" gets long and begins to cost money and carry risk. Jim hasn't finished his work yet; before any release can happen, Jim has to finish his work and merge the complete changes to all affected branches. If we can't wait for him to finish, then we're rolling back his changes and, oops, someone rolled back the same change on multiple branches.

Now we can't launch a hundred features because there was an interdependency between two.

Merges is the stupidest thing there ever is in the land of source control. Contrary to the author I struggle to find even a single use that would make merge merited.

My main point is that merges are not easily computable. comm3 = merge(comm1, comm2) is not a kind of function you can run and see for yourself. Instead it is a kind of hand wavy magic in which we declare that those two apples plus a lemon equals three mangoes. You throw the beauty of hash tree out of the window like an overdue christmas tree.

What's the alternative? Only work on master and only on one document at a time?
Rebase everything so you get a clean timeline without meaningless "merge of bla" commit messages. Can be made mandatory by turning your repo fast-forward-only.
Isn't that just a merge, but done one commit at a time?
No it is not, because there is no mystic handwavy commit involved. You just apply a patch basically.

Much less this handwavy commit exposed to uninterested parties.

GP's comment was that a _rebase_ is a merge, one commit at a time, due to replaying each commit over whatever was there.

Although you still fix the conflicts each time, it means you don't have many merge commits as you go along

I'm really bad at rebase, but doesn't rebasing a branch A onto branch B, remove all the history from A?
Not necessarily. That only happens if you also squash your branch first.

Most people do that though because they don't want potentially unbuildable commits on master. And the value of keeping around WIP commits from a feature branch is questionable.

Feature branch has to be a sequence of buildable commits that explain the feature.
Rebase has a lot of options, including squashing many commits into one or more "good" commits. This is usually a good thing if you have commits that are "work in progress" commits. You can use squash/skip to avoid having commits in your history that break the project or feature, which is great if you use git bisect.

The major downside of rebase is that even if you don't squash/skip it changes the hash of every commit. This is very problematic when others have ever checked out your branch locally, or made commits that haven't been pushed. It takes greater communication between the team in my experience.

> It takes greater communication between the team in my experience.

You can also, as a substitute for greater communication, establish a protocol around branch naming and follow it.

What works for us is using the word "release" and "wip" in our branch names. If a branch is a "release" branch, then it is safe to base other work on it. If a branch is a "wip" branch, then it is not safe for merging upstream.

These two are not exclusive. For example, you might have a branch "dev" and a branch "release", and maybe another branch "release-dev-wip" – these all have different properties. "release-dev-wip" is a short-lived merge target for features that might not be completed yet. It is not safe to merge this upstream, unless you've checked with all of your colleagues who merged feature branches to it, and they all certified that it is no longer "Work In Progress."

The key I think to make this protocol work is to distinguish between "feature branches" and "environment branches" – dev is an environment branch, and a permanent one, so it should not be rebased. Feature branches merge back to environment branches, and environment branches are deployable.

You can break this rule, say if someone hotfixes master, which is upstream from dev... but it should probably be an exception to do this, and not a regular occurrence, as many people may have already based their work on "dev" and they will all need to rebase on the new (rebased) dev, in order to get a clean merge later. This is where the communication is not always optional. It might be a better choice, if the hotfix is unavoidable, but the project is large and this type of communication is logistically impossible, to merge in reverse (checkout dev, then merge master – back to dev). It might be ugly, but it's considerate. Either way, there should be a clear protocol and no ambiguity on the matter of whether you have a feature branch or an environment branch when you hold it in your hand. Feature branches represent work, environment branches probably ought to just combine the work, and maybe keep a record of how it was deployed.

The branch "dev-wip" is a temporary environment branch – it might be deployed to a dev environment for example, but you should not expect it to remain permanently in the git history. At some point, perhaps it will be renamed to describe the features it contains, and then rebased and merged back to dev. If you merged your feature to it, you might expect that you will need to keep the feature branch around, so you can rebase it on "dev" or "master" later, and finally merge it back.

The whole branch might not get merged upstream at once. (You can also call it "release-dev-wip" and then, the person who looks at it will know that it may contain some completed features that for some reason were not ready to merge upstream, but perhaps should not be discarded entirely. I personally like to rebase wip branches on their upstream before discarding them, just to be sure I'm not throwing away someone's work that they may have thought they merged.)

Protocol is just a different form of communication that is done up-front. If you decide on a protocol and forget to explain it to your team before you implement it, you will obviously not have solved any problems. It's also important to be clear and confirm understanding, so that you can be sure nobody is imputing meanings that you didn't intend. Some teams might choose to only do prod deploys from the "release" branch, and that anything in the "master" branch must be safe to merge to release and send off to production. You could easily get yourself into trouble if you didn't understand when your team expects to work this way. Some teams might prefer to organize their releases on a "release" branch, and then use Continuous Delivery to trigger prod deploys whe...

I highly recommend being less bad at rebase - it's one of git's best features and actually pretty simple when you get the hang of it.

Being able to completely rewrite a feature branch at will before merging it (or rebasing or cherry picking or whatever) to master lets you go full on XCKD[1] with your commit messages but still compress them down to 1-2 logical "add change 1" "fix bug 2" commits at the end.

Thinking about commit messages mid-flow isn't worth it ;)

[1] https://xkcd.com/1296/

>I'm really bad at rebase

Yeah, I hate rebase, but the git boys (a very clever bunch) seem to like it.

Take a look at `git rebase --preserve-merges`

There are three ways to rebase that you should learn, and I only have 5 minutes, but I'll try to sum them up quickly:

rebase -i: This takes a single upstream (parent) commit as an argument and assumes you want to change something, or everything after that. That was the most important part to understand... rebase assumes you have some stable base, and that is the commit that you want to keep. (You can still squash other commits into it, or rewrite the message, but if you want to remove it altogether, you should pick the commit before it as the stable base.)

Say you have a commit where you tried something, and then three commits that follow which shored that up into a working something. You may want to use rebase -i and squash those small commits, to make one good commit. (You might also use "fixup" to erase those commit messages about fixing something, because this time you're just going to deliver one commit that works. As a reviewer, I often tell my coworkers that while history is nice, I don't need to see every little thing you tried if it turned out that simply didn't work. If you want me to review your commit history, you should review it too, and feel free to clean it up first before sending it over to me!)

git rebase [original-base] --onto [new-base]

This is like cherry-pick for branches. This is where `--preserve-merges` really shines. Say you have a branch that you merged another branch into, and you merged that branch into your wip-release branch, and it still isn't finished, so you want to preserve all of that history when you rebase it ahead of master, where someone else has already merged their changes in before you. The tool for this is rebase --onto. Imagine you pruned the tree, leaving the node where your work started "original-base" intact, and grafted the branch containing your work that you trimmed onto "new-base". This almost has the argument semantics of a tool like "mv" -- mv [from-file] [to-file].

Then last but not least, git pull --rebase

Someone has merged into your upstream (say, master) ahead of you, and you don't have any interesting merge history that you want to preserve. You just want to pretend you started your work after they finished theirs, and any empty merge commits you might have accumulated since then can be discarded, because it's really just one feature branch you're working on. (When you get good at working this way, you might actually find that you merge more often simply because it's so easy to discard the merges when they are no longer needed.)

You may want to do this sooner than later, because you suspect their will be a merge conflict with their work, and you want to resolve it now, while their work and their merge is fresh in their head... so you can ask them what they were thinking, and this merge won't be looming over you for the duration of your feature branch. Depending on how you work, feature branches or wip-release branches could live for a duration of several weeks or longer. Pull rebase works like `git pull` in how it resolves merges, except it takes your commits and applies them one at a time, rewriting each commit to resolve the merge. This strategy won't generate any new merge commits.

OK, one more! This one isn't a rebase, but you'll probably want it if you are using all of the above... "merge --no-ff" – once you are an expert at rebasing, you are probably going to miss those clean merge commits, because by default all of your merges are going to be fast-forwards, and there's actually some merit to the idea of having a merge commit that shows "here, a branch was ended." Basically every Github or Bitbucket will do this for you when you merge a Pull Request without choosing the "squash" option. It's handy to have in your command-line toolbox as well. Your co-workers will know that they can base their work off of yours and they c...

Take a look at `--rebase-merges` if you haven't. It's new and kind of amazing. Works much better than `--preserve-merges` in my experience.
Thanks! I meant to mention that --preserve-merges is not really for use with interactive rebase, and does not work at all with pull --rebase, but I was in a bit of a hurry.

I did not know about rebase-merges!

> The --rebase-merges mode is similar in spirit to --preserve-merges, but in contrast to that option works well in interactive rebases: commits can be reordered, inserted and dropped at will.

Make small commits on feature branches and cherry pick onto the master branch regularly. Rebase the feature branches on the master branch regularly. If your commits are small (1 to 50 lines outside of unit test code) at most you'll only have a few lines of merge conflicts.

It's always possible to make smaller commits, and it's always possible to cherry pick onto master regularly. If you're not doing so, you're being lazy.

This style has more benefits than easier merges. Smaller commits are easier to test and less prone to bugs. Also less likely to be affected by context switching.

If you got a long-lived branch is extremely cumbersome to rebase that everytime you need something from master, especially if you are not the only developper on it.

Maybe our stories are too big, but a feature branch in our company lives at least 3-4 weeks with 4 developers working on it.

The only time I use rebase is when I got local commits, and I want to pull changes from my team mates also working on the same branch.

More everyone rebases - less cumbersome it gets. People start structuring their code across time, not just using the commits as dumps.
Yep exactly, also makes the code easier to test, understand, and less susceptible to loss of time due to context switching (since you can read those 20 lines of code you wrote and remember where you were up to in a shorter time).
It's okay to have a long lived branch. But you should be cherry picking commits from it to master regularly and rebasing on master even more regularly. It's always possible to do so, and I'm happy to explain how it can be done if you think I'm wrong.

If you cherry pick to master regularly, rebases on master are never cumbersome.

> cherry picking commits from it to master regularly

Surely the whole point of a long-lived branch is to contain stuff that's not in master - ie things that are being deliberately kept out of it?

Sure, and that's what you can do. For instance, for a website the long lived branch can contain a commit adding a link to a new feature on the homepage, only cherry picking that aspect at the very end of development so that you can work on the feature in shadow.

The point is that you merge anything that is safe to merge, which is usually most of the code if you find a way to hide it or otherwise incrementally change things.

Huh, I’ve used this development pattern before, but never re-used the same branch after merging code into master. I’d keep the new feature behind a configurable flag so it could be enabled in local or dev environments. Multiple branches might get created and merged to master over time. The feature would be tested in the dev environment (and maybe on a subset of users in prod), before the flag is flipped to enable the feature in prod.
That's actually my preferred way of doing it too, I was more explaining how a long lived branch can be useful while still not having to deal with merge conflicts when using cherry picking.
I agree, I too think merges are kinda weird, but maybe it's just you and me having a different mental model.

I like small, atomic commits that do just one thing. I frequently rebase onto master whenever I'm working on a feature. When my branch is good to be taken into master, I do a fast-forward merge (basically a commit by commit patch application); no magical merge commit from nowhere. Of course, FF merges are possible only when you're rebased.

Things people not used to this workflow might think will be a problem:

>Conflicts when I rebase from master

Solve them. They're going to happen anyway when you merge into master, so it's easier for everyone if you resolve your conflicts then and there.

>Multiple people need to work on the same feature branch

Sure, do your work locally. Before pushing, fetch the remote branch and rebase onto it as well. Resolve conflicts and your work history will be pristine.

Ask everyone on the team to do the same and nobody will have to resolve somebody else's merge conflicts (because you're the best person to resolve your conflict).

What do you mean by 'beauty of hash tree'? If you are worried about FF merge, you might be right but no-ff is there as best practice for most of the teams so nothing is lost.
Beauty of hash tree is that hash(commit) = hash(parent commit) ∘ hash(changes). You could compute that by hand if you wanted. But you can't compute merge by hand since it involves basically rebasing commits to a different tree which is non-trivial and prone to introducing errors. If there is 3-way merge you add some changes on top of that.

I don't know what's FF merge, what's no-FF and what you do when your FF merge turns into no-FF.

I only cherry-pick when it's the only commit of a feature branch that's going to be removed anyways. If development continued in that branch, I would merge master back into the feature branch again, or rebase the feature branch on master.

Not sure why, but I always knew from the beginning that cherry-picking is something not to be done from a live in-development feature branch without taking steps afterwards.

Stop excessive branching if you can use feature flags.
This is only a good idea if the things you're working on are isolatable "features", and you have an explicit process for garbage-collecting old feature flags. Otherwise the combinational explosion of possible flags kills your testing over time.
In addition to flags for functional changes, I'd say "design refactors to be incremental". It takes some care and thought, but almost every refactor I've seen can be framed as small steps that can each be immediately integrated into master. For example, when porting code from Python 2 to 3, you can change individual files to be 2/3 compatible until the whole codebase is, then flip the version without touching the source code, then get rid of 2/3 compatibility code. This tends to be safer and more reviewable than trying to rewrite the world on a separate long-lived branch and then land it all at once.
I would normally rebase the feature branch onto master if I need fixes from master and no one else is working on it. Usually this is the case - one developer works on each feature in a feature branch until it becomes a Pull / merge request and gets commented on and eventually merged.

I will sometimes make a fix in a feature branch that should go onto master before the feature is ready, and cherry pick it to a new bugfix branch. Once it's merged, if you rebase the feature onto master git will normally sort it all out, or you can use rebase -i to skip the cherry picked commit manually.

Rebasing is great for branches you haven't pushed that nobody else is working on. For that case, definitely rebase before you push.

If that is not the case and you have previously pushed your branch somewhere remote already, rebasing would end up rewriting history and things get a bit more tricky. You can still get away with this if it is just you.

Now here comes the crux: if you have a branch that you've been working on for several days, you really should be pushing it regularly to remote so that you don't lose your work. Also you should be creating lots of small commits and create a giant commit that touches everything. So if you then go and rebase, you are potentially rewriting history for someone. If that is at all going to be an issue, the better solution is to create a new branch and push that (or for Github folk, create a new pull request).

Many bigger projects will be configured to flat out reject rebased histories overwriting existing ones. Most of the public projects only allow you to create pull requests that have to be accepted by someone with commit rights on the main repo. And many of those people prefer clean rebased histories when they are merging so that they can do a so-called fast forward merge where the history stays nice and linear. If you have a branch with lots of back and forward merges, that is likely not going to go down well. It makes reviewing a lot harder and you may be better off creating a new branch and then rebasing the old one on top. Typically you'll have a bit of work re-solving all the conflicts you resolved earlier. This is a good argument against long lived branches.

Cherry-picking is sometimes useful but it potentially creates issues for the subsequent merge (fast forward or not). The nice thing with the pull request model is that it is not the problem of whomever cherry-picked but only that of whomever created the branch. If there is a conflict, it would be theirs to sort out before they can get their PR merged. If you are cherry-picking small fixes, this is usually not a big issue and a perfectly valid way of solving a small issue without landing a lot of work in progress.

Rebasing is perfectly fine for shared branches, all you have to do is pull —rebase and push —force-with-lease when changing branch history.

I do this at work for all the feature projects I work on with my team.

You mostly run into problems when people get confused about what is happening when they see the branch history has changed.

Git histories are wonderful and immutable so messing around with commits is quite easy if you know how to move branches around to undo changes if you mess up.

Git histories are only immutable if you don't rewrite them with --force. That's why it's called --force. It means you messed up and need to rewrite history. Nice to have the option but probably not great to make it a habit to mess up your history.

It's a feature that gets turned off server side in bigger projects and on OSS projects you don't get to push your changes at all and instead you need to work to get people to pull your changes. I'd argue that's a great way to isolate yourself from people misbehaving on their own branches since it is entirely their problem to get their branch in a a shape where it is straight forward to pull (as in fast forward merge). Rebasing can be a valid tool to get there. Force push is not thing generally in such projects except maybe on your own private fork.

This gets more complex when you have, for example, a volatile open source codebase as an upstream.

At work. we regularly merge upstream FreeBSD into our codebase, and run a suite of tests to make sure that we have not introduced a regression in performance or functionality. These tests take days to run, and roughly another day to analyze.

What causes a cherry pick is if we realize late in our release process that we're suffering from a bug that was fixed upstream (or that we can fix upstream). Since we're late in the release process, we don't want to take a full sync to the tip of the upstream master and consume potentially unstable changes, as that will mean re-running all our tests and resetting the clock on the release. So we cherry pick the fix (sometimes after committing it upstream ourselves).

Out of curiosity. What are you working on? A derivative of FreeBSD?
The Netflix CDN. We run a slightly modified version of FreeBSD.
(comment deleted)
Is this just the kernel, or the OS as a whole? This is probably a redundant comment, but maintaining a fork of a vendored product kind of defeats the purpose of using the vendored product. If at all possible I would recommend either to use the vanilla release, or fork it once and stop integrating. If you were shipping your custom FreeBSD to customers, and they were relying on it being FreeBSD plus your changes, I can see integration providing significant value. But since it's for a CDN, it's probably only being used by your team.
Are you working on the PS4 OS ?
(comment deleted)
Looking through the comments, seems like stacked diffs would serve better in a lot of these cases...
Some background on stacked diffs in case people aren't aware:

https://medium.com/@kurtisnusbaum/stacked-diffs-keeping-phab...

I've been using stacked diffs for years now after trying various other git workflows, and I love the freedom it gives me. I almost always structure my work as a single branch (not a separate branch per feature), one commit per code review, and I'm free to edit, reorder, split, and combine changes as I please (mostly with interactive rebase). When a commit gets accepted, I just move it to the bottom of my stack (if it's not there already) and push it. It makes it easy to split changes into a "pipeline" of logical steps that can each be independently reviewed and landed, with multiple steps in code review at once.

I hate it when people treat dependent code reviews as an "advanced feature". They're only advanced if you make your workflow so advanced that they're hard to keep track of (many commits per review, many branches, merge commits). If you keep it simple (one commit per review, always rebase), that simplicity gives you much more power in other areas that IMO are more important.

I just wish it wasn't so much of a pain to get working in GitHub.

Stacked diffs seems to be a rebase based workflow, so it has many of the same drawbacks as cherry picking.
You should only use merging to merge two divergent or unrelated histories. Merging should be a tool you use infrequently. GitHub has trained you to do it wrong.
Would you care to add more details? Are you saying that id you use some workflow like one branch / one feature, you should usually rebase?

Almost all branches have related history, it is very rare to have completely unrelated branches with separate starting points.

Github is respecting the original intention of git. For a long time it didn't even have cherry-pick functionality.
Citation needed. So far as I can tell from reviewing git's source code, cherry-pick was there since at least September 2005, after which the trail runs cold because of a big refactor. Git's first release was in April 2005.
Why wouldn't I want to merge?
It makes a merge commit which is usually unnecessary. If there were no conflicts, there's no need to have a commit fixing them up. If you just want to know who integrated the patch, git stores the author and committer fields separately.

Most git users in touch with the design goals of git use branches sparingly, either for long lived projects (with hundreds of commits) or as a sandbox in which work is frequently rebased into a concise history of a few patches, which are usually integrated upstream without a merge. Merges are also used to merge disparate trees, like when Linus merges in trees from around the kernel development scene (since each kernel subsystem maintains their kernel tree as almost independent projects, using merge makes sense to integrate the foreign histories into the upstream kernel). The Linux kernel approach probably requires a full-length article to fully explain, though, so don't read too much into that.

I would argue the "design goals" of git have evolved far past the kernel, which is a very unique and special project. These new patterns don't seem to invalidate how the kernel uses git and they offer a lot of ergonomics in a Github-styled atmosphere.

Are the only downsides that 1) users don't understand how kernel development is done, which extends far beyond source control; and 2) a merge commit is left as a marker?

It's not really about the kernel, the kernel is just where the world's most talented git experts work.

The downsides include the creation of a merge commit, sure, but also the fact that it's used as a crutch by users who would rather not strive to understand how git works.

Git uses a DAG for multiple reasons. Avoiding merge commits is ignoring most of the power of having a DAG for data structure. The Linux Kernel repo itself has some fascinating examples of DAG usage, including some of the most well known "octopus" merges with many more than two merge parents.
I don't think octopus merges or most merges in the Linux kernel are related to the point the article is making, or the workflow GitHub encourages. I'm not saying you should never merge, I said:

>You should only use merging to merge two divergent or unrelated histories.

Merge history can be useful history in source control. Rebases and cherry picks just hide merges, sweeping them under the rug, sometimes making them extremely hard to spot when they go wrong, and even harder to fix down the line when it causes the next merger to go wrong (as this article series works to show in explicit examples).

It's better to merge often and rarely if ever allow two branches to even have "divergent or unrelated histories".

The conflicts shown in the article are generally solved by rebasing and are not any different from a merge conflict. If the changes which caused them represent divergences in genuinely separate histories, then a merge makes sense.

>It's better to merge often and rarely if ever allow two branches to even have "divergent or unrelated histories".

Remember that git is distributed, not just nominally but also in workflow.

Merges record what actually happened, rebase rewrites history. Recording accurate history is not strictly necessary, but it's a useful thing inba vcs. Merge commits visibility in revision log is just an UI artifact.
Not only is it not strictly necessary to record an accurate history, but it's explicitly desirable to rewrite history. Effective git use starts with frequent and thoughtful rebasing. Merge commits are supposed to show up in the UI, for the reason you said - recording it when it happens. But it's only useful when it needed to happen, during the normal course of development this is not usually the case.
So what are the costs of preserving history that exceed the benefits? Benefits include at least (1) tracking down merge errors, (2) safety from the train wrecks when someone inevitably rebases and merges the same code, (3) additional resolution in the the actual history of the code as it happened including dead ends as failed experiments to learn from?

I can think of only one well founded case for wanting to rewrite history (instead of adding metadata to the history, such as merge commits and revert commits), and that is polisihng the history after the fact to hide what happened in the sausage factory. This may be a legit reason if you are not working in a safe environment or otherwise want to prepare for a future adverserial setting with some people who will see the history. A special case of this is when you accidentally commit a secret into the code, but that case has satisfactory technical recipes already.

If we want to improve the s/n in the log messages, a good avenue is better commit messages and metadata for the merges. For example in a github PR based workflow the main change note in each unit of work is the PR's description, and the individual commit messages are just minor notes along the way.

How else do you get bugfixes onto a release branch when the main develop branch has already moved on? Should you rebase your feature branch on the release branch instead, then merge it?
Exactly. This article seems to not really discuss that case. It’s specific to feature branches off a master line. Git rebase is generally what I use in feature branches.

Cherry-picking is important in the case that you mention, master to release, the opposite direction could be a merge, ie release branch to master.

Branch off the release branch for fixes to it. Then merge back into release and develop branch.
But what happens if it's a fix that was made to master. Then later, the boss decides that they need that change as a bugfix to the current release? How do you accomplish that without cherry-picking? Do you rebase the feature branch it was a part of and merge it?
They explain that in part 3, direct link:

  https://blogs.msdn.microsoft.com/oldnewthing/20180314-00/?p=98235
The technique is not new, in the Monotone wiki it is known as daggy fixes:

  http://wiki.monotone.ca/DaggyFixes/
Does anyone have any references / recommendations along the vein of "guide to branch strategies" for certain development methodologies? For example: working in sprints / AGILE, branches for testing/QA/user-acceptance environments, etc?

(While I would hope the answers to this question would be widely useful, I mostly work on C# applications, with less focus on JavaScript-based applications. Maybe that's relevant for deciding on strategy - hopefully not!)

I'd argue that language is less important than product/deployment type. The difference between releasing e.g. a desktop product on a yearly schedule with maintenance fixes going both to the 2018 and earlier main releases, versus building a web service with a single deployed version deployed perhaps several times per day is rather big when it comes to a branching strategy.
The most commonly referenced references to read can be found by searching for GitFlow (heavier) and GitHubFlow (lighter).

Personally, I recommend the GitHubFlow and CI/CD for "agile", and never branching per-environment but there are almost as many opinions as there are developers.

Hmm, I find cherry picking much easier to reason about than merges, but to each their own I suppose.
This sort of discussion makes me even more grateful for patch-based systems (Darcs, and possibly Pijul) than does having to use git for apparently simple tasks required to make contributions to things.