Branching Strategies and Workflows

Module 2: Version Control & Containerization

Introduction to Git Branching

In our previous lectures, we covered the basics of Git, including its history, installation, configuration, and core commands. Now, we're ready to explore one of Git's most powerful features: branching.

Branching is what sets Git apart from many other version control systems. It allows developers to diverge from the main line of development and work independently without affecting the main codebase. Think of branches as parallel universes where you can experiment safely before merging your changes back into the main timeline.

gitGraph commit commit branch feature checkout feature commit commit checkout main commit merge feature commit

In this lecture, we'll explore:

By the end of this lecture, you'll understand how to structure your workflow using branches to enable collaborative, parallel development while maintaining a stable codebase.

Understanding Git Branches

Before diving into strategies, let's ensure we have a solid understanding of what Git branches actually are and how they work under the hood.

What is a Branch?

In Git, a branch is simply a lightweight, movable pointer to a commit. When you create a new branch, Git just creates a new pointer—it doesn't change the repository in any other way.

Think of branches like sticky notes that you place on a specific commit. When you make new commits while "on" a branch, the sticky note automatically moves forward to the newest commit.

gitGraph commit id: "C1" commit id: "C2" branch develop commit id: "C3" checkout main commit id: "C4" checkout develop commit id: "C5" checkout main merge develop commit id: "C6"

In this diagram:

The HEAD Pointer

In addition to branch pointers, Git maintains a special pointer called HEAD. The HEAD pointer indicates which branch you're currently on. When you check out a branch, HEAD points to that branch, and when you make a commit, the branch that HEAD points to moves forward.

If you check out a specific commit instead of a branch (called a "detached HEAD" state), HEAD points directly to that commit rather than a branch.

Understanding the HEAD pointer is key to understanding how Git tracks your current state and how commands like checkout and reset work.

Basic Branching Operations

Let's review the fundamental branching operations in Git:

Creating a Branch

git branch branch-name

This creates a new branch pointing to the same commit as HEAD, but doesn't switch to it.

Listing Branches

git branch                  # Local branches
git branch -r              # Remote branches
git branch -a              # All branches (local and remote)

The current branch is marked with an asterisk (*).

Switching to a Branch

git checkout branch-name

This updates the working directory to match the branch and points HEAD to that branch.

In newer Git versions (2.23+), you can use the more intuitive command:

git switch branch-name

Creating and Switching in One Step

git checkout -b branch-name

Or in newer Git versions:

git switch -c branch-name

Deleting a Branch

git branch -d branch-name     # Safe delete (won't delete if unmerged changes exist)
git branch -D branch-name     # Force delete (will delete even with unmerged changes)

Merging a Branch

git checkout target-branch     # Switch to the branch you want to merge into
git merge source-branch       # Merge the source branch into the current branch

Renaming a Branch

git branch -m old-name new-name     # If you're not on the branch
git branch -m new-name            # If you're already on the branch you want to rename

Common Branching Strategies

Now that we understand the mechanics of branching, let's explore various branching strategies teams use to organize their development workflow. These strategies are like different recipes for managing collaborative development—each with its own advantages for particular types of projects.

1. GitFlow

GitFlow is one of the most well-known branching models, introduced by Vincent Driessen in 2010. It defines a strict branching model designed around project releases.

gitGraph commit branch develop checkout develop commit branch feature/login checkout feature/login commit commit checkout develop merge feature/login branch release/1.0 checkout release/1.0 commit checkout main merge release/1.0 branch hotfix/1.0.1 checkout hotfix/1.0.1 commit checkout main merge hotfix/1.0.1 checkout develop merge hotfix/1.0.1

Key Branches

Workflow

  1. Feature development happens on feature branches
  2. Completed features are merged into develop
  3. When develop has enough features for a release, create a release branch
  4. After testing and bug fixes, merge the release branch into both main and develop
  5. If bugs are found in production, create a hotfix branch from main
  6. After fixing, merge the hotfix into both main and develop

Pros and Cons

Pros:

Cons:

