Skip to main content
    Courses/Git/Git Workflows and Best Practices

    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 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.

    Worked example: a full feature-branch cycle
    # 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-deps
    Output
    Already 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).
    Run these commands in a real Git repository in your own terminal.

    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: start a feature branch
    # 🎯 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'.
    Run these in a test repository. Compare your terminal against the expected output in the comments.

    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.

    Worked example: open a PR, address review, merge
    # 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 merge
    Output
    branch '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-auth
    The gh 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.

    Worked example: merge, rebase, and the golden rule
    # 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/main
    Output
    Updating 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.
    Practise on a test repo. Notice rebasing only changes commits nobody else has yet.

    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 versioningMAJOR.MINOR.PATCH — so the version number itself tells people how big a change is.

    Worked example: git stash and git tag
    # 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 fix
    Output
    Saved 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.0
    Run these in a real repository. Remember tags aren't pushed unless you push them.

    Now you try. Fill in the four blanks to shelve some work, restore it, and tag a release — then run it.

    🎯 YOUR TURN: stash, restore, and tag a release
    # 🎯 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.0
    Run these in a test repository and check your output against the expected lines.

    5️⃣ 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.

    Worked example: .gitignore, aliases, and pro habits
    # 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
    Output
    # (these commands set config and write .gitignore — no console output)
    # Verify your aliases took effect:
    $ git config --global --get alias.lg
    log --oneline --graph --all
    These set up your config and ignore rules. Most produce no output — that's expected.

    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

    TaskCommandResult
    New feature branchgit checkout -b feature/xbranch + switch
    Push & trackgit push -u origin feature/xready for a PR
    Merge a branchgit merge feature/xkeeps history
    Tidy your branchgit rebase mainstraight line
    Shelve changesgit stashclean tree
    Restore changesgit stash popre-apply + drop
    Tag a releasegit tag -a v1.2.0 -m "..."named release
    Push a taggit push origin v1.2.0tag 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: a full feature-branch cycle
    # 🎯 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
    Work through it in a throwaway repository — that's the safe place to practise the whole flow.

    🎉 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.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.