Git Workflow ─ The Complete Guide
From Solo to Team
The real reason Git never sticks ─ you're learning "commands," not "workflow." Memorize commands and you forget them; understand the workflow and it serves you for life. This guide breaks the whole thing down, from solo dev to team collaboration ─ branching strategies, PR conventions, rebase vs merge, conflict resolution, and the 10 commands you'll use every day. Cheatsheet included.
Start with 3 mindsets
01 Git isn't a backup tool ─ it's a time machine
A lot of beginners treat Git as "a way to push code to GitHub for backup" ─ that's using a Ferrari as a bicycle.
Git's real value: you can go back to any past state, see "who changed what, when, and why," and experiment on multiple branches at the same time.
02 Branches are free
A Git branch isn't "a copy of the whole project" ─ it's "a pointer to a particular commit." Creating one costs only a few KB.
So ─ don't be afraid to branch. Trying out an idea? Branch. Fixing a bug? Branch. Running a little experiment? Branch.
03 Commits are written for future you
How well you write today's commit message decides whether the you of three months from now can figure out "why this piece of code looks the way it does."
This has nothing to do with "short vs long commits" ─ it's about "whether the person writing it thought about the future."
The simplest workflow for solo dev
If you haven't joined a company yet and you're just practicing on your own, this is enough:
# 1. Set up the repo git init git remote add origin [email protected]:user/repo.git # 2. New feature, new branch ─ every time git checkout -b feat/login-page # 3. Write code, commit often (key: committing often beats committing rarely) git add src/Login.tsx git commit -m "feat(auth): add login form skeleton" # 4. Done? Push it up git push -u origin feat/login-page # 5. Open a PR on GitHub and review yourself (yes, review your own code) # 6. Merge back into main # 7. Delete the local branch git checkout main git pull git branch -d feat/login-page
The key ─ use the PR flow even when it's just you.
Why? Because it forces you to "pause and look at your own code through a third party's eyes." Most bugs you catch yourself the moment you open the PR.
The 3 team workflows
01 GitHub Flow (simplest, most common)
Best for: 80% of web product teams
Structure:
main─ always shippablefeat/xxx,fix/xxx─ short-lived branches
Flow:
- Branch off
main - Write code and commit on the branch
- Push it up and open a PR
- A teammate reviews + CI passes
- Merge back into
mainand auto-deploy
Pros: simple, great for continuous deployment.
Cons: there's no intermediate "ready to ship" state.
02 GitLab Flow (good when you need release control)
Best for: products that ship in versions (mobile apps, enterprise software)
Structure:
main─ in developmentproduction─ the live versionrelease/v1.2─ a specific release branch
03 Git Flow (complex, for large projects)
Best for: traditional software companies that need a strict release cycle
Structure (the complex one):
main─ livedevelop─ in developmentfeature/xxx─ feature branchesrelease/xxx─ release prephotfix/xxx─ emergency fixes
Pros: rigorous, with a dedicated process for everything.
Cons: too heavy for web products ─ most small teams don't need it.
My advice ─ start with GitHub Flow and level up only when you actually need to.
Branch naming ─ keep it consistent
A naming convention I recommend:
feat/short-description # new feature fix/short-description # bug fix refactor/short-description # refactor docs/short-description # docs test/short-description # tests chore/short-description # chores (dependency bumps, config) # Examples feat/oauth-google-login fix/payment-cancel-bug refactor/extract-pricing-service docs/update-readme chore/upgrade-node-20
Why it helps:
- The branch name alone tells you what it's about
- GitHub groups similar types together when sorting
- It matches your commit message type (feat / fix / refactor)
Commit messages ─ a structured format
I recommend the Conventional Commits format:
<type>(<scope>): <subject> <optional body> <optional footer>
Example:
feat(auth): add OTP for mobile users Mobile users were getting locked out due to SMS delivery delays. Added OTP fallback with 3-minute validity window. Closes #142
Common types:
feat─ new featurefix─ bug fixrefactor─ refactor (no behavior change)docs─ docstest─ testschore─ choresperf─ performance optimization
The upside of this format ─ tools can auto-generate a changelog and auto-bump the version based on your commits.
Rebase vs Merge ─ when to use which
git merge
Combines two branches, keeps all history, and adds an extra "merge commit."
When to use it:
- Merging a branch back into main (feat → main)
- A shared main with multiple people ─ avoid rewriting already-pushed history
git rebase
"Replays" your commits on top of another branch's latest state, turning history into a single straight line.
When to use it:
- Syncing your feature branch to the latest main before opening a PR
- Cleaning up commit history (squash / reword)
The key rule
Don't rebase anything you've already pushed.
Because rebase changes the commit hashes, and the history others have already pulled gets scrambled.
So in practice:
- Personal feature branch (not shared yet) → fine to rebase and clean up
- Shared branch (main / develop / anything others are on) → always merge
Resolving merge conflicts ─ the 4-step method
A lot of beginners freeze on conflicts ─ but there's actually a fixed process:
Step 1: Fetch the latest first
git fetch origin git status # check where things stand
Step 2: Pull the latest main into your branch
# If you haven't pushed yet git checkout feat/your-branch git rebase origin/main # If you've already pushed git checkout feat/your-branch git merge origin/main
Step 3: Open your editor and read the conflict markers
The conflicted file will contain:
<<<<<<< HEAD your version ======= their version >>>>>>> origin/main
Keep one side, or combine both, then delete the <<<, ===, and >>> markers.
Step 4: Mark resolved + continue
git add <filename> git rebase --continue # if you're using rebase # or git commit # if you're using merge
The key ─ if you're unsure, abort:
git rebase --abort # cancel the rebase git merge --abort # cancel the merge
You'll be back to the state before the conflict ─ think it through, then try again. No one is forcing you to resolve a conflict on the spot.
10 commands you'll use every day
| Command | What it does |
|---|---|
git status | See your current state and which files changed |
git diff | See uncommitted changes |
git log --oneline -10 | See the last 10 commits (compact view) |
git add <file> | Add changes to staging |
git commit -m "msg" | Commit (the one you use most) |
git checkout -b <branch> | Create a new branch and switch to it |
git push -u origin <branch> | Push a new branch for the first time |
git pull --rebase | Pull the latest with rebase, no merge commit |
git stash | Stash current changes and clear the working dir |
git reset --soft HEAD~1 | Undo the last commit but keep the changes |
Advanced moves ─ 5 of them
01 Use git rebase -i to clean up history
Before opening a PR, tidy up your messy commits into clean ones:
git rebase -i HEAD~5 # tidy up the last 5 commits
Inside the editor you can:
pick─ keep itsquash─ merge into the previous onereword─ change the commit messagedrop─ delete this commit
This makes your PR look like a senior wrote it.
02 Use git stash to switch tasks
You're mid-feature and the PM suddenly says "there's a bug in production, fix it now":
git stash # stash your current changes git checkout main git checkout -b fix/urgent-bug # fix the bug, merge back into main git checkout feat/your-feature git stash pop # bring back your changes and keep going
Way cleaner than "throwing in a random WIP commit."
03 git bisect to find which commit introduced a bug
You know the code was bug-free a week ago and it's broken today ─ no need to check commits one by one:
git bisect start git bisect bad # current state has the bug git bisect good <commit-from-a-week-ago> # Git auto-checks out a commit in the middle; you test it # No bug → git bisect good # Has bug → git bisect bad # Git binary-searches its way to the exact commit that introduced the bug
On a big codebase, this command is a lifesaver.
04 Make good use of .gitignore and .gitattributes
A basic .gitignore:
# Node node_modules/ .env* !.env.example # Editor .vscode/ .idea/ *.swp # OS .DS_Store Thumbs.db # Build dist/ build/ *.log
05 Add a pre-commit hook
Run checks automatically before each commit (lint, format, test):
# husky + lint-staged (the most common combo)
npm i -D husky lint-staged
npx husky init
# In .husky/pre-commit, write
npx lint-staged
# In package.json
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"]
}
Once it's set up, every commit gets checked automatically ─ no more "I thought I fixed it but it's broken."
3 scenarios that scare beginners
01 "I committed the wrong thing"
# Not pushed yet git reset --soft HEAD~1 # undo the commit, keep the changes # fix it and commit again # Already pushed # Don't git push --force (unless it's your own branch) # Use git revert <commit> to make a "reverse commit"
02 "I worked on the wrong branch"
# Not committed yet git stash git checkout <the-right-branch> git stash pop # Committed but not pushed git checkout <the-right-branch> git cherry-pick <that commit hash> git checkout <the-wrong-branch> git reset --hard HEAD~1 # remove the commit from the wrong branch
03 "I accidentally committed my .env"
# Immediately rotate every secret in .env (passwords, API keys) # Then remove it from history completely # Use git filter-repo (recommended) or BFG Repo Cleaner git filter-repo --path .env --invert-paths # Force push (this rewrites remote history ─ coordinate with the team first) git push --force
The key ─ don't pretend it's fine. A committed secret is already leaked ─ rotate it immediately, then scrub the history.
One last reminder
Git's value isn't learning the commands ─ it's building the habits.
Before you write code ─ branch.
Every time you finish one small thing ─ commit.
Every commit ─ write a message for future you.
Build these 3 habits for a month and you'll find Git becomes the tool that "lets me change code without fear" ─ because you know any mistake can be undone.
Git isn't something engineers need to learn ─ it's working like an engineer, itself.
Stuck on Git and want someone to walk you through it?
A 30-minute 1-on-1 session for NT$1,500 ─ I'll run you through a full workflow and tackle the conflict / rebase problems your repo is actually stuck on.