Example GitFlow Commands

# Starting a feature
git checkout develop
git checkout -b feature/user-login

# Completing a feature
git checkout develop
git merge feature/user-login
git branch -d feature/user-login

# Starting a release
git checkout develop
git checkout -b release/1.0

# Completing a release
git checkout main
git merge release/1.0
git checkout develop
git merge release/1.0
git branch -d release/1.0

# Creating a hotfix
git checkout main
git checkout -b hotfix/1.0.1

# Completing a hotfix
git checkout main
git merge hotfix/1.0.1
git checkout develop
git merge hotfix/1.0.1
git branch -d hotfix/1.0.1

There's also a gitflow tool that automates much of this workflow.

2. GitHub Flow

GitHub Flow is a simpler, more streamlined workflow focused on frequent deployments. It's particularly well-suited for web applications and continuous delivery environments.

gitGraph commit branch feature-a checkout feature-a commit commit checkout main merge feature-a branch feature-b checkout feature-b commit checkout main merge feature-b branch hotfix checkout hotfix commit checkout main merge hotfix

Key Branches

Workflow

  1. Create a branch from main for a new feature or fix
  2. Develop, commit, and push changes to the branch
  3. Open a pull request for discussion and review
  4. Deploy the branch to test the changes in a production-like environment
  5. Merge the pull request into main after approval
  6. Deploy main to production immediately

Pros and Cons

Pros:

Cons:

Example GitHub Flow Commands

# Starting a feature
git checkout main
git pull
git checkout -b feature/add-login

# During development
git add .
git commit -m "Implement login form"
git push -u origin feature/add-login

# After pull request approval and merge
git checkout main
git pull
git branch -d feature/add-login

3. Trunk-Based Development

Trunk-Based Development is a source-control branching model where developers collaborate on code in a single branch called "trunk" (main), and avoid maintaining long-lived feature branches by integrating their changes frequently.

gitGraph commit branch feature-quick checkout feature-quick commit checkout main merge feature-quick branch bugfix checkout bugfix commit checkout main commit checkout bugfix commit checkout main merge bugfix commit

Key Branches

Workflow

  1. Developers frequently pull from the main branch
  2. Small changes can be made directly on main
  3. Larger changes use short-lived feature branches (typically merged within a day)
  4. Comprehensive automated testing ensures main remains stable
  5. Feature flags hide incomplete features in production

Pros and Cons

Pros:

Cons:

Example Trunk-Based Development Commands

# Start the day by getting latest changes
git checkout main
git pull

# Small change directly on main
git add .
git commit -m "Fix typo in login form"
git push

# Larger change using short-lived branch
git checkout -b feature/add-validation
# Work, commit, then quickly reintegrate (same day)
git checkout main
git pull
git merge feature/add-validation
git push
git branch -d feature/add-validation

