5  Branching Strategies

Defining a branching strategy can be tricky and requires identifying the needs of each repository. While there is no best branching strategy, this part provides a few examples of what could be considered as potential branching strategies.

There is no need to define one single branching strategy for all of the repositories of a company or institution. The choice should be made by type of activity.

TipAbout branches

This guide explains what a branch is and how it works.

5.1 Basic git structure

A Git repository is typically organized around a primary branch, main, from which feature branches are created. These feature branches isolate development work and are eventually merged back into main through a pull request.

flowchart TD
    main["Main branch"]
    feature-1("Feature 1 branch")
    feature-2("Feature 2 branch")
    PR1>"Pull request"]
    PR2>"Pull request"]
    main --> feature-1
    feature-1 --> PR1
    PR1 --> main
    main -.-> feature-2
    feature-2 -.-> PR2
    PR2 -.-> main

    linkStyle 0 stroke:#0066FF,stroke-width:2px
    linkStyle 1 stroke:#0066FF,stroke-width:2px
    linkStyle 2 stroke:#FF6600,stroke-width:3px

    linkStyle 3 stroke:#0066FF,stroke-width:2px
    linkStyle 4 stroke:#0066FF,stroke-width:2px
    linkStyle 5 stroke:#FF6600,stroke-width:3px
Figure 5.1: Basic Git branching structure

This branching strategy can be used in the case of a team working on a specific activity of a single clinical study within a repository.

It could potentially be used for a study or a product with several distinct activities using tagging releases.

5.2 Usage of a devel branch

A devel branch is usually a development branch located between the main and other specific development branches. It is used to integrate multiple features and updated before all of those are merged with the main branch. The main objective is to ensure stability of interdependent updates. It is possible that two different features, which work fine in separate branches, may cause conflicts when merged into the same branch. The devel branch is used to integrate all updated features and make any necessary final adjustments before merging into the main branch. It works as a safety net before the final merge to main.

flowchart TD
    main["Main branch"]
    devel(["Devel branch"])
    feature-1("Feature 1 branch")
    feature-2("Feature 2 branch")
    resolve["Resolve conflicts"]
    pr1>"Pull request"]
    pr2>"Pull request"]
    main --> devel
    devel --> feature-1
    devel -.-> feature-2
    feature-1 --> pr1
    feature-2 -.-> pr2
    pr1 --> devel
    pr2 -.-> devel
    devel --> resolve
    resolve -->|"Resolve conflicts"| devel
    devel ==>|"Merge to main"| main
    
    linkStyle 0 stroke:#0066FF,stroke-width:2px
    linkStyle 1 stroke:#0066FF,stroke-width:2px
    linkStyle 2 stroke:#0066FF,stroke-width:2px
    linkStyle 3 stroke:#0066FF,stroke-width:2px
    linkStyle 4 stroke:#0066FF,stroke-width:2px
    linkStyle 5 stroke:#0066FF,stroke-width:2px
    linkStyle 6 stroke:#0066FF,stroke-width:2px
    linkStyle 7 stroke:#FF6600,stroke-width:3px,stroke-dasharray: 10 5
    linkStyle 8 stroke:#FF6600,stroke-width:3px,stroke-dasharray: 10 5
    linkStyle 9 stroke:#FF6600,stroke-width:4px
