DevOpsbeginner

Git Workflow & Branching Strategies

Master Git fundamentals and branching strategies. Learn GitHub Flow, Git Flow, trunk-based development, merge vs rebase, conflict resolution, and commit conventions.

13 min read·Published May 3, 2026
devopsgitbranchingworkflow

Why Git Matters

Git is the version control system behind virtually every modern software project. Whether you're solo or on a 200-person team, how you use Git directly impacts code quality, release velocity, and team sanity.

This guide covers Git fundamentals, the three dominant branching strategies, merge vs rebase tradeoffs, conflict resolution, and commit message conventions that actually help.

Git Fundamentals

The Three Areas

Every Git repository has three conceptual areas that files move between:

+-------------------+     git add     +-------------------+    git commit    +-------------------+
|  Working Directory | -------------> |   Staging Area     | -------------> |   Repository       |
|  (your files)      | <------------- |   (index)          |                |   (.git history)   |
+-------------------+   git restore   +-------------------+                +-------------------+
  • Working Directory -- the actual files on disk you edit
  • Staging Area (Index) -- a snapshot of what will go into the next commit
  • Repository -- the complete history of committed snapshots

Essential Commands

# Initialize a new repository
git init

# Clone an existing repository
git clone https://github.com/user/repo.git

# Check current status
git status

# Stage specific files
git add src/app.ts src/utils.ts

# Stage all changes
git add .

# Commit staged changes
git commit -m "feat: add user authentication"

# Push to remote
git push origin main

# Pull latest changes
git pull origin main

# Create and switch to a new branch
git checkout -b feature/login-page

# Switch to an existing branch
git checkout main

# Modern alternative to checkout for switching
git switch main
git switch -c feature/login-page

Viewing History

# Full commit log
git log

# Compact one-line log
git log --oneline

# Visual branch graph
git log --oneline --graph --all

# Show changes in last commit
git show

# Show differences between working directory and staging
git diff

# Show differences between staging and last commit
git diff --staged

Undoing Changes

# Unstage a file (keep changes in working directory)
git restore --staged src/app.ts

# Discard working directory changes for a file
git restore src/app.ts

# Amend the last commit message (before pushing)
git commit --amend -m "fix: correct typo in auth module"

# Revert a specific commit (creates a new commit that undoes it)
git revert abc1234

# Reset to a previous commit (destructive -- use with caution)
git reset --hard HEAD~1

Branching Strategies

Choosing the right branching strategy depends on team size, release cadence, and project maturity. Here are the three most widely used.

Strategy 1: GitHub Flow

GitHub Flow is the simplest strategy. It works well for web apps with continuous deployment.

main ----*----*----*----*----*----*----*----*---->
              \                  /
feature/login  *----*----*------
                    (PR + review)

Rules:

  1. main is always deployable
  2. Create a feature branch from main
  3. Make commits on the feature branch
  4. Open a Pull Request for review
  5. Merge to main after approval
  6. Deploy from main
# Start feature
git checkout main
git pull origin main
git checkout -b feature/user-dashboard

# Work on feature
git add .
git commit -m "feat: add dashboard layout"
git commit -m "feat: add activity feed component"

# Push and open PR
git push -u origin feature/user-dashboard
# Open PR on GitHub, get review, merge

Best for: small teams, SaaS products, continuous deployment.

Drawbacks: no concept of releases or hotfixes as first-class citizens.

Strategy 2: Git Flow

Git Flow is a more structured strategy with dedicated branches for releases and hotfixes. Created by Vincent Driessen in 2010.

main     ----*-----------------*-----------*---->
              \               / \         /
release        \     *---*---    \       /
                \   /             \     /
develop  ----*---*---*---*---*----*---*---*---->
              \     /       \       /
feature        *---*         *---*-

