UseToolSuite UseToolSuite

Git Best Practices Every Developer Should Know

A practical guide to Git collaboration: semantic commits, trunk-based vs GitFlow, rebasing, automating conflict resolution with `git rerere`, and Husky pre-commit hooks.

Necmeddin Cunedioglu Necmeddin Cunedioglu 7 min read
Part of the Developer Generators: The Tools That Save You Hours Every Week series

Practice what you learn

.gitignore Generator

Try it free →

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 .env file 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:

  1. feat: a new feature (e.g., feat(auth): implement Google OAuth 2.0).
  2. fix: a bug fix (e.g., fix(cart): prevent negative checkout quantities).
  3. refactor: changing structure without changing behavior (e.g., refactor(api): extract DB validation logic).
  4. perf: changes that improve runtime performance or memory usage.
  5. docs: updates to the README, JSDoc comments, or Swagger definitions.
  6. chore: routine maintenance, dependency upgrades, or CI/CD changes.
  7. 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) and develop (daily integration).
  • It uses temporary feature/, release/, and hotfix/ 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 main branch (the trunk) stays green and deployable at all times.
  • Developers branch directly off main into 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 main only 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


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.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author
7 min read
-- views

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.