Git Best Practices Every Developer Should Know
One git push origin main --force on a Friday afternoon can overwrite the remote and wipe out two weeks of commits from the rest of the team. The build breaks, and recovery means spending the weekend stitching the history back together from local reflogs. It’s a common enough disaster, and it points to something worth saying plainly: Git isn’t a backup tool, it’s a collaboration protocol.
Writing working code is only half the job. The other half is integrating that code into a shared repository without clobbering the history, overwriting your peers, or breaking CI.
The difference between someone who “knows Git commands” and a senior engineer isn’t raw intelligence — it’s a set of disciplined habits. This guide covers branching strategies, how the commit log actually works, automating conflict resolution, and the emergency commands worth knowing before you need them.
Prevent leaks before they happen. A single leaked
.envfile with an AWS access key can cost thousands within minutes via crypto-mining bots. Set your repository boundaries with our local Gitignore Generator before initializing the repo.
1. Semantic Commit Messages
When a production bug surfaces at 2 a.m., the on-call engineer leans on git bisect and the commit log to find when the bug was introduced.
If your commit log looks like this, you’re making that harder than it needs to be:
❌ "fix bug"
❌ "update files"
❌ "WIP"
❌ "asdfasdf"
❌ "finally works"
These messages are useless — they force the on-call engineer to open the diff of every commit to figure out what changed.
Conventional Commits
The common standard is the Conventional Commits specification. It uses a machine-readable format that gives context to both humans and CI/CD pipelines.
type(scope): short description (under 50 characters)
Longer explanation if needed. Explain WHY the change was made,
not WHAT the code does (the diff already shows the what).
Fixes Jira-1234
The seven core types:
feat: a new feature (e.g.,feat(auth): implement Google OAuth 2.0).fix: a bug fix (e.g.,fix(cart): prevent negative checkout quantities).refactor: changing structure without changing behavior (e.g.,refactor(api): extract DB validation logic).perf: changes that improve runtime performance or memory usage.docs: updates to the README, JSDoc comments, or Swagger definitions.chore: routine maintenance, dependency upgrades, or CI/CD changes.test: adding unit, integration, or end-to-end tests.
The CI/CD payoff: semantic commits let tools like Semantic Release parse your history, generate release notes, and bump your SemVer tags automatically.
Enforcing it with Husky hooks
Don’t rely on people remembering the rules — enforce them at the Git layer with Git hooks.
Install husky and commitlint in your Node.js project to intercept git commit. If someone runs git commit -m "wip", the hook rejects it until the message is formatted correctly. That keeps the history clean.
2. Branching Strategies
A branching strategy decides how several engineers work on the same codebase at once without overwriting each other or producing constant merge conflicts.
The legacy model: GitFlow
GitFlow is a multi-tiered strategy popularized in the early 2010s.
- It keeps two permanent branches:
main(production releases) anddevelop(daily integration). - It uses temporary
feature/,release/, andhotfix/branches merged back and forth.
When to use it: only when you have to support multiple historical versions at once (e.g., maintaining v2.0 for on-premise clients while building v3.0). For modern SaaS apps, GitFlow adds unnecessary overhead.
The modern standard: trunk-based development (GitHub Flow)
For most web apps, SaaS products, and libraries, trunk-based development is the better model. It prioritizes iteration speed, continuous integration, and automated testing.
main ──────────────────────────────────────────────→ (Production)
\ /
feature/add-search ─── (Pull Request + Automated CI Tests)
The rules:
- The
mainbranch (the trunk) stays green and deployable at all times. - Developers branch directly off
maininto short-lived feature branches (feature/user-profile). - Keep branches short. Aim for 1–3 days. Long-lived branches (a three-week rewrite) lead to painful merge conflicts when they rejoin the trunk.
- Code enters
mainonly through pull requests that pass CI checks (tests, linters) and at least one peer review. - Delete the feature branch right after merging.
3. Rebase vs Merge
This is one of the most debated topics in Git. Understanding how each handles history is what matters.
git merge (3-way merge)
git checkout main
git merge feature/new-ui
What it does: ties your feature branch into main with a new merge commit.
Result: it preserves the exact timeline of when branches diverged and merged. But with many active developers, the history becomes a tangle of crossing lines (the “railroad track” look).
git rebase (linear history)
git checkout feature/new-ui
git rebase main
What it does: rewinds your feature commits, pulls in the latest main, then replays your commits on top.
Result: a straight, linear history — no crossing lines, no merge commits. It reads as if you wrote the code in one sequence.
The golden rule of rebasing
Never rebase a shared, remote branch. Rebasing rewrites the commit SHAs, so if you rebase a branch someone else is working on, their local repo will conflict with the remote. Rebase only local, private branches.
Automating conflict resolution with git rerere
If you rebase often, you may hit the same conflict repeatedly. Enable Reuse Recorded Resolution (rerere):
git config --global rerere.enabled true
When you resolve a conflict, Git records how you fixed it. The next time the same conflict appears during a rebase, Git applies the fix automatically.
4. .gitignore: Your First Line of Defense
A missing or misconfigured .gitignore is behind some of the most expensive security mistakes in software.
If someone commits a 2GB database dump (.sql) or a node_modules directory, it bloats the repo forever. Even if you delete the file in a later commit, Git keeps it in history — so everyone who clones the repo downloads that ghost file.
Worse, if someone commits an .env file with an AWS secret key, bots scanning GitHub find it and spin up thousands of dollars of crypto-mining within a minute.
Essential exclusions
# 1. Package dependencies
node_modules/
vendor/
.venv/
# 2. Environment variables and SECRETS (critical)
.env
.env.local
.env.production
*.pem
*.key
credentials.json
# 3. Build outputs
dist/
build/
.next/
out/
# 4. OS and IDE artifacts
.DS_Store
Thumbs.db
.idea/
.vscode/settings.json
# 5. Logs and local databases
*.log
*.sqlite3
dump.sql
Recovering from a leaked key: you can’t just use git rm — the secret stays in history. Use history-rewriting tools like BFG Repo-Cleaner or git filter-repo. This is destructive: every teammate has to delete their local clone and re-clone. Revoke the key in AWS immediately, too.
Don’t rely on memory. Generate a tested ignore file for any combination of languages (Node.js, Python, Go, Rust) with our .gitignore Generator.
5. Emergency Recovery Commands
Git rarely loses code — it usually just hides it. Memorize these commands and you can recover from almost anything.
1. Soft reset (undo a commit safely)
You committed, then realized you forgot a file or made a typo in the message.
# Rewind HEAD by 1 commit, keeping your changes staged
git reset --soft HEAD~1
# Add the missing file and commit again
git add missing_file.js
git commit -m "feat: complete authentication module"
2. Hard reset (the nuclear option)
You spent three hours on an approach that turned out wrong and want to return to the last clean state.
# WARNING: this permanently deletes uncommitted code. It can't be undone.
git reset --hard HEAD
# To also remove untracked files and directories
git clean -fd
3. git reflog (the time machine)
You deleted a feature branch with three days of unmerged work. git log won’t show it because the branch pointer is gone.
# View the local log of every action you've taken
git reflog
# Output:
# a1b2c3d HEAD@{0}: checkout: moving from feature/lost to main
# e4f5g6h HEAD@{1}: commit: feat: crucial unmerged work
# Recreate the deleted branch from the ghost SHA
git checkout -b recovered-branch e4f5g6h
The reflog keeps a local history of branch-tip changes for about 90 days. It’s your safety net against accidental deletions and bad rebases.
4. git bisect (the bug hunter)
You deployed, a bug appeared, and you don’t know which of the last 50 commits caused it.
# Start the search
git bisect start
# Mark the current commit as broken
git bisect bad
# Mark a commit you know was working
git bisect good v1.0.0
Git runs a binary search, checking out commits in the middle of the range and asking you to mark each good or bad. In 5–6 steps it pinpoints the commit that introduced the bug, saving hours of manual debugging.
Further Reading
- Environment Variables: Preventing Secret Leaks in Git
- Regex for Developers: Parsing Log Files
- JSON vs YAML: Structuring CI/CD Configurations
Set up your repository cleanly. Use our Gitignore Generator to exclude dependencies before initializing, and our Diff Checker to untangle merge conflicts outside the terminal.