Figure 5.2: Devel branch structure
flowchart TD
    main["Main branch"]
    devel(["Devel branch"])
    feature-1("Feature 1:<br>Remove<br>ADAE.ARELGRP1")
    feature-2("Feature 2:<br>Update ADSL<br>(still uses<br>ADAE.ARELGRP1)")
    pr1>"Pull request #1"]
    pr2>"Pull request #2"]
    conflict["Conflict detected<br>after merging<br>both features"]
    resolve>"Resolve conflicts<br>in devel branch"]
    
    main -->|"1. Create devel"| devel
    devel -->|"2. Create"| feature-1
    devel -->|"2. Create"| feature-2
    feature-1 -->|"3. Merge"| pr1
    feature-2 -->|"3. Merge"| pr2
    pr1 -->|"4. Merge to devel"| devel
    pr2 -->|"4. Merge to devel"| devel
    devel -.->|"5. Conflict appears!"| conflict
    conflict -.->|"6. Resolve"| resolve
    resolve -.->|"7. Apply fix"| devel
    devel ==>|"8. Merge to main<br>(conflict resolved)"| main
    
    classDef conflictBox fill:#FFE5E5,stroke-width:2px
    classDef resolveBox fill:#FFF4E5,stroke-width:2px
    classDef develBox fill:#E5F3FF,stroke-width:2px
    
    class conflict conflictBox
    class resolve resolveBox
    class devel develBox
    
    linkStyle 0 stroke:#0066FF,stroke-width:2px
    linkStyle 1 stroke:#0066FF,stroke-width:2px
    linkStyle 2 stroke:#0066FF,stroke-width:2px
    linkStyle 3 stroke:#0066FF,stroke-width:2px
    linkStyle 4 stroke:#0066FF,stroke-width:2px
    linkStyle 5 stroke:#0066FF,stroke-width:2px
    linkStyle 6 stroke:#0066FF,stroke-width:2px
    linkStyle 7 stroke:#CC0000,stroke-width:3px,stroke-dasharray: 5 5
    linkStyle 8 stroke:#FF6600,stroke-width:3px,stroke-dasharray: 8 4
    linkStyle 9 stroke:#FF6600,stroke-width:3px,stroke-dasharray: 8 4
    linkStyle 10 stroke:#FF6600,stroke-width:4px
Figure 5.3: Devel branch structure with conflict resolution

A devel branch is very useful in the following cases:

  • Several different people are working at the same time on different features

  • The features can present interdependencies

In these cases, the devel branch will serve as a convergence point for the different features and reduce the integration time when merging into the main branch. The conflicts are detected early before being presented into the main branch. A devel branch also reduces the risk of having late conflicts when features are merged late to main (everything is merged in devel first). Resolving all conflicts in the devel branch will also make sure to have a clean main branch.

In the case of a single individual or a small team working together, or when tasks have no interdependencies, the devel branch could be avoided.

An example of workflow using a devel branch is detailed at this link.

5.3 One main branch duplicated per milestone

Across a clinical portfolio, multiple studies and milestones can share a common set of base programs, while some milestones require additional specific code. One approach is to store all programs in the main branch and create a dedicated copy when finalizing a milestone.

flowchart TB
    main["Main branch"]
    csr(["CSR"])
    dmc1(["DMC #1"])
    dmc2(["DMC #2"])
    main -.-> csr
    main -.-> dmc1
    main -.-> dmc2
    flag_csr[("Tag #1")]
    flag_dmc1[("Tag #2")]
    flag_dmc2[("Tag #3")]
    csr -.-> flag_csr
    dmc1 -.-> flag_dmc1
    dmc2 -.-> flag_dmc2
Figure 5.4: Main branch duplicated per milestone structure

Several possibilities can be applied. The most common is to use a tag release on the main branch to identify the milestone. It will save the main branch and clearly identify it as a milestone reached. This link provides more details about tagging releases on GitHub. Similar actions can be performed on other source code management platforms.

Another way is to duplicate the main branch and use a flag to identify the milestone, with the possibility to lock it. A branch can be locked using a protection rule (see #13).

5.4 One main branch per milestone

A single study often needs to deal with different activities, sometimes happening concurrently. One potential way to use git in these cases could be to have:

  • 1 repository for the study

  • 1 main branch per activity (CSR, DMC #n, DSUR, etc)

This branching strategy would allow for working separately on each activity, keeping them intact, but also to work on both at the same moment if needed. The following examples shows a repository dedicated to a single study with the 2 following main branches:

  • CSR: for daily ongoing activities related to the CSR preparation

  • DMC #1: specific milestone happening soon

Two issues are handled at the same moment. Issue #1 is dedicated to an efficacy endpoint that should not be part of the DMC, while Issue #2 is strictly dedicated to the DMC activity and will not be part of the final CSR outputs.

flowchart TD
    csr["CSR"]
    dmc["DMC"]
    branch-1("Branch #1")
    branch-2("Branch #2")
    pr1>"Pull request"]
    pr2>"Pull request"]
    csr --> branch-1
    dmc --> branch-2
    branch-1 --> pr1
    pr1 --> |"Merge #1"| csr
    branch-2 --> pr2
    pr2 --> |"Merge #2"| dmc
