Git Workflows
A Git workflow is a recipe or guideline for how to use Git effectively. The right workflow depends on your team size, release cadence, and deployment strategy. Choosing poorly can slow development; choosing well enables rapid, safe shipping.
This guide covers the five most popular workflows, their trade-offs, and when each is appropriate — from solo development to large distributed teams.
Gitflow uses long-lived main and develop branches, with feature, release, and hotfix branches. It excels at scheduled releases with versioned software.
| 1 | # Gitflow setup |
| 2 | git flow init -d |
| 3 | |
| 4 | # Feature development |
| 5 | git flow feature start user-authentication |
| 6 | # ... work on feature ... |
| 7 | git flow feature finish user-authentication |
| 8 | |
| 9 | # Release preparation |
| 10 | git flow release start 2.0.0 |
| 11 | # ... bump version, update changelog ... |
| 12 | git flow release finish 2.0.0 |
| 13 | |
| 14 | # Hotfix for production bugs |
| 15 | git flow hotfix start critical-fix |
| 16 | # ... fix the bug ... |
| 17 | git flow hotfix finish critical-fix |
| 18 | |
| 19 | # Branch structure: |
| 20 | # main — production-ready code |
| 21 | # develop — integration branch |
| 22 | # feature/* — new features (branch from develop) |
| 23 | # release/* — release preparation (branch from develop) |
| 24 | # hotfix/* — production fixes (branch from main) |
best practice
GitHub Flow is a lightweight, branch-based workflow. Every change goes through: create a branch, make changes, open a PR, get reviewed, deploy to staging, merge to main. Simple and effective for continuous deployment.
| 1 | # GitHub Flow — simplified |
| 2 | # 1. Create a descriptive branch |
| 3 | git checkout main && git pull |
| 4 | git checkout -b feature/add-dark-mode |
| 5 | |
| 6 | # 2. Make changes and commit |
| 7 | git add -A |
| 8 | git commit -m "feat: add dark mode toggle" |
| 9 | |
| 10 | # 3. Push and create PR |
| 11 | git push -u origin feature/add-dark-mode |
| 12 | # Open PR on GitHub |
| 13 | |
| 14 | # 4. After review and CI passes, merge (squash) |
| 15 | # 5. Deploy main automatically |
| 16 | |
| 17 | # Naming conventions: |
| 18 | # feature/description — new features |
| 19 | # fix/description — bug fixes |
| 20 | # docs/description — documentation |
| 21 | # refactor/description — code refactoring |
| 22 | # chore/description — maintenance tasks |
info
GitLab Flow sits between Gitflow and GitHub Flow. It uses environment branches (staging, production) with downstream merging, giving you more control than GitHub Flow while staying simpler than Gitflow.
| 1 | # GitLab Flow — environment branches |
| 2 | # Feature branches merge into pre-production first |
| 3 | git checkout -b feature/new-api |
| 4 | # ... work ... |
| 5 | # Merge: feature -> pre-production -> production |
| 6 | |
| 7 | # Release branches for versioned software |
| 8 | git checkout -b release/2.0 |
| 9 | # Cherry-pick fixes back to main |
| 10 | |
| 11 | # Push rules enforce merge-down, cherry-pick-up: |
| 12 | # main -> pre-production -> production (forward) |
| 13 | # hotfix main -> cherry-pick to pre-production (backward) |
| 14 | |
| 15 | # GitLab CI/CD auto-deploys per environment: |
| 16 | # main -> dev environment |
| 17 | # pre-production -> staging |
| 18 | # production -> live |
Trunk-based development means everyone commits to main (the trunk) frequently — ideally multiple times per day. Feature flags gate incomplete work instead of long-lived branches.
| 1 | # Trunk-based: short-lived branches, frequent merges |
| 2 | git checkout main && git pull |
| 3 | git checkout -b feat/dark-mode |
| 4 | |
| 5 | # Small, frequent commits |
| 6 | git commit -m "feat: add theme provider" |
| 7 | git commit -m "feat: add dark mode toggle component" |
| 8 | git commit -m "test: add dark mode tests" |
| 9 | |
| 10 | # Merge quickly (hours, not days) |
| 11 | git push && gh pr create |
| 12 | |
| 13 | # Feature flags gate incomplete work |
| 14 | # In code: |
| 15 | if (featureFlags.isEnabled("dark-mode")) { |
| 16 | return <DarkMode />; |
| 17 | } |
| 18 | |
| 19 | # Release from main — no release branches |
| 20 | git tag v2.0.0 |
| 21 | git push origin v2.0.0 |
best practice
| 1 | # Keep branches short-lived (< 2 days ideal) |
| 2 | # Rebase before merging to keep history linear |
| 3 | git checkout feature/my-feature |
| 4 | git fetch origin |
| 5 | git rebase origin/main |
| 6 | # Resolve conflicts if any |
| 7 | git push --force-with-lease # Safe force push after rebase |
| 8 | |
| 9 | # Squash merge for clean history |
| 10 | git checkout main |
| 11 | git merge --squash feature/my-feature |
| 12 | git commit -m "feat: add user authentication system" |
| 13 | |
| 14 | # Or rebase merge for preserving individual commits |
| 15 | git checkout feature/my-feature |
| 16 | git rebase main |
| 17 | git checkout main |
| 18 | git merge --ff-only feature/my-feature |
| 19 | |
| 20 | # Clean up merged branches |
| 21 | git branch -d feature/my-feature |
| 22 | git push origin --delete feature/my-feature |
| 23 | |
| 24 | # Prune stale remote-tracking branches |
| 25 | git fetch --prune |
info
| 1 | # Semantic Versioning: MAJOR.MINOR.PATCH |
| 2 | # 1.0.0 -> 1.0.1 (patch: bug fix) |
| 3 | # 1.0.1 -> 1.1.0 (minor: new feature, backwards compatible) |
| 4 | # 1.1.0 -> 2.0.0 (major: breaking change) |
| 5 | |
| 6 | # Create a release |
| 7 | git tag -a v2.1.0 -m "Release 2.1.0: dark mode, performance improvements" |
| 8 | git push origin v2.1.0 |
| 9 | |
| 10 | # Generate changelog automatically |
| 11 | git log --pretty=format:"- %s" v2.0.0..v2.1.0 > CHANGELOG.md |
| 12 | |
| 13 | # Automated release with GitHub Actions |
| 14 | # .github/workflows/release.yml: |
| 15 | # on: |
| 16 | # push: |
| 17 | # tags: ['v*'] |
| 18 | # jobs: |
| 19 | # release: |
| 20 | # steps: |
| 21 | # - uses: actions/create-release@v1 |
| 22 | # with: |
| 23 | # tag_name: ${{ github.ref }} |
| 24 | # release_name: Release ${{ github.ref_name }} |
best practice
Extra depth for production teams — conflict strategies, automation, and recovery.
Automation-friendly flags
| 1 | git status --porcelain=v1 |
| 2 | git diff --name-only --diff-filter=ACMR |
| 3 | git log -1 --pretty=format:%H |
| 4 | git merge-base HEAD origin/main |
| 5 | git rev-list --count origin/main..HEAD |
Recovery drill
| 1 | git reflog | head -20 |
| 2 | git fsck --lost-found | head |
| 3 | git branch rescue HEAD@{1} |
| 4 | git log --oneline rescue -5 |
pro tip
- No secrets in history for this change set
- CI green on the PR
- Rebased or merged with latest main
- Rollback plan: revert SHA known
- Tags/releases updated if needed
| 1 | git status -sb |
| 2 | git log --oneline origin/main..HEAD |
| 3 | git diff --check |
| 4 | git rev-parse HEAD |
| 1 | # Cherry-pick a range onto a release branch |
| 2 | git switch release/1.2 |
| 3 | git cherry-pick abc123^..def456 |
| 4 | # conflict? |
| 5 | git status |
| 6 | # fix, then: |
| 7 | git add -A && git cherry-pick --continue |
| 8 | # or abort: |
| 9 | # git cherry-pick --abort |
| 1 | # Bisect with a script |
| 2 | git bisect start |
| 3 | git bisect bad HEAD |
| 4 | git bisect good v1.0.0 |
| 5 | git bisect run ./scripts/test-bug.sh |
| 6 | git bisect reset |
| 1 | # Submodule bump |
| 2 | git submodule update --remote --merge libs/shared |
| 3 | git add libs/shared |
| 4 | git commit -m "chore(deps): bump shared submodule" |
| 5 | git submodule status |
| 1 | # Workflow: trunk-based short PR |
| 2 | git fetch origin |
| 3 | git switch -c fix/timeout origin/main |
| 4 | # change + test |
| 5 | git commit -am "fix: request timeout" |
| 6 | git push -u origin HEAD |
| 7 | gh pr create --fill |
| 8 | gh pr checks |
| 9 | gh pr merge --squash --delete-branch |
Workflow choice is a team contract. Pick one, write it down, and automate enforcement with branch protection and CI.
Trunk-based (recommended default)
| 1 | git fetch origin |
| 2 | git switch -c feature/small origin/main |
| 3 | # ship in < 2 days |
| 4 | git push -u origin HEAD |
| 5 | gh pr create --fill |
| 6 | gh pr merge --squash --delete-branch |
Git Flow excerpt
| 1 | git switch -c develop origin/develop |
| 2 | git switch -c feature/x |
| 3 | # ... |
| 4 | git switch develop && git merge --no-ff feature/x |
| 5 | git switch -c release/1.3 develop |
| 6 | git switch main && git merge --no-ff release/1.3 |
| 7 | git tag -a v1.3.0 -m "v1.3.0" |
| 8 | git switch develop && git merge --no-ff release/1.3 |
Hotfix path
| 1 | git fetch origin |
| 2 | git switch -c hotfix/sev1 origin/main |
| 3 | # fix + test |
| 4 | git push -u origin HEAD |
| 5 | gh pr create --base main --fill |
| 6 | # cherry-pick onto release branch if needed |
| 7 | git switch release/1.2 |
| 8 | git cherry-pick <hotfix-sha> |
Decision table
- Single deployable product, strong CI → trunk-based
- Multiple supported release lines → release branches + cherry-pick
- Open source with many forks → fork + PR
- Regulated change control → Git Flow-like with protected releases
best practice
| 1 | # Simulate trunk-based locally with bare remote |
| 2 | rm -rf /tmp/wf && mkdir -p /tmp/wf/remote |
| 3 | git init --bare /tmp/wf/remote/app.git |
| 4 | git clone /tmp/wf/remote/app.git /tmp/wf/dev |
| 5 | cd /tmp/wf/dev |
| 6 | git config user.email a@b.c && git config user.name Dev |
| 7 | echo x > f && git add f && git commit -m init && git push -u origin main |
| 8 | git switch -c feature/x |
| 9 | echo y > f && git commit -am feat && git push -u origin HEAD |
| 10 | git switch main && git merge --no-ff feature/x -m merge && git push |
| 11 | git log --oneline --graph |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.