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.
In this lecture, we'll explore:
- The concept of branching and why it's important
- Basic branching operations
- Common branching strategies used in real-world projects
- How to implement these strategies with Git commands
- Best practices for effective branching
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.
In this diagram:
- Main and develop are branches (pointers to commits)
- When we create the develop branch, it points to the same commit as main (C2)
- As we make commits on different branches, the pointers move forward
- When we merge, Git creates a new commit (if needed) that combines the changes
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.
Key Branches
- main: Always contains production-ready code
- develop: Integration branch for features, serves as a staging area for the next release
- feature/*: Created from and merged back into develop, for developing new features
- release/*: Branched from develop when it's ready for release, for final polishing and bug fixes
- hotfix/*: Created from main to quickly fix critical bugs in production
Workflow
- Feature development happens on feature branches
- Completed features are merged into develop
- When develop has enough features for a release, create a release branch
- After testing and bug fixes, merge the release branch into both main and develop
- If bugs are found in production, create a hotfix branch from main
- After fixing, merge the hotfix into both main and develop
Pros and Cons
Pros:
- Well-defined structure with clear roles for different branches
- Supports parallel development of multiple features
- Clean separation between in-progress work and stable code
- Good for projects with scheduled releases
Cons:
- Complex for smaller projects or teams
- Can lead to lengthy merge processes and conflicts
- Not ideal for continuous delivery environments
- Main branch can become outdated between releases
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.
Key Branches
- main: Always deployable, contains production code
- feature branches: Created from main for developing features or fixes
Workflow
- Create a branch from main for a new feature or fix
- Develop, commit, and push changes to the branch
- Open a pull request for discussion and review
- Deploy the branch to test the changes in a production-like environment
- Merge the pull request into main after approval
- Deploy main to production immediately
Pros and Cons
Pros:
- Simple and easy to understand
- Well-suited for continuous delivery
- Encourages frequent integration and deployment
- Reduces merge conflicts through smaller, shorter-lived branches
Cons:
- Less structured than GitFlow for managing releases
- Requires robust testing and deployment infrastructure
- May not be suitable for projects that need to maintain multiple versions
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.
Key Branches
- main (or trunk): The primary development branch
- Short-lived feature branches: Exist for very short periods, typically less than a day
- Release branches: Optional, created only when needed to support specific releases
Workflow
- Developers frequently pull from the main branch
- Small changes can be made directly on main
- Larger changes use short-lived feature branches (typically merged within a day)
- Comprehensive automated testing ensures main remains stable
- Feature flags hide incomplete features in production
Pros and Cons
Pros:
- Simple workflow with minimal branch management
- Encourages small, frequent commits and continuous integration
- Reduces merge conflicts through frequent integration
- Well-suited for mature teams with good automated testing
Cons:
- Requires disciplined developers and excellent test coverage
- Feature flags can become complex to manage
- Less isolated development compared to feature branching
- Difficult for open-source projects with many contributors
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.
Key Branches
- main: The primary integration branch, should always be in a healthy state
- feature/*: Created from and merged back into main via pull requests
- release/*: Created from main when preparing for a specific release
- hotfix/*: Created to fix critical issues in released software
Workflow
- Feature development happens on feature branches
- Feature branches are merged into main via pull requests
- Main is kept in a healthy state through automated testing and code reviews
- Release branches are created from main when preparing for a release
- Bug fixes for a release happen on the release branch and are cherry-picked back to main
- After release, the release branch is merged back to main
Pros and Cons
Pros:
- Balances the simplicity of GitHub Flow with the structured releases of GitFlow
- Works well for complex products with multiple release cycles
- Supports both continuous integration and scheduled releases
- Clear separation of in-progress work through pull requests
Cons:
- More complex than GitHub Flow
- Cherry-picking can be error-prone
- Requires good tooling and process discipline
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
- Team size and distribution: Larger, distributed teams often need more structure
- Release cadence: How often do you deploy to production?
- Project complexity: More complex projects may benefit from more structured approaches
- Quality requirements: Higher quality requirements may need more isolation and testing
- Team experience: More experienced teams can handle simpler workflows effectively
- Contribution model: Open source vs. closed team development
Decision Framework
Here's a simple framework to help you choose:
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:
- Use GitHub Flow but add release branches for major versions
- Follow GitFlow but simplify it by omitting certain branch types
- Implement Trunk-Based Development with longer-lived feature branches for larger changes
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:
- feature/: For new features (e.g.,
feature/user-authentication) - bugfix/: For non-critical bug fixes (e.g.,
bugfix/login-validation) - hotfix/: For critical bug fixes (e.g.,
hotfix/security-vulnerability) - release/: For release preparation (e.g.,
release/1.0.0) - refactor/: For code refactoring (e.g.,
refactor/clean-up-auth-module) - docs/: For documentation updates (e.g.,
docs/api-reference)
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:
- Require pull request reviews: Prevent direct pushes to important branches
- Require status checks: Ensure tests pass before merging
- Restrict who can push: Limit access to certain branches
- Require linear history: Enforce rebasing instead of merge commits
- Automatically delete merged branches: Keep the repository clean
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.
After merge:
After rebase:
When to use each:
- Merging: Preserves history, showing exactly when branches diverged and were integrated. Good for feature branches that other developers may be using.
- Rebasing: Creates a cleaner, linear history. Good for cleaning up your local changes before sharing them.
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:
- pick: Keep the commit as-is
- reword: Edit the commit message
- edit: Stop to amend the commit
- squash: Combine with the previous commit
- fixup: Combine with the previous commit, discarding this commit's message
- drop: Remove the commit entirely
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
- Each branch should have a single, clear purpose
- Limit the scope of changes in a branch
- Merge or delete branches promptly after they serve their purpose
- Shorter-lived branches mean fewer merge conflicts
Anti-pattern: "Development hell" branches that live for months with dozens or hundreds of commits
2. Commit Often with Meaningful Messages
- Make small, focused commits that are easy to understand
- Write clear commit messages that explain the why, not just the what
- Consider using a commit message convention (e.g., Conventional Commits)
- Frequent commits make it easier to identify where issues were introduced
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
- Pull from the main branch frequently
- Resolve conflicts as they arise, not all at once at the end
- Consider rebasing feature branches on main before creating pull requests
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
- Create pull requests for all significant changes
- Keep pull requests reasonably sized for effective review
- Include context and testing instructions in the description
- Respond to feedback promptly and constructively
Tip: Use draft pull requests for work in progress to signal that you're not ready for review yet
5. Enforce Branch Protections
- Prevent direct pushes to important branches
- Require code reviews before merging
- Set up CI/CD to run tests automatically
- Document branch protection policies for the team
Remember: Branch protections are only effective if they're consistently applied
6. Clean Up Merged Branches
- Delete branches after they're merged
- Periodically clean up stale branches
- Consider automating branch cleanup
# 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
- Create a clear, concise guide to your branching strategy
- Include visual diagrams to explain the workflow
- Document branch naming conventions
- Keep the documentation in the repository (e.g., in CONTRIBUTING.md)
Sample documentation outline:
- Overview of the branching strategy
- Branch types and their purposes
- Naming conventions
- Step-by-step workflows for common scenarios
- Examples of Git commands
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:
- Synchronize with main/develop regularly
- Keep branches small and focused
- Communicate with team members working on related code
Resolution:
- Use visualization tools to understand the conflict (e.g.,
git mergetool) - Resolve conflicts in small batches
- When in doubt, consult with the developer who made the conflicting changes
- After resolving, carefully test the merged code
Long-Running Feature Branches
Challenge: Large features can lead to branches that live for weeks or months, creating integration headaches.
Solutions:
- Break large features into smaller, independently deliverable chunks
- Use feature flags to hide incomplete features in production
- Implement regular integration with the main branch
- Consider using a "development" branch for early integration
Accidental Commits to the Wrong Branch
Challenge: Sometimes you realize you've been working on the wrong branch.
Solutions:
- For uncommitted changes: Stash, switch, apply
git stash git checkout correct-branch git stash apply - For committed changes: Cherry-pick
git checkout correct-branch git cherry-pick <commit-hash> git checkout wrong-branch git reset --hard HEAD~1 # Remove the commit from the wrong branch
Accidental Push to Protected Branch
Challenge: You've accidentally pushed directly to a protected branch like main.
Solutions:
- If the push was rejected due to branch protection, breathe a sigh of relief
- If it went through:
# Create a branch with the current state git checkout -b temp-branch # Reset main to its previous state git checkout main git reset --hard origin/main@{1} # Reset to the previous state git push --force-with-lease # Force push, but only if no one else has pushed # Now create a PR from temp-branch to main - Always use
--force-with-leaseinstead of--forceto avoid overwriting others' changes
Managing Hotfixes Across Multiple Versions
Challenge: A critical bug affects multiple versions of your software.
Solutions:
- Fix in the oldest affected version first
- Cherry-pick the fix forward to newer versions
git checkout release/1.0
git checkout -b hotfix/critical-bug
# Fix the bug and commit
git checkout release/1.0
git merge hotfix/critical-bug
git tag v1.0.1
git checkout release/2.0
git cherry-pick <fix-commit-hash>
git tag v2.0.1
git checkout main
git cherry-pick <fix-commit-hash>
Dealing with Large Binary Files
Challenge: Large binary files can bloat your repository and cause performance issues.
Solutions:
- Use Git Large File Storage (LFS) for binary assets
- Consider keeping binaries outside of Git entirely
- Use .gitignore to exclude build artifacts and other generated binaries
Practice Exercises
Let's apply what we've learned with some practical exercises:
Exercise 1: Basic Branching and Merging
- Create a new Git repository
- Create a file called
index.htmlwith basic HTML structure - Commit this file to the main branch
- Create and switch to a new branch called
feature/header - Add a header to the HTML file and commit the change
- Switch back to the main branch
- Create and switch to another branch called
feature/footer - Add a footer to the HTML file (note: this should be a different section than your header) and commit the change
- Switch back to the main branch
- Merge the
feature/headerbranch into main - Merge the
feature/footerbranch into main - View the commit history with
git log --graph --oneline
Exercise 2: Implementing GitFlow
- Create a new Git repository
- Initialize it with a main branch and an initial commit
- Create a develop branch
- Create a feature branch from develop
- Make several commits to the feature branch
- Merge the feature branch back into develop
- Create a release branch from develop
- Make a fix on the release branch
- Merge the release branch into both main and develop
- Create a tag on main for the release
- Create a hotfix branch from main
- Make a critical fix on the hotfix branch
- Merge the hotfix branch into both main and develop
- Create another tag on main for the hotfix
- Visualize the result with
git log --graph --all --oneline
Exercise 3: Resolve a Merge Conflict
- Create a new Git repository with a file called
README.mdcontaining some text - Create two branches:
branch-aandbranch-b - In
branch-a, modify line 1 of README.md and commit - In
branch-b, modify the same line differently and commit - Try to merge
branch-aintobranch-b - Resolve the merge conflict by editing the file
- Complete the merge with
git commit - Visualize the result with
git log --graph --oneline
Exercise 4: Interactive Rebasing
- Create a new Git repository
- Create a file and make an initial commit
- Create a feature branch
- Make at least 5 small commits on the feature branch
- Use interactive rebasing to:
- Squash some commits together
- Reword a commit message
- Reorder some commits
- 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:
- Explains the strategy at a high level
- Includes a diagram (can be hand-drawn)
- Lists branch naming conventions
- Provides step-by-step instructions for common scenarios
- Includes example Git commands
This documentation should be clear enough that a new team member could understand and follow your branching strategy.