4. Release Flow (Microsoft's Approach)

Release Flow is the branching strategy used by Microsoft's DevOps teams. It combines elements of both GitFlow and GitHub Flow.

gitGraph commit branch feature/login checkout feature/login commit commit checkout main commit branch feature/payment checkout feature/payment commit checkout feature/login commit checkout main merge feature/login checkout feature/payment commit checkout main merge feature/payment branch release/1.0 checkout release/1.0 commit checkout main merge release/1.0 tag: "v1.0"

Key Branches

Workflow

  1. Feature development happens on feature branches
  2. Feature branches are merged into main via pull requests
  3. Main is kept in a healthy state through automated testing and code reviews
  4. Release branches are created from main when preparing for a release
  5. Bug fixes for a release happen on the release branch and are cherry-picked back to main
  6. After release, the release branch is merged back to main

Pros and Cons

Pros:

Cons:

Example Release Flow Commands

# Starting a feature
git checkout main
git pull
git checkout -b feature/new-dashboard

# Creating a pull request (after development)
git push -u origin feature/new-dashboard
# (Create pull request in GitHub/Azure DevOps)

# Creating a release branch
git checkout main
git pull
git checkout -b release/1.0

# Fixing a bug in the release
git checkout release/1.0
git checkout -b bugfix/login-error
# Fix the bug
git checkout release/1.0
git merge bugfix/login-error

# Cherry-picking the fix to main
git checkout main
git cherry-pick 

# Finalizing a release
git checkout main
git merge release/1.0
git tag -a v1.0 -m "Version 1.0"
git push --tags

Choosing the Right Branching Strategy

With several branching strategies to choose from, how do you decide which one is right for your project? Let's explore the factors to consider and provide guidance for different scenarios.

Key Considerations

Decision Framework

Here's a simple framework to help you choose:

flowchart TD A[Do you need to support multiple versions?] -->|Yes| B[Do you have frequent releases?] A -->|No| C[Do you practice continuous delivery?] B -->|Yes| D[GitFlow or Release Flow] B -->|No| D C -->|Yes| E[Does your team have strong testing practices?] C -->|No| F[GitHub Flow] E -->|Yes| G[Trunk-Based Development] E -->|No| F style A fill:#f9f9f9,stroke:#333,stroke-width:2px style D fill:#e1f5fe,stroke:#0288d1 style F fill:#e8f5e9,stroke:#43a047 style G fill:#ffebee,stroke:#c62828

Recommendations for Different Scenarios

For Startups and Small Teams

Recommended: GitHub Flow or Trunk-Based Development

Why: Simpler workflows reduce overhead and allow for faster iteration. Small teams can communicate easily to resolve issues.

Considerations: Invest in automated testing early to ensure main stays stable.

For Enterprise Products

Recommended: GitFlow or Release Flow

Why: These strategies provide structure for managing complex releases and multiple product versions.

Considerations: Implement good tooling to automate repetitive branch operations.

For Web Applications with Continuous Delivery

Recommended: GitHub Flow or Trunk-Based Development

Why: These approaches support rapid, iterative development and frequent deployments.

Considerations: Robust CI/CD pipelines and monitoring are essential.

For Open Source Projects

Recommended: GitHub Flow or GitFlow (depending on complexity)

Why: These provide clear structures for contributors and maintainers.

Considerations: Well-documented contribution guidelines are crucial.

For Highly Regulated Industries

Recommended: GitFlow or Release Flow

Why: These provide separation of concerns and clear audit trails for changes.

Considerations: Add additional review and approval steps in the workflow.

Hybrid and Custom Approaches

Remember that these strategies are not rigid rules but rather templates that you can adapt to your specific needs. Many teams use hybrid approaches, taking elements from different strategies.

For example, you might:

The key is to be deliberate about your branching strategy and ensure the whole team understands and follows it consistently.

Implementing Branching Strategies with Git

Now that we understand different branching strategies conceptually, let's explore how to implement them effectively using Git commands and tools.

Setting Up a Repository with Branching in Mind

When starting a new project, set up your repository with your branching strategy in mind:

# Initialize repository with main branch (modern approach)
git init -b main

# Set up default branch for new repositories (Git 2.28+)
git config --global init.defaultBranch main

# For GitFlow, create the develop branch
git checkout -b develop
git push -u origin develop

For existing repositories, you might need to rename the default branch:

# Rename master to main
git branch -m master main
git push -u origin main
git push origin --delete master

Branch Naming Conventions

Consistent branch naming helps everyone understand the purpose of each branch:

Use descriptive names after the prefix, typically using kebab-case (lowercase with hyphens).

Enforcing Branch Protections

Most Git hosting platforms (GitHub, GitLab, Bitbucket) allow you to set branch protection rules to enforce your workflow:

These protections are configured through the web interface of your Git hosting platform.

Tools to Assist with Branching Workflows

Several tools can help you implement branching strategies more efficiently:

GitFlow Extension

The gitflow extension provides high-level commands for the GitFlow workflow:

# Initialize GitFlow in a repository
git flow init

# Start a feature
git flow feature start user-authentication

# Finish a feature
git flow feature finish user-authentication

# Start a release
git flow release start 1.0.0

# Finish a release
git flow release finish 1.0.0

GitHub CLI

The GitHub CLI makes it easier to work with GitHub Flow:

# Create a branch and pull request in one step
gh pr create --fill

# Check out a pull request locally
gh pr checkout 123

# Review, approve, and merge pull requests from the command line
gh pr review 123 --approve
gh pr merge 123

Git Aliases

Create aliases for common branching operations:

# Create a new feature branch
git config --global alias.feature "checkout -b feature/"

# Usage: git feature user-authentication

# Create a new bugfix branch
git config --global alias.bugfix "checkout -b bugfix/"

# Clean up merged branches
git config --global alias.cleanup "!git branch --merged | grep -v '\\*\\|main\\|develop' | xargs -n 1 git branch -d"

Automated Scripts

For complex workflows, consider creating scripts to automate repetitive tasks:

#!/bin/bash
# Example script for starting a new feature

BRANCH_NAME="feature/$1"

# Check if on main branch
CURRENT_BRANCH=$(git symbolic-ref --short HEAD)
if [ "$CURRENT_BRANCH" != "main" ]; then
    echo "Error: Must be on main branch to start a feature"
    exit 1
fi

# Pull latest changes
git pull

# Create and switch to feature branch
git checkout -b "$BRANCH_NAME"

echo "Created and switched to $BRANCH_NAME"

Advanced Branching Techniques

As you become more comfortable with Git branching, you can leverage some advanced techniques to make your workflow more efficient.

Rebasing vs. Merging

There are two ways to integrate changes from one branch into another: merging and rebasing.

Merging

git checkout main
git merge feature-branch

This creates a merge commit that combines the histories of both branches.

Rebasing

git checkout feature-branch
git rebase main

This replays the commits from your feature branch on top of the main branch, creating a linear history.

gitGraph commit id: "A" commit id: "B" branch feature checkout feature commit id: "C" commit id: "D" checkout main commit id: "E" commit id: "F" checkout feature commit id: "G"

After merge:

gitGraph commit id: "A" commit id: "B" branch feature checkout feature commit id: "C" commit id: "D" checkout main commit id: "E" commit id: "F" checkout feature commit id: "G" checkout main merge feature id: "Merge"

After rebase:

gitGraph commit id: "A" commit id: "B" commit id: "E" commit id: "F" commit id: "C'" commit id: "D'" commit id: "G'"

When to use each:

Golden rule of rebasing: Never rebase branches that others are working on.

Cherry-Picking

Cherry-picking allows you to apply specific commits from one branch to another:

git checkout target-branch
git cherry-pick commit-hash

This is useful for applying bug fixes from one branch to another, especially in strategies like Release Flow.

Example: Applying a hotfix from a release branch back to main

git checkout release/1.0
git log  # Find the commit hash of the fix
git checkout main
git cherry-pick abc123  # The commit hash of the fix

Interactive Rebasing

Interactive rebasing allows you to modify a series of commits before integrating them:

git checkout feature-branch
git rebase -i main

This opens an editor where you can:

Interactive rebasing is especially useful for cleaning up your branch before submitting a pull request.

Managing Long-Lived Branches

For branches that exist for extended periods (like develop in GitFlow), it's important to keep them synchronized with other branches:

# Keep develop in sync with main
git checkout develop
git merge main

# Or for a cleaner history
git checkout develop
git rebase main

Regular synchronization reduces the risk of complex merge conflicts later.

Working with Remote Branches

When implementing branching strategies with remote repositories, remember these commands:

# Push a local branch to the remote
git push -u origin branch-name

# Track a remote branch
git checkout --track origin/branch-name

# Delete a remote branch
git push origin --delete branch-name

# Prune deleted remote branches
git fetch --prune

Keeping your local and remote repositories in sync is crucial for effective collaboration.

Best Practices for Effective Branching

To make the most of Git branching regardless of which strategy you choose, follow these best practices:

1. Keep Branches Focused and Short-Lived

Anti-pattern: "Development hell" branches that live for months with dozens or hundreds of commits

2. Commit Often with Meaningful Messages

Example commit message format:

feat(auth): add password reset functionality

Implement password reset via email link to enhance user experience.
The system now sends a time-limited token via email and validates
it when the user clicks the reset link.

Resolves: #123

3. Synchronize Regularly with the Main Branch

Tip: Set up a routine (e.g., start each day by pulling from main) to stay in sync

4. Use Pull Requests for Code Review

Tip: Use draft pull requests for work in progress to signal that you're not ready for review yet

5. Enforce Branch Protections

Remember: Branch protections are only effective if they're consistently applied

6. Clean Up Merged Branches

# List merged branches
git branch --merged

# Delete local branches that have been merged
git branch --merged | grep -v '\\*\\|main\\|develop' | xargs -n 1 git branch -d

# Prune remote branches that no longer exist
git fetch --prune

7. Document Your Branching Strategy

Sample documentation outline:

Common Challenges and Solutions

Even with a well-designed branching strategy, you may encounter challenges. Here are some common issues and how to address them:

Merge Conflicts

Challenge: Conflicts occur when Git can't automatically merge changes.

Prevention:

Resolution:

Long-Running Feature Branches

Challenge: Large features can lead to branches that live for weeks or months, creating integration headaches.

Solutions:

Accidental Commits to the Wrong Branch

Challenge: Sometimes you realize you've been working on the wrong branch.

Solutions:

Accidental Push to Protected Branch

Challenge: You've accidentally pushed directly to a protected branch like main.

Solutions:

Managing Hotfixes Across Multiple Versions

Challenge: A critical bug affects multiple versions of your software.

Solutions:

Dealing with Large Binary Files

Challenge: Large binary files can bloat your repository and cause performance issues.

Solutions:

Practice Exercises

Let's apply what we've learned with some practical exercises:

Exercise 1: Basic Branching and Merging

  1. Create a new Git repository
  2. Create a file called index.html with basic HTML structure
  3. Commit this file to the main branch
  4. Create and switch to a new branch called feature/header
  5. Add a header to the HTML file and commit the change
  6. Switch back to the main branch
  7. Create and switch to another branch called feature/footer
  8. Add a footer to the HTML file (note: this should be a different section than your header) and commit the change
  9. Switch back to the main branch
  10. Merge the feature/header branch into main
  11. Merge the feature/footer branch into main
  12. View the commit history with git log --graph --oneline

Exercise 2: Implementing GitFlow

  1. Create a new Git repository
  2. Initialize it with a main branch and an initial commit
  3. Create a develop branch
  4. Create a feature branch from develop
  5. Make several commits to the feature branch
  6. Merge the feature branch back into develop
  7. Create a release branch from develop
  8. Make a fix on the release branch
  9. Merge the release branch into both main and develop
  10. Create a tag on main for the release
  11. Create a hotfix branch from main
  12. Make a critical fix on the hotfix branch
  13. Merge the hotfix branch into both main and develop
  14. Create another tag on main for the hotfix
  15. Visualize the result with git log --graph --all --oneline

Exercise 3: Resolve a Merge Conflict

  1. Create a new Git repository with a file called README.md containing some text
  2. Create two branches: branch-a and branch-b
  3. In branch-a, modify line 1 of README.md and commit
  4. In branch-b, modify the same line differently and commit
  5. Try to merge branch-a into branch-b
  6. Resolve the merge conflict by editing the file
  7. Complete the merge with git commit
  8. Visualize the result with git log --graph --oneline

Exercise 4: Interactive Rebasing

  1. Create a new Git repository
  2. Create a file and make an initial commit
  3. Create a feature branch
  4. Make at least 5 small commits on the feature branch
  5. Use interactive rebasing to:
    • Squash some commits together
    • Reword a commit message
    • Reorder some commits
  6. Compare the before and after commit history

Exercise 5: Document a Branching Strategy

Choose one of the branching strategies we've discussed and create a document that:

This documentation should be clear enough that a new team member could understand and follow your branching strategy.

Further Reading