Git Workflows and Best Practices
Reviewed & published by Brayan K
By the end of this final Git lesson you'll run the real workflow professional teams use — feature branches, pull requests and code review — and know when to rebase vs merge, how to stash and tag, and the habits that keep a shared repo healthy.
Part of the free Git course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • Run the feature-branch workflow: branch, commit, merge through review
- • Open a pull request and respond to code review
- • Choose git rebase vs git merge — and obey the golden rule of rebasing
- • Shelve work in progress with git stash
- • Mark releases with git tag and semantic versioning
- • Apply pro habits: small commits, clear messages, .gitignore, protected main
1️⃣ The Feature-Branch Workflow
This is the workflow almost every team uses, and it rests on three rules: main is always deployable, every change lives on its own branch, and work reaches main only through a reviewed pull request. A branch is just an isolated line of work, so your half-finished feature can never break what's live. Read this worked example, then you'll run your own.
# Feature Branch Workflow — the workflow almost every team uses.
# Three rules: main is ALWAYS deployable; every change gets its OWN branch;
# work reaches main only after a Pull Request has been reviewed.
# 1. Start from an up-to-date main
git checkout main
git pull origin main # grab everyone else's merged work first
# 2. Create a branch named for the work you're about to do
git checkout -b feature/user-auth
# 3. Develop, committing in small steps as you go
git commit -m "feat: add login form"
git commit -m "feat: add auth validation"
# 4. Push the branch, then open a Pull Request on GitHub/GitLab.
# Teammates review it, CI runs the tests, and it merges once approved.
git push -u origin feature/user-auth
# 5. After it's merged, return to main, pull the merge, delete the branch
git checkout main && git pull
git branch -d feature/user-auth # tidy up — the branch has served its purpose
# Common branch-name prefixes:
# feature/ new features feature/user-dashboard
# fix/ bug fixes fix/login-redirect
# hotfix/ urgent prod fix hotfix/security-patch
# chore/ maintenance chore/update-depsYour turn. The cycle below is almost complete — fill in the two blanks marked ___ using the hints in the comments, then run it.
# 🎯 YOUR TURN — fill in each ___, then run the commands in a real repo.
# You're starting a new feature called "search". Begin from a fresh main.
git checkout main
git pull origin main
# 1) Create AND switch to a branch named feature/search in one command
git checkout ___ feature/search # 👉 the flag that creates a new branch
# ...you add a search box and commit it...
git commit -m "feat: add search box"
# 2) Push the branch and set it to track the remote (so 'git push' works later)
git push ___ origin feature/search # 👉 the flag for "set upstream", short form
# ✅ Expected output (your commit hash will differ):
# Switched to a new branch 'feature/search'
# [feature/search 1c2d3e4] feat: add search box
# branch 'feature/search' set up to track 'origin/feature/search'.2️⃣ Pull Requests & Code Review
A pull request (PR) is a request to merge your branch into main, with a built-in place for discussion. It's where code review happens (a teammate reads your changes and leaves comments) and where CI runs the tests automatically. You address feedback with ordinary commits — the PR updates itself. The single biggest factor in good review is size: small PRs get read carefully, giant ones get rubber-stamped.
# A Pull Request (PR) is a request to merge YOUR branch into main, with a
# built-in space for review. It is where code review and CI happen.
# You usually open it in the web UI, but the GitHub CLI does it from the terminal:
# Push your branch, then open the PR
git push -u origin feature/user-auth
gh pr create --title "Add user authentication" \
--body "Adds login form, validation, and session handling."
# A reviewer leaves comments. You address them with NORMAL commits:
git commit -m "fix: address review — validate email format"
git push # the PR updates automatically
# Once approved AND the checks are green, merge it (squash keeps history tidy):
gh pr merge --squash --delete-branch
# What good review looks like:
# - small PRs (a few hundred lines) get reviewed properly; huge ones don't
# - the PR description says WHAT changed and WHY
# - main is PROTECTED: no direct pushes, review + passing CI required to merge3️⃣ Rebase vs Merge (and the Golden Rule)
Both merge and rebase combine two branches, but in opposite styles. Merge records a truthful, branching history and adds a merge commit. Rebase replays your commits on top of another branch to make history a clean straight line — but it rewrites commit IDs. That rewrite is exactly why there's a golden rule: never rebase history other people already have. Rebase your own private branch to tidy it; never rebase a shared branch like main.
# Both merge and rebase combine work from two branches — differently.
# MERGE keeps both histories and adds a "merge commit" that ties them together.
# History is truthful but can look like a braided rope on busy projects.
git checkout main
git merge feature/login # creates a merge commit on main
# REBASE replays your branch's commits on top of the latest main, as if you
# had started from main today. History becomes a clean, straight line.
git checkout feature/login
git rebase main # moves your commits on top of main's tip
# ...fix any conflicts, then: git rebase --continue
# ⚠️ THE GOLDEN RULE OF REBASE:
# Never rebase commits that other people already have.
# Rebasing REWRITES commit IDs. If a branch is shared and you rebase it,
# everyone else's copy now disagrees with yours — chaos and lost work.
#
# ✅ Safe: rebase YOUR OWN branch that nobody else has pulled.
# ❌ Unsafe: rebase main, develop, or any branch teammates are using.
# A common, safe pattern: rebase your private feature branch onto main to
# tidy it up just before you open the PR.
git checkout feature/login
git fetch origin
git rebase origin/main4️⃣ Stashing Work & Tagging Releases
git stash shelves uncommitted changes so your working tree is instantly clean — perfect for "I'm mid-edit but must switch branches right now." git tag marks a commit as a named release so you can always find exactly what shipped. Use semantic versioning — MAJOR.MINOR.PATCH — so the version number itself tells people how big a change is.
# git stash — shelve unfinished changes so your working tree is clean.
# Perfect for "I'm mid-edit but must switch branches to fix an urgent bug."
git stash # tuck away ALL uncommitted changes
git stash list # stash@{0}: WIP on feature/search: 1c2d3e4 ...
# ...now your branch is clean — switch, fix the bug, come back...
git stash pop # re-apply your changes AND drop the stash
git stash push -m "half-done search filters" # a named stash is easier to find
# git tag — mark a specific commit as a release (a fixed, named point in history).
git tag v1.0.0 # lightweight tag (just a name)
git tag -a v1.2.0 -m "Release 1.2.0" # annotated tag (recommended for releases)
git tag # list all tags
git push origin v1.2.0 # tags are NOT pushed by default — push it
git push origin --tags # or push every tag at once
# Tags use Semantic Versioning: MAJOR.MINOR.PATCH
# v2.0.0 breaking change v1.3.0 new feature v1.2.1 bug fixNow you try. Fill in the four blanks to shelve some work, restore it, and tag a release — then run it.
# 🎯 YOUR TURN — fill in each ___, then run it in a real repo.
# You're mid-edit when a teammate asks you to review their PR. Shelve your work.
git ___ # 👉 the command that tucks away uncommitted changes
# ...you switch branches, review the PR, then come back to your branch...
# Bring your shelved changes back and remove them from the stash list
git stash ___ # 👉 the subcommand that re-applies AND drops it
# Later, you ship version 2.0.0. Create an ANNOTATED release tag for it.
git tag ___ v2.0.0 -m "Release 2.0.0" # 👉 the flag that makes a tag annotated
# Push that one tag to the remote
git push origin ___ # 👉 the tag name you just created
# ✅ Expected output (hashes will differ):
# Saved working directory and index state WIP on main
# Dropped refs/stash@{0} (a17f0c9...)
# * [new tag] v2.0.0 -> v2.0.05️⃣ Best Practices That Make You a Pro
Tools are only half the job — habits are the rest. Small, focused commits are easy to review and easy to revert. Clear messages in the imperative mood ("fix: stop double-charging on retry") turn your history into documentation. A good .gitignore keeps secrets and build junk out of the repo, and a protected main branch (no direct pushes, review required) stops accidents reaching production.
Pro Tips
- 💡 Keep branches short-lived. A branch that lives a day or two merges cleanly; one that lives three weeks becomes a conflict minefield.
- 💡 Rebase only your own branch. A quick git rebase main on your private feature branch before the PR keeps history tidy — never rebase shared branches.
- 💡 Pull before you push. git pull first so you're building on the latest work and avoid "rejected, non-fast-forward" errors.
- 💡 Use git lg. The alias log --oneline --graph --all draws your whole branch and merge history as a picture.
Common Errors (and the fix)
- Rebasing a shared branch — you ran git rebase on main (or any branch teammates have), and now everyone's history disagrees with yours. The fix: don't. Rebase only private branches; on shared ones, use git merge.
- "Updates were rejected (non-fast-forward)" → reaching for git push --force — force-pushing to a shared branch overwrites teammates' commits and is how work gets destroyed. Instead git pull to integrate, then push. If you truly must force on your own branch, use the safer git push --force-with-lease.
- Giant commits — a single commit touching 40 files with the message "updates" is impossible to review or revert. Make one small commit per logical change, with a clear message.
- Working directly on main — committing straight to main skips review and can break production. Always git checkout -b feature/... first, and protect main so it's enforced for everyone.
- "Committed a secret to .env" — once pushed, a key in history is compromised. Rotate the key immediately, then add the file to .gitignore so it never happens again.
📋 Quick Reference
| Task | Command | Result |
|---|---|---|
| New feature branch | git checkout -b feature/x | branch + switch |
| Push & track | git push -u origin feature/x | ready for a PR |
| Merge a branch | git merge feature/x | keeps history |
| Tidy your branch | git rebase main | straight line |
| Shelve changes | git stash | clean tree |
| Restore changes | git stash pop | re-apply + drop |
| Tag a release | git tag -a v1.2.0 -m "..." | named release |
| Push a tag | git push origin v1.2.0 | tag on remote |
Mini-Challenge: Ship a Feature Properly
No blanks this time — just a brief and an outline. Carry out a complete feature-branch cycle end to end in a test repo, then check your output against the example in the comments. This is exactly how real features get shipped.
🎉 Course Complete!
That's the whole Git course — from your first commit to running the same workflow professional teams use every day. You can now:
- ✅ Work the feature-branch workflow: branch, commit small, merge via review
- ✅ Open pull requests and respond to code review with normal commits
- ✅ Choose merge vs rebase — and never rebase shared history
- ✅ Shelve work with git stash and mark releases with annotated git tag
- ✅ Keep a repo healthy: small commits, clear messages, .gitignore, protected main
Where next? Put it into practice: open a pull request on a real GitHub project — your own, or your first open-source contribution. Then go deeper with GitHub Actions for CI/CD automation, and explore git bisect (find the commit that introduced a bug) and git cherry-pick (copy one commit between branches). You've got the foundation to collaborate on any codebase with confidence.
Practice quiz
In the feature-branch workflow, what is always true of main?
- It holds only experiments
- It should always be deployable
- It is never merged into
- It is a private branch
Answer: It should always be deployable. A core rule of the feature-branch workflow is that main stays always deployable; work happens on branches.
What is a pull request (PR)?
- A command that downloads commits
- A way to delete a branch
- A request to merge your branch into main, with built-in review
- A type of git tag
Answer: A request to merge your branch into main, with built-in review. A PR proposes merging your branch into main and is where code review and CI happen.
How do you address review feedback on an open PR?
- Make normal commits and push; the PR updates automatically
- Close the PR and open a new one
- Force-push over main
- Email the reviewer the diff
Answer: Make normal commits and push; the PR updates automatically. You push ordinary follow-up commits to the branch and the PR refreshes itself.
What does git rebase do to history?
- Adds a merge commit tying two branches
- Deletes commits permanently
- Pushes to the remote
- Replays your commits on top of another branch for a straight, linear history
Answer: Replays your commits on top of another branch for a straight, linear history. Rebase replays your commits onto the tip of another branch, producing a clean linear history but new commit IDs.
What is the golden rule of rebasing?
- Always rebase before every commit
- Never rebase commits other people already have
- Rebase main daily
- Rebase only shared branches
Answer: Never rebase commits other people already have. Rebasing rewrites commit IDs, so rebasing shared history breaks everyone else's copy.
What does git stash do?
- Shelves uncommitted changes so your working tree is clean
- Commits all changes
- Tags a release
- Pushes to origin
Answer: Shelves uncommitted changes so your working tree is clean. git stash tucks away uncommitted changes without a commit, perfect for switching branches quickly.
Are tags pushed to the remote by a normal git push?
- Yes, always
- Only annotated tags
- No — you must push them explicitly
- Only on the first push
Answer: No — you must push them explicitly. A normal push sends branch commits only; push a tag with git push origin v1.2.0 or all with --tags.
Which command creates an annotated release tag?
- git tag v1.2.0
- git tag --release v1.2.0
- git release v1.2.0
- git tag -a v1.2.0 -m "Release 1.2.0"
Answer: git tag -a v1.2.0 -m "Release 1.2.0". git tag -a -m creates an annotated tag that stores the author, date, and message, recommended for releases.
In semantic versioning MAJOR.MINOR.PATCH, what does a bump to MAJOR signal?
- A breaking change
- A small bug fix
- A documentation tweak
- A new patch release
Answer: A breaking change. MAJOR increments for breaking changes; MINOR for new features; PATCH for bug fixes.
Which is a best practice for a healthy shared repo?
- Commit secrets so the team can share them
- Push giant 40-file commits
- Protect main and require review plus passing CI before merging
- Always work directly on main
Answer: Protect main and require review plus passing CI before merging. Protecting main with required review and CI keeps unreviewed or broken code out of production.
Continue this course
- Previous: Resolving Merge Conflicts
- Next: The Staging Area & git diff — Stage changes precisely with git add -p and inspect them with git diff
- Quick reference: Git cheat sheet
Frequently asked questions
What is the difference between git merge and git rebase?
Both combine work from two branches. Merge ties them together with a new merge commit, preserving the true, branching history. Rebase replays your commits on top of another branch so history becomes a single straight line — cleaner to read, but it rewrites commit IDs. Use merge for shared branches; use rebase only to tidy your own private branch before opening a pull request.
What is the golden rule of rebasing?
Never rebase commits that other people already have. Rebasing creates brand-new commits with different IDs, so the rewritten history no longer matches the copies your teammates pulled. That forces ugly fixes and can lose work. Only rebase a branch that is exclusively yours and that nobody else has based work on — never main, develop, or any shared branch.
What does git stash do?
git stash shelves your uncommitted changes and gives you a clean working tree, without making a commit. It's ideal when you're mid-edit but need to switch branches to fix an urgent bug. Run git stash to shelve, do your other work, then git stash pop to bring your changes back and remove them from the stash list. You can keep several stashes and name them with git stash push -m "message".
Why aren't my tags showing up on GitHub after I push?
Git does not push tags by default — a normal git push only sends branch commits. Push a single tag with git push origin v1.2.0, or push them all with git push origin --tags. For releases, prefer annotated tags (git tag -a v1.2.0 -m "...") over lightweight ones, because annotated tags store the author, date, and message.
What should go in a .gitignore file?
Anything that is generated or secret. That means build output (dist/, build/), installed dependencies (node_modules/, vendor/), editor and OS files (.idea/, .DS_Store), log files, and above all secrets like .env containing API keys or passwords. Listing them means Git never tracks them, so they can't be committed by accident. Add the .gitignore on day one — removing a leaked secret from history afterwards is painful.
What's the best Git workflow for a small team?
The Feature Branch workflow. Keep main always deployable, do every change on its own short-lived branch, and merge into main only through a reviewed pull request with passing CI. It's simple, scales from solo projects to large teams, and underpins GitHub Flow. Keep branches small and short (a day or two) to avoid painful merge conflicts.