← Misc

Multiple-branch repos

Multiple-branch repos

Sometimes we must maintain multiple versions of an artifact concurrently. This is most common in libraries when we make breaking changes, or when one of our dependencies makes a breaking change. It is not always possible to upgrade all clients at once, so we need to maintain multiple versions until everyone has migrated off the legacy version.

Branch structure

A solution to this is git branching: earlier versions of the library are maintained on a series branch. We do breaking development on main, and create a series/* branch for every legacy version we support. Assuming semantic versioning, we name the series branches after the major version:

% git branch -r
  origin/main
  origin/series/6.x
  origin/series/7.x
  origin/series/8.x

Flow of changes

Code can originate on any branch, but it should flow in one direction, from the earliest series branch to main:

Diagram of branch flow

Where to initiate new pull requests

  • If your language has binary dependencies (e.g., Scala and *.jar), binary-breaking changes must start on main.
  • If the change is merely source-breaking, it should probably still go on main, but you have some wiggle room.
  • Otherwise, it’s up to you. Starting on a series branch is more work for the maintainer, but spreads your work to more clients faster and makes you a nice person.

Merging forward

Congratulations. You’ve fixed a bug on series/6.x, and refreshed a couple of its dependencies and merged your pull request. All aboard the merge train!

  1. Fetch the latest. We’re not pulling any local copies. We’re just updating the remote references:

    % git fetch -a origin
    
  2. Create a new branch off our target to hold the merge:

    % git checkout -b merge-6.x-$(date -I) origin/series/7.x
    Branch 'merge-6.x-2021-06-29' set up to track remote branch 'series/7.x' from 'origin'.
    Switched to a new branch 'merge-6.x-2021-06-29'
    
  3. Merge the previous branch.

    % git merge origin/series/6.x
    Auto-merging project/plugins.sbt
    CONFLICT (content): Merge conflict in project/plugins.sbt
    Auto-merging project/build.properties
    CONFLICT (content): Merge conflict in project/build.properties
    Auto-merging core/src/test/scala/com/banno/vault/transit/TransitSpec.scala
    CONFLICT (content): Merge conflict in core/src/test/scala/com/banno/vault/transit/TransitSpec.scala
    Auto-merging core/src/test/scala/com/banno/vault/VaultSpec.scala
    CONFLICT (content): Merge conflict in core/src/test/scala/com/banno/vault/VaultSpec.scala
    Auto-merging core/src/main/scala/com/banno/vault/transit/Base64.scala
    Auto-merging build.sbt
    CONFLICT (content): Merge conflict in build.sbt
    CONFLICT (add/add): Merge conflict in .github/workflows/ci.yml
    Auto-merging .github/workflows/ci.yml
    Automatic merge failed; fix conflicts and then commit the result.
    
  4. Question all life decisions that brought you to this point.

  5. Breathe. Most of them aren’t this bad.

    The two branches have a merge base, which represents a common ancestor commit between the branches. This step is not routine, but to help visualize:

    % git merge-base origin/series/6.x HEAD
    91ddf7341425932c378642083d12060fc83b730c
    

    You are at the frowny face:

    Unmerged branches

    The merge conflicts represent things that changed on both branches since this commit. Git has rewritten all conflicting files with intentionally jarring conflict markers. For example:

    <<<<<<< HEAD
    val http4sV = "0.21.20"
    val specs2V = "4.10.6"
    val munitCatsEffectV = "0.13.1"
    val munitScalaCheckV = "0.7.22"
    
    val kindProjectorV = "0.11.3"
    =======
    val http4sV = "0.21.24"
    
    val specs2V = "4.12.2"
    
    val kindProjectorV = "0.13.0"
    >>>>>>> series/6.x
    
  6. For each conflicting file, decide which to keep. Sometimes you want the top. Sometimes you want the bottom. Sometimes you want both. Sometimes you want to do something else entirely. These decisions are why you make the big bucks. Add the files as you go:

    % git add build.sbt
    

    You’re done when all the <<<<<<<, >>>>>>>, and ======= are done and your tests still pass.

  7. Everything green again? Great! Commit:

    % git commit
    [merge-6.x-2021-06-29 f6794c3] Merge branch 'origin/series/6.x' into merge-6.x-2021-06-29
    

    Merged branches

What have we accomplished?

We’ve advanced the merge base! Like the previous merge-base, we can skip this step. But we can demonstrate that it changed:

% git merge-base origin/series/6.x HEAD
4c7404d63c9f286677655aeb987be33ebe2709db

Who cares? The next person to merge forward cares! Instead of redoing all the work since 91ddf73, the next merge train leaves from 4c7404d.

New merge base

Publishing the change

  1. Push it:

    % git push origin merge-6.x-2021-06-29
    remote:
    To github.com:banno/vault4s.git
     * [new branch]      merge-6.x-2021-06-29 -> merge-6.x-2021-06-29
    
  2. Open a pull request. Be careful to select the right base (left dropdown)!

    Base is series/7.x

This hurts my soul

It hurts all our souls, but we can cope together:

  • Avoid breaking changes. Can that breaking change be a deprecation and save a major version?
  • Prune old versions. Is it easier to help another team upgrade than it is to drag series/2.x behind you for the rest of your days?
  • Minimize git noise. The less branches diverge, the less space to conflict.
    • Turn off vertical alignment in your code formatters. It looks nice, but expands the merge conflicts.
    • If your language supports them, trailing commas are your friend.
    • Postpone your big sweeping reformats for when you’re back to one branch.

Tools to ease the pain

There are many ways to do this. Please add yours!

Editors

Emacs

I’m feeling lucky

You tested locally. You haven’t been through CI yet, but you ran the tests locally, and, heck, everyone’s busy. Skip the PR:

% git push origin merge-6.x-2021-06-29:series/7.x
To github.com:banno/vault4s.git
   8b8d77f..f6794c3  merge-6.x-2021-06-29 -> series/7.x

Things to ponder before taking this shortcut:

  • Is it Tuesday at 10am or Friday at 4:45pm?
  • Does it quietly publish a snapshot or loudly continuously deliver?
  • Will an auditor ask you who signed off?
  • What’s your team policy?
  • Was it a trivial merge or a new creation that resembles neither parent?

Be it on your head if it breaks. When in doubt, follow the PR flow.

Further reading