Branches:

  • main -- production-ready code, tagged with version numbers
  • develop -- integration branch for features
  • feature/* -- individual features, branch from develop
  • release/* -- release preparation, branch from develop
  • hotfix/* -- urgent production fixes, branch from main
# Start a feature
git checkout develop
git checkout -b feature/payment-system

# Finish feature -- merge back to develop
git checkout develop
git merge --no-ff feature/payment-system

# Start a release
git checkout develop
git checkout -b release/2.1.0

# Finish release -- merge to main AND develop
git checkout main
git merge --no-ff release/2.1.0
git tag -a v2.1.0 -m "Release 2.1.0"

git checkout develop
git merge --no-ff release/2.1.0

# Hotfix
git checkout main
git checkout -b hotfix/critical-auth-bug
# fix the bug...
git checkout main
git merge --no-ff hotfix/critical-auth-bug
git tag -a v2.1.1 -m "Hotfix 2.1.1"
git checkout develop
git merge --no-ff hotfix/critical-auth-bug

Best for: large teams, software with formal release cycles, mobile apps, libraries.

Drawbacks: complex, many long-lived branches, merge conflicts increase.

Strategy 3: Trunk-Based Development

Trunk-based development keeps all work on a single branch (main or trunk) with very short-lived feature branches (hours, not days).

main ----*--*--*--*--*--*--*--*--*--*--*--*---->
             \  /     \  /        \ /
              **       **          *
         (short-lived feature branches)

Rules:

  1. Everyone commits to main (or merges within hours)
  2. Feature branches live at most 1-2 days
  3. Use feature flags to hide incomplete work
  4. CI runs on every commit
  5. Broken builds are fixed immediately
# Short-lived branch
git checkout -b feat/button-color
git add .
git commit -m "feat: update button color to brand blue"
git push -u origin feat/button-color
# PR, review, merge same day

# Or commit directly to main (with feature flag)
git checkout main
git add .
git commit -m "feat: add search (behind flag ENABLE_SEARCH)"
git push origin main

Best for: experienced teams, high deployment frequency, microservices.

Drawbacks: requires strong CI/CD, feature flags add complexity.

Comparison Table

AspectGitHub FlowGit FlowTrunk-Based
ComplexityLowHighLow
Branch LifespanDaysDays to weeksHours
Release ProcessDeploy mainDedicated release branchDeploy main
Hotfix ProcessBranch from mainDedicated hotfix branchCommit to main
Best Team Size1-1510-100+5-50
Deploy FrequencyContinuousScheduledContinuous
CI/CD RequirementMediumLowHigh
Learning CurveEasySteepEasy (hard discipline)

Merge vs Rebase

This is the most debated topic in Git. Both achieve the same result (integrating changes) but produce different histories.

Merge

Merge creates a new "merge commit" that combines two branches. It preserves the full history.

git checkout main
git merge feature/login
Before merge:
main     ----A----B----C
                        \
feature                  D----E

After merge:
main     ----A----B----C---------F (merge commit)
                        \       /
feature                  D----E

Rebase

Rebase replays your commits on top of another branch. It creates a linear history.

git checkout feature/login
git rebase main
Before rebase:
main     ----A----B----C
                        \
feature                  D----E

After rebase:
main     ----A----B----C
                        \
feature                  D'----E' (replayed commits)

Merge vs Rebase Comparison

AspectMergeRebase
HistoryNon-linear (branched)Linear (straight line)
Merge CommitsYes (extra commits)No
Commit HashesPreservedChanged (rewritten)
Shared BranchesSafeDangerous (rewrites history)
Conflict ResolutionOnce (at merge)Per commit (during replay)
TraceabilityFull branch contextCleaner but less context
Undo DifficultyEasy (revert merge commit)Harder (history rewritten)

The Golden Rule of Rebase

Never rebase commits that have been pushed to a shared branch. Rebasing rewrites commit hashes. If others have based work on those commits, you will cause conflicts for the entire team.

# Safe: rebase your local feature branch onto updated main
git checkout feature/my-work
git rebase main

# Dangerous: NEVER do this
git checkout main
git rebase feature/my-work

Squash Merge

A popular middle ground. Squash all feature commits into a single commit on main.

# On GitHub: "Squash and merge" button
# Or manually:
git checkout main
git merge --squash feature/login
git commit -m "feat: add login page with OAuth support"
Before:
feature  D----E----F----G (4 messy commits)

After squash merge:
main     ----A----B----C----H (single clean commit)

Merge Conflict Resolution

Conflicts happen when two branches change the same lines. Git marks the conflicting sections.

What a Conflict Looks Like

function getGreeting(name) {
<<<<<<< HEAD
  return `Hello, ${name}! Welcome back.`;
=======
  return `Hey ${name}, good to see you!`;
>>>>>>> feature/new-greeting
}
  • <<<<<<< HEAD -- your current branch's version
  • ======= -- separator
  • >>>>>>> feature/new-greeting -- the incoming branch's version

Resolution Steps

# 1. Attempt the merge
git merge feature/new-greeting
# CONFLICT: Merge conflict in src/greeting.ts

# 2. Open conflicted files and resolve manually
# Pick one version, combine both, or write something new

# 3. After editing, the resolved file looks like:
function getGreeting(name) {
  return `Hey ${name}! Welcome back.`;
}
# 4. Stage the resolved file
git add src/greeting.ts

# 5. Complete the merge
git commit -m "merge: resolve greeting conflict"

Preventing Conflicts

  1. Pull frequently -- stay up to date with main
  2. Keep branches short-lived -- less time = less divergence
  3. Communicate -- if two people work on the same file, coordinate
  4. Use consistent formatting -- auto-formatters prevent whitespace conflicts
# Stay up to date while working on a feature
git checkout feature/dashboard
git fetch origin
git rebase origin/main
# Resolve any small conflicts incrementally

Aborting a Merge

If a conflict is too complex and you want to start over:

git merge --abort

This returns your branch to the state before the merge attempt.

Commit Message Best Practices

Good commit messages make projects maintainable. Bad ones are useless noise.

Conventional Commits

The Conventional Commits specification provides a structured format:

<type>(<scope>): <description>

[optional body]

[optional footer(s)]

Types:

  • feat -- new feature
  • fix -- bug fix
  • docs -- documentation only
  • style -- formatting, no code change
  • refactor -- code change that neither fixes a bug nor adds a feature
  • test -- adding or correcting tests
  • chore -- build process, tooling, dependencies

Examples

# Feature
git commit -m "feat(auth): add Google OAuth login"

# Bug fix
git commit -m "fix(cart): prevent negative quantity on item update"

# Breaking change
git commit -m "feat(api)!: change response format to JSON:API spec

BREAKING CHANGE: All API responses now follow JSON:API format.
Clients must update their parsers."

# With scope and body
git commit -m "refactor(database): migrate from callbacks to async/await

Replaced all callback-based database queries with async/await.
No functional changes. All existing tests pass."

The Seven Rules of Great Commit Messages

  1. Separate subject from body with a blank line
  2. Limit the subject line to 50 characters
  3. Capitalize the subject line
  4. Do not end the subject line with a period
  5. Use the imperative mood ("add feature" not "added feature")
  6. Wrap the body at 72 characters
  7. Explain the what and why, not the how

Bad vs Good Messages

# Bad
git commit -m "fix stuff"
git commit -m "WIP"
git commit -m "asdfasdf"
git commit -m "changes"

# Good
git commit -m "fix(auth): handle expired token refresh race condition"
git commit -m "feat(search): add fuzzy matching for product names"
git commit -m "docs: add API rate limiting section to README"
git commit -m "test(cart): add edge case for empty cart checkout"

Git Tags

Tags mark specific points in history, typically used for releases.

# Create annotated tag (recommended for releases)
git tag -a v1.0.0 -m "Initial stable release"

# Create lightweight tag
git tag v1.0.0-beta

# List tags
git tag -l

# List tags matching a pattern
git tag -l "v1.*"

# Push a specific tag
git push origin v1.0.0

# Push all tags
git push origin --tags

# Delete a local tag
git tag -d v1.0.0-beta

# Delete a remote tag
git push origin --delete v1.0.0-beta

Semantic Versioning with Tags

Follow semver (MAJOR.MINOR.PATCH):

v1.0.0    # Initial release
v1.1.0    # New feature (backward compatible)
v1.1.1    # Bug fix
v2.0.0    # Breaking change

Git Stash

Stash lets you save uncommitted changes without committing them. Useful when you need to switch branches quickly.

# Stash current changes
git stash

# Stash with a descriptive message
git stash push -m "WIP: dashboard layout changes"

# List stashes
git stash list

# Apply most recent stash (keep it in stash list)
git stash apply

# Apply and remove most recent stash
git stash pop

# Apply a specific stash
git stash apply stash@{2}

# Drop a specific stash
git stash drop stash@{0}

# Clear all stashes
git stash clear

.gitignore Best Practices

A .gitignore file tells Git which files to exclude from tracking.

# Dependencies
node_modules/
vendor/

# Build output
dist/
build/
.next/
out/

# Environment files (secrets!)
.env
.env.local
.env.production

# IDE files
.vscode/settings.json
.idea/

# OS files
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*

# Test coverage
coverage/

# Temporary files
*.tmp
*.swp
# If you already committed a file and want to stop tracking it:
git rm --cached .env
echo ".env" >> .gitignore
git commit -m "chore: remove .env from tracking"

Git Aliases for Productivity

Add these to your ~/.gitconfig to speed up daily work:

[alias]
    s = status
    co = checkout
    br = branch
    ci = commit
    lg = log --oneline --graph --all --decorate
    last = log -1 HEAD
    unstage = restore --staged
    undo = reset HEAD~1 --mixed
    amend = commit --amend --no-edit
    branches = branch -a
    stashes = stash list
    wip = !git add . && git commit -m "WIP"
# Usage
git s          # instead of git status
git co main    # instead of git checkout main
git lg         # pretty log graph
git undo       # undo last commit, keep changes

Common Workflows Cheat Sheet

Starting a New Feature

git checkout main
git pull origin main
git checkout -b feature/TICKET-123-add-search
# work...
git add .
git commit -m "feat(search): add basic search endpoint"
git push -u origin feature/TICKET-123-add-search
# open PR

Updating a Feature Branch

git checkout feature/TICKET-123-add-search
git fetch origin
git rebase origin/main
# resolve conflicts if any
git push --force-with-lease  # safe force push for rebased branch

Emergency Hotfix

git checkout main
git pull origin main
git checkout -b hotfix/fix-payment-crash
# fix the bug...
git add .
git commit -m "fix(payment): handle null card token"
git push -u origin hotfix/fix-payment-crash
# open PR, get emergency review, merge

Cleaning Up Merged Branches

# Delete local branch
git branch -d feature/TICKET-123-add-search

# Delete remote branch
git push origin --delete feature/TICKET-123-add-search

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

Key Takeaways

  • Git Flow for structured releases, GitHub Flow for simplicity, Trunk-Based for speed
  • Merge preserves history, rebase linearizes it -- never rebase shared branches
  • Squash merge is a practical middle ground for clean main history
  • Conventional commits make changelogs and automation possible
  • Pull frequently, keep branches short-lived, communicate with your team
  • Stash saves you when you need to context-switch without committing
  • Good .gitignore prevents secrets and junk from entering the repo

Found this helpful?

Support devsofus — help us keep creating free dev guides.

Related Articles