Lesson 6 • Advanced
Git Workflows and Best Practices 🌿
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.
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 rebasevsgit merge— and obey the golden rule of rebasing - • Shelve work in progress with
git stash - • Mark releases with
git tagand semantic versioning - • Apply pro habits: small commits, clear messages,
.gitignore, protected main
main) — that would ruin the dish for everyone. Each cook works at their own station (a feature branch); a dish is only plated once the head chef has tasted and approved it (a pull request review). The result: the plate going out the door is always something the whole team is proud of.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-depsAlready on 'main'
Already up to date.
Switched to a new branch 'feature/user-auth'
[feature/user-auth 3a1f9c2] feat: add login form
[feature/user-auth 7b4e012] feat: add auth validation
* [new branch] feature/user-auth -> feature/user-auth
branch 'feature/user-auth' set up to track 'origin/feature/user-auth'.
Switched to branch 'main'
Updating 7b4e012..9d2a0f1
Deleted branch feature/user-auth (was 7b4e012).Your 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 mergebranch 'feature/user-auth' set up to track 'origin/feature/user-auth'.
https://github.com/acme/app/pull/142
[feature/user-auth a90c413] fix: address review — validate email format
Everything up-to-date
✓ Squashed and merged pull request #142
✓ Deleted branch feature/user-authgh commands use GitHub's official CLI. You can do the same from the GitHub website.3️⃣ 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/mainUpdating 9d2a0f1..c44b7a8
Merge made by the 'ort' strategy.
src/login.js | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
Successfully rebased and updated refs/heads/feature/login.4️⃣ 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 fixSaved working directory and index state WIP on feature/search
stash@{0}: WIP on feature/search: 1c2d3e4 feat: add search box
Dropped refs/stash@{0} (a17f0c9...)
v1.0.0
v1.2.0
* [new tag] v1.2.0 -> v1.2.0Now 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.
# A solid .gitignore keeps generated files and secrets OUT of the repo.
# Anything listed here is never tracked, so it can't be committed by accident.
cat > .gitignore <<'EOF'
node_modules/ # installed packages — re-created with 'npm install'
dist/ # build output — re-created by your build step
build/
.env # SECRETS: API keys, passwords — NEVER commit these
.env.local
.DS_Store # OS junk
.idea/ # editor settings
*.log
EOF
# Aliases — type less. Add shortcuts to your global config once.
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.lg "log --oneline --graph --all"
# Now 'git st' runs 'git status', and 'git lg' draws the branch graph.
# The habits that separate pros from beginners:
# - SMALL commits: one logical change each, so they're easy to review & revert
# - CLEAR messages: imperative mood, e.g. "fix: stop double-charging on retry"
# - PROTECT main: require PR review + passing CI; never push to it directly
# - PULL before you push, so you build on the latest work
# - .gitignore secrets and build output from day one# (these commands set config and write .gitignore — no console output)
# Verify your aliases took effect:
$ git config --global --get alias.lg
log --oneline --graph --allPro 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 mainon your private feature branch before the PR keeps history tidy — never rebase shared branches. - 💡 Pull before you push.
git pullfirst so you're building on the latest work and avoid "rejected, non-fast-forward" errors. - 💡 Use
git lg. The aliaslog --oneline --graph --alldraws your whole branch and merge history as a picture.
Common Errors (and the fix)
- Rebasing a shared branch — you ran
git rebaseonmain(or any branch teammates have), and now everyone's history disagrees with yours. The fix: don't. Rebase only private branches; on shared ones, usegit 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. Insteadgit pullto integrate, then push. If you truly must force on your own branch, use the safergit 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 tomainskips review and can break production. Alwaysgit checkout -b feature/...first, and protectmainso 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.gitignoreso 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 |
Frequently Asked Questions
Q: 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.
Q: 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.
Q: 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".
Q: 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.
Q: 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.
Q: 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.
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.
# 🎯 MINI-CHALLENGE: Ship a feature the professional way
# Carry out a full feature-branch cycle from start to finish.
#
# 1. From an up-to-date main, create a branch feature/dark-mode
# 2. Make a commit: "feat: add dark mode toggle"
# 3. Push the branch and set it to track the remote (-u)
# 4. (On GitHub) open a Pull Request and get it reviewed + merged
# 5. Back on main, pull the merge and delete the local branch
# 6. Tag the new release v1.4.0 as an annotated tag, and push the tag
#
# ✅ Expected (your hashes/output will differ):
# Switched to a new branch 'feature/dark-mode'
# [feature/dark-mode ...] feat: add dark mode toggle
# branch 'feature/dark-mode' set up to track 'origin/feature/dark-mode'.
# Deleted branch feature/dark-mode (was ...).
# * [new tag] v1.4.0 -> v1.4.0
# your commands here🎉 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
mergevsrebase— and never rebase shared history - ✅ Shelve work with
git stashand mark releases with annotatedgit tag - ✅ Keep a repo healthy: small commits, clear messages,
.gitignore, protectedmain
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.
Sign up for free to track which lessons you've completed and get learning reminders.