Git Workflow Best Practices — The Habits That Keep Teams Moving Fast
Git is the one tool every developer on a team shares, which makes how you use it either a productivity multiplier or a constant source of friction. The mechanics are easy to learn; the habits that keep history clean, PRs reviewable, and merges conflict-free take deliberate practice. This covers the workflow decisions that matter most — from commit discipline to branch strategy to the nuances that separate a collaborative codebase from a chaotic one.
Commit Discipline
The best test for a good commit: can you describe what it does in one imperative sentence under 72 characters? If the honest answer requires “and” — “Fix login bug and update user model and refactor session handling” — you’ve bundled multiple changes into one commit. Pull them apart.
Atomic commits that do one thing are easier to review, easier to revert, and easier to bisect when something breaks. A bug introduced in a 500-line commit that mixes features, fixes, and refactoring is much harder to isolate than one in a 50-line commit that does exactly one thing.
The size isn’t the rule — coherence is. A commit that touches 10 files but makes a single conceptual change is fine. A commit that touches 3 files for 3 unrelated reasons is not.
Branch Strategy
Feature branches off main. One feature, one branch. Short-lived.
The naming convention that makes branches scannable: type/short-description. Types: feat, fix, chore, refactor, docs. Example: feat/user-notifications, fix/payment-timeout, chore/update-gems.
Short-lived matters more than you might expect. A branch that lives for two weeks accumulates drift from main, which means a harder merge and a harder review. Branches that merge in days stay easy. If a feature is too big to finish in a week, break it into smaller pieces that can ship independently — even if the feature flag hides them until the full feature is ready.
Rebase vs. Merge
This debate has more heat than it deserves. Here’s the practical split:
Rebase for local cleanup. Before opening a PR, rebase your feature branch on main to incorporate recent changes and present a clean, linear history. git rebase main followed by resolving any conflicts gives reviewers a diff that’s easy to follow.
Merge commits for main. When merging a PR into main, a merge commit preserves the true history — you can see when the feature branch diverged and when it landed. Squash merging is also fine, especially for small features, because it collapses a noisy branch history into one clean commit on main.
Never rebase a shared branch. Rewriting history on a branch other people are working from creates divergence that’s painful to untangle. The rule is simple: once a branch is pushed and others have based work on it, stop rebasing it.
Writing Commit Messages That Age Well
The commit message that’s useful six months later is different from the one that makes the test suite pass today. Three habits:
Imperative mood in the subject. “Add email validation” not “Added email validation” or “Adding email validation.” Git’s own commits follow this convention, and it reads cleanly in git log.
Explain why, not what. The diff shows what changed. The commit message should explain why you made the change — the constraint you were working around, the bug you were fixing, the decision you made when there were multiple options.
Reference issues. Fixes #482 or See #301 in the body creates a link between the commit and the context. When someone reads this commit in two years, they can follow the issue link to understand the original problem.
Before You Push
One habit that catches most mistakes before they become PR comments: review your own diff before pushing.
git diff origin/main...HEAD
Read it like a reviewer who’s seeing it for the first time. Specifically look for:
- Debug statements (
puts,console.log,binding.pry) - TODOs you meant to resolve
- Hardcoded values that should be config
- Secrets or credentials accidentally included
- Files you didn’t intend to change
Running tests before pushing seems obvious but gets skipped under time pressure. The two minutes it takes to catch a failing test locally is much cheaper than the 20 minutes of CI + review cycle that follows a broken build.
Managing Merge Conflicts
The best merge conflict is the one that never happens. Small, focused PRs that merge quickly are the prevention. When two developers work in the same part of the codebase for weeks, a merge conflict isn’t a surprise — it’s the cost of delayed integration.
When conflicts do happen, the productive approach is understanding both sides rather than just picking one. git log --merge -p shows the commits on both sides that touched the conflicted file. Understanding what each change was trying to accomplish usually makes the resolution obvious.
For complex conflicts, a three-way diff tool (VS Code has a decent built-in one, Beyond Compare is excellent) is much easier to reason about than raw conflict markers. The ours and theirs labels map to your branch and the branch you’re merging — keep that orientation clear.
Pro-Tip:
git add -p(patch mode) is one of Git’s most underused features. When you’ve made two unrelated changes in the same file and want to split them into separate commits,git add -plets you stage individual hunks of the diff. You get clean, atomic commits even when your working changes aren’t cleanly organized by file.
Keeping History Readable
A few conventions that compound over time:
Squash or fixup small cleanup commits. If you’ve got “WIP”, “fix typo”, and “address PR feedback” commits before merging, squash them into the relevant commits they belong to. git rebase -i with fixup or squash keeps the main branch history meaningful.
Tag releases. Even on small projects, annotated tags (git tag -a v1.2.0 -m "Release notes here") create a clean trail of what shipped when. Checking out a specific version is trivial; comparing versions with git log v1.1.0..v1.2.0 is immediately useful.
Don’t commit generated files. Build artifacts, bundled output, and anything your tools generate should live in .gitignore. Every generated file committed is a merge conflict waiting to happen.
Conclusion
Git workflow habits are the kind of thing that feels like overhead until you’re six months into a project and can read the history like documentation. Atomic commits, short-lived branches, clean messages that explain why, and reviewing your own diff before pushing — none of these take much time individually. Together, they make code review faster, debugging easier, and the codebase more maintainable for everyone who works in it.
FAQs
Q1: Should I commit directly to main on solo projects?
For truly solo projects with no collaborators, committing directly to main is fine. The overhead of feature branches pays off when you need to review, revert, or collaborate. On solo projects, the more important habit is atomic commits — branch strategy matters less when you’re the only reviewer.
Q2: How do I undo a commit I just made?
git reset HEAD~1 undoes the last commit and unstages the changes, leaving your working directory intact. Add --soft to keep changes staged, or --hard to discard changes entirely. If the commit is already pushed to a shared branch, use git revert HEAD instead — it creates a new commit that undoes the previous one without rewriting shared history.
Q3: What’s the right number of commits per PR?
There’s no fixed rule, but PRs that tell a coherent story are easier to review than either one massive commit or 40 micro-commits. A common useful range is 3–8 commits that each represent a meaningful step. If you’re reviewing your own PR and can’t follow the narrative, that’s a signal to reorganize.
Q4: How do I handle a long-running feature branch?
Sync it with main frequently — rebase or merge main into your branch at least every day or two of active work. The longer you let it drift, the more painful the eventual reconciliation. For features that take weeks, consider shipping behind a feature flag so smaller pieces can merge to main continuously.
Q5: Is there a good way to learn what went wrong in a past release?
git bisect is the most powerful tool for this. You give it a known-good commit and a known-bad commit, and it binary-searches the history running a test you specify. For subtler questions, git log -S "the string that changed" (the pickaxe search) finds every commit that added or removed a specific string — useful for tracking down when a particular piece of logic was introduced or removed.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