Figure 5.5: One branch per milestone structure

5.4.1 Common issue

If something common to more than one of the main branches should be made, it is possible to make as many pull requests as required. The only tricky part is to define the source branch. In this example, we take the CSR.

flowchart TB
    csr["CSR"]
    dmc["DMC"]

    branch-c("Common issue branch")
    prc>"Pull request"]
    prd>"Pull request"]

    csr --> branch-c --> prc
    branch-c -.-> prd    
    prc --> csr
    prd -.-> dmc

    linkStyle 0 stroke:#0066FF,stroke-width:2px
    linkStyle 1 stroke:#0066FF,stroke-width:2px
    linkStyle 2 stroke:#0066FF,stroke-width:2px
    linkStyle 3 stroke:#FF6600,stroke-width:3px
    linkStyle 4 stroke:#FF6600,stroke-width:3px
Figure 5.6: Common issue branch structure

5.5 Hot fixes

Hot fixes are urgent corrections that need to be applied to a specific milestone that has already been released or tagged. These fixes are typically critical bugs or issues discovered after a milestone has been finalized and need immediate attention.

It should focus only on the resolution of the critical issue, and prevent it from happening again. It should be clearly documented and tested, even given the urgency of the fix.

Hot fixes are essential for maintaining the integrity of released milestones while allowing development to continue on the main branch without disruption.

This article gives more details about hot fixes and how to avoid them.

When working with milestone branches, hot fixes can be applied in different ways depending on the branching strategy used:

5.5.1 Hot fixes with tagged milestones

When milestones are identified using tags on the main branch, a hot fix should be created from the specific tag, applied, and then merged back to main. This ensures the fix is available for future milestones while also addressing the specific tagged milestone.

%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#87CEEB'}}}%%
gitGraph
    commit id: "Initial"
    commit id: "Milestone 1"
    commit id: "Tag: v1.0"
    commit id: "Continue dev"
    branch hotfix-1.0.1
    checkout hotfix-1.0.1
    commit id: "Hot fix"
    checkout main
    merge hotfix-1.0.1 tag: "PR: Hot fix"
    commit id: "Tag: v1.0.1"
Figure 5.7: Hot fix workflow with tagged milestones

5.5.2 Hot fixes with milestone branches

When using separate milestone branches (e.g., CSR, DMC), hot fixes should be created from the specific milestone branch, applied, and then merged back to that milestone branch. If the fix is also relevant for other milestones or the main development branch, it can be cherry-picked or merged to those branches as well.

flowchart TB
    milestone["Milestone branch<br/>(e.g., DMC #1)"]
    hotfix("Hot fix branch")
    pr>"Pull request"]
    milestone --> hotfix
    hotfix --> pr
    pr -->|"Merge hot fix"| milestone
    
    main["Main branch"]
    pr -->|"Cherry-pick or merge"| main
Figure 5.8: Hot fix workflow with milestone branches

This article provides more details about cherry-picking when using a devel branch.

5.6 Pros and cons

Many other strategies can be defined, and the choice depends on the needs of each study team. Also, some of the strategies displayed above can be combined (for instance using a devel branch with a milestone main branch strategy).

Here is a summary of the pros and cons of the different strategies presented above:

Strategy Pros Cons
Devel branch
  • Safety net before merging into main

  • Useful when multiple branches are active and merged at the same moment

  • Not as instantaneous merge into main, takes more time

  • Not very useful when a few people are working at the same moment on a project

Main branch duplicated
  • Only one branch to maintain

  • Useful when most of the files are common to several milestones

  • Files tagged for a milestone its do not belong to

    or

  • Need to identify and remove files when duplicating and tag the main branch

One main branch per milestone
  • Clear difference between milestones

  • No risk to have file located in the wrong milestone branch

  • Useful when milestones have really different files to run

  • Several branches to maintain

  • Intermediate branches regarding files common to several milestone must be merged once for each milestone