
Introduction 🎯
Git is the single most important tool in a developer’s toolbox. Yet many developers only learn the basics and miss workflows that make collaboration safer and faster. This guide focuses on practical tips you can use today: safer commits, clean history, branch strategies, undoing mistakes, and commands you’ll use daily.
Keep this post as your quick-reference checklist when you reach for the terminal.
Core Principles
- Make small, focused commits. Each commit should represent one logical change. Small commits are easier to review and revert.
- Prefer a clean history for shared branches. Use interactive rebase to tidy feature branches before merging.
- Protect
main/trunk. Only merge reviewed, CI-green changes. - Use descriptive branch names. e.g.
feature/auth-rate-limit,fix/login-typo,chore/deps.
Visual Workflow (how Git fits into shipping)
graph TD Dev[Developer] -->|edit| Work[Local Working Tree] Work -->|stage| Index[Staging Area] Index -->|commit| LocalRepo[(Local Repo)] LocalRepo -->|push| Remote[(Remote Repo)] Remote -->|open PR| CI[CI / Checks] CI -->|pass| Merge[Merge to main] Merge -->|deploy| Prod[Production]
This simple flow shows the canonical path from local edits to production: stage → commit → push → PR → CI → merge → deploy.
Command Reference — detailed breakdowns
For each command below: what it does / when to use it / Do / Don’t / Example.
-
git clone <repo>- What: makes a local copy of a remote repository (including .git history).
- When: first time you work on a repo or to create a fresh workspace.
- Do: clone over SSH for write access; use
--depth=1for quick read-only clones. - Don’t: edit or push to a shallow clone if you need long history or rebases.
- Example:
git clone git@github.com:owner/repo.git
-
git switch -c <branch>(orgit checkout -b)- What: create and switch to a new branch.
- When: start a feature, bugfix, or experiment.
- Do: name branches descriptively:
feature/auth-rate-limit,fix/login-typo. - Don’t: use branches with ambiguous names like
workortempin shared repos. - Example:
git switch -c feature/login-retry
-
git add -p/git add <file>- What: stage changes;
-plets you choose hunks interactively. - When: before committing;
-pis great to make atomic commits from mixed edits. - Do: use
git add -pto separate unrelated work into multiple commits. - Don’t: stage everything blindly when part of changes belong to different logical units.
- Example:
git add -p src/server.js
- What: stage changes;
-
git commit -m "msg"(and message conventions)- What: records staged changes into a new local commit.
- When: after staging a coherent, testable change.
- Do: use imperative messages:
fix: handle nil responseand include body for rationale. - Don’t: commit large, multi-concern patches without description or tests.
- Example:
git commit -m "feat: add avatar upload endpoint"
-
git log --oneline --graph --decorate --all- What: shows compact commit history and branch graph.
- When: inspecting history or locating commits.
- Do: use when reviewing recent merges or debugging regressions.
- Don’t: rely on
git logonly for finding bugs — usegit bisectwhen needed.
-
git fetch originthengit merge origin/main(merge update)- What: fetch remote refs and merge main into your branch, creating a merge commit.
- When: you want a safe update that preserves branch history and records merges.
- Do: use for long-running branches where merge commits are acceptable.
- Don’t: use merge if your team enforces linear history without merge commits.
-
git fetch originthengit rebase origin/main(rebase update)- What: rebase your local commits onto the new tip of
origin/main, producing a linear history. - When: before opening or updating PR to keep history clean.
- Do: rebase interactively (
git rebase -i) to squash or fixup small commits. - Don’t: rebase public commits that others have pulled — that rewrites shared history.
- Example:
git fetch origin && git rebase origin/main
- What: rebase your local commits onto the new tip of
-
git push -u origin <branch>- What: push your branch to remote and create an upstream tracking ref.
- When: first push of a new branch.
- Do: set
-uonce so futuregit push/git pullare easier.
-
git push --force-with-lease- What: force-push updates but only if remote hasn’t advanced (safer than
--force). - When: after an interactive rebase of your branch.
- Do: prefer
--force-with-leaseto avoid clobbering others’ pushes. - Don’t: force-push to shared branches like
main.
- What: force-push updates but only if remote hasn’t advanced (safer than
-
git reset --soft HEAD~1/--mixed/--hard- What: move branch pointer;
--softkeeps staged changes,--harddiscards work. - When: undo local commits during local cleanup.
- Do: use
--softto uncommit but keep work; use--hardonly when safe. - Don’t:
--hardon changes you haven’t backed up or pushed.
- What: move branch pointer;
-
git revert <sha>- What: create a new commit that undoes the changes of a previous commit (safe for shared history).
- When: undoing a bad commit that has already been pushed and is shared.
- Do: revert rather than reset for public branches.
-
git reflog/ recovering commits- What: local log of HEAD movements useful for recovering lost commits.
- When: after an accidental reset or branch deletion.
- Do: run
git reflogto find the lost hash, thengit checkout <hash>and recreate a branch.
-
git stash/git stash push -m "wip"- What: temporarily save uncommitted changes.
- When: when you need a clean working tree to switch branches or apply a hotfix.
- Do: use named stashes (
-m) to remember content;git stash popto restore. - Don’t: rely on stashes as long-term storage.
-
git cherry-pick <sha>- What: apply a single commit from another branch onto current branch.
- When: backporting a bug fix or applying a single change without merging whole branch.
- Do: cherry-pick small, well-audited commits.
- Don’t: cherry-pick large or complex changes without testing.
-
git bisect- What: binary search across commits to find the one that introduced a bug.
- When: when you know the commit range that introduced a regression.
- Do: write a reliable test or script for
git bisect runto automate.
-
git tag -a vX.Y.Z -m "msg"- What: annotate release points; useful for releases and rollbacks.
- When: during release process.
- Do: use annotated tags for release notes and signatures.
Common Workflows (with mermaid)
Feature branch → PR flow:
sequenceDiagram participant D as Dev participant L as Local participant R as Remote participant C as CI D->>L: make changes, git add -p, git commit L->>R: git push origin feature/x R->>C: PR opens, run tests C-->>R: tests pass R->>R: merge to main (squash or merge) R->>Prod: deploy
Hotfix flow (production issue):
graph LR ProdIssue --> HotfixBranch HotfixBranch --> Test Test --> MergeMain MergeMain --> Deploy MergeMain --> BackportToDevelop
Do / Don’t — Practical Rules
- Do: make each commit single-purpose and testable.
- Don’t: mix refactors + feature work in one commit.
- Do: keep PRs small; reviewers are more likely to approve quickly.
- Don’t: push
--forceto shared branches. - Do: protect
mainwith branch rules and require CI to pass before merging. - Don’t: skip CI in order to merge faster.
Solo/SmallTeam vs Industry/LargeTeam — tradeoffs & recommendations
Solo / SmallTeam
- Low ceremony, speed-first: Trunk-Based/GitHub Flow is recommended.
- Use short-lived feature branches and merge on green.
- Simpler tooling: GitHub Actions + minimal monitoring.
- Do: automate deploys, keep single-source-of-truth for infra config, use managed DBs.
- Don’t: overcomplicate with enterprise tooling too early.
Industry / LargeTeam
- Higher ceremony for safety: protected branches, release branches, required reviews, RBAC.
- Use merge commits or enforced squash semantics per team policy; adopt CI gates, canaries, and feature flags.
- Do: enforce commit conventions (Conventional Commits), use automation (semantic-release), integrate security scans.
- Don’t: rely on manual approvals only—automate checks and record audit trails.
Specific tradeoffs:
- Rebasing: great for tidy history, but avoid on public shared branches in large teams unless workflows are agreed.
- Force-push: acceptable on private feature branches in small teams after coordination; in large teams it should be restricted.
Troubleshooting common blockers
- ‘merge conflicts’ during rebase: stop, fix conflicts,
git addthe resolved files, thengit rebase --continue. - ‘detached HEAD’: create a branch
git switch -c rescueto preserve work. - ‘lost commits after reset’: use
git reflogto find and recover.
Quick reference — examples & snippets
Create a branch, work, rebase, and push:
git switch -c feature/cool
# make changes
git add -p
git commit -m "feat: add cool feature"
git fetch origin
git rebase origin/main
git push -u origin feature/cool
Safely revert a bad pushed commit:
git revert <sha>
git push origin main
Recover deleted branch:
git reflog
git branch recovered/<short> <commit-hash>
Final Notes
Git is simple at the surface and powerful under the hood. With small habits—atomic commits, clear branches, protected main, and CI—you can avoid most blockers. Tell me if you want:
- a
CONTRIBUTING.mdtemplate with branch rules and commit message guidelines, or - a
pre-commit+huskyconfig and samplepackage.jsonadditions to enforce checks locally.
Essential Commands (quick reference)
# clone a repo
git clone git@github.com:owner/repo.git
# create and switch to a feature branch
git switch -c feature/my-feature
# stage changes selectively
git add -p
# commit with a message
git commit -m "feat: add user avatar upload"
# see concise history
git log --oneline --graph --decorate --all
# update feature branch from main (merge)
git fetch origin
git merge origin/main
# update feature branch from main (rebase) — preferred for tidy history
git fetch origin
git rebase origin/main
# push branch (first push creates tracking)
git push -u origin feature/my-feature
# force-push after rebase (careful)
git push --force-with-lease
Commit Hygiene and Atomic Changes
- Use present-tense, imperative commit messages:
fix: handle null response. - Keep the message body to explain why, not what (the code shows what).
- Use
git add -pto craft atomic commits from a larger working tree.
If you accidentally included unrelated changes, split them before pushing:
# interactively stage hunks
git add -p
# create a commit for staged hunks
git commit -m "chore: update README"
# remaining changes can be staged separately
git add <files>
git commit -m "feat: add healthcheck endpoint"
Merge vs Rebase — Which to use?
- Rebase: Rewrites your feature branch onto the tip of the target branch. Use for a linear, readable history before merging a feature branch. Rebase locally, then
--force-with-leasepush. - Merge: Creates a merge commit combining histories. Safe and preserves original branch context; good for long-running release branches or when you want explicit merge points.
Recommended workflow for feature branches:
- Work on
feature/*locally, commit often. - Rebase interactively to squash/fixup and clean history:
git rebase -i origin/main. - Push and open a pull request.
- Merge using GitHub/Platform merge strategy your team prefers (merge commit or squash).
Note: avoid rebasing public commits shared with others unless the team agrees.
Branching Strategies — Quick Comparison
- Trunk-Based Development: Short-lived feature branches or direct commits to
mainwith feature flags. Fast and simple; best for small teams and CI-driven workflows. - GitHub Flow:
mainis always deployable; create short-lived branches, open PRs, and merge when green. - Gitflow: Feature, develop, release, hotfix branches. More ceremony; useful for projects with formal release cycles.
Which to pick:
- If you ship frequently and have CI/CD: prefer Trunk-Based or GitHub Flow.
- If you maintain long-lived stable releases: consider Gitflow.
Pull Request & Review Tips
- Make PRs small and focused — aim for < 400 lines changed.
- Describe the problem, solution, and testing steps in the PR description.
- Link relevant issue numbers and CI runs.
- Request specific reviewers and add context for non-obvious design choices.
Use the PR to run automated checks: linters, unit tests, type checks, and security scans. Block merges on failing checks.
Undoing Mistakes — Practical Recipes
- Uncommit the last commit but keep changes staged:
git reset --soft HEAD~1
- Drop the last commit and discard changes:
git reset --hard HEAD~1
- Restore a deleted branch:
# find the commit where the branch pointed
git reflog
# recreate branch at the commit hash
git branch my-branch <commit-hash>
- Revert a pushed commit (safe for shared history):
git revert <sha>
git push origin main
- Recover a lost commit (useful after an accidental reset):
git reflog # locate the lost commit hash
git checkout <hash> # inspect
git branch recovery/<short> <hash>
Releases, Tags & Changelogs
- Create lightweight tags for releases:
git tag -a v1.2.0 -m "Release v1.2.0: add auth improvements"
git push origin v1.2.0
-
Use annotated tags (
-a) for release notes and signatures when needed. -
Automate changelogs with commit message conventions (e.g. Conventional Commits) and a release tool (semantic-release).
CI/CD & Protected Branches
- Protect
mainwith branch protection rules: require PR reviews, status checks, and passing CI. - Run quick checks in your local pre-commit hooks to catch style and lint errors early (see Husky, lint-staged).
Example lint-staged config in package.json:
"lint-staged": {
"*.ts": ["eslint --fix", "prettier --write"],
"*.md": ["prettier --write"]
}
Helpful Tips & Shortcuts
git stash push -m "wip"— temporarily save work in progress.git cherry-pick <sha>— move a commit between branches.git bisect— find the commit that introduced a bug.git shortlog -sn— see contribution summary.
Conclusion
Good Git habits scale: they reduce friction, speed up reviews, and make rollbacks predictable. Start small—clean commits, descriptive branches, and protected main. When the team grows, your history will remain understandable and your releases reliable.