Force Push Wiped Commits — Version Control Best Practices
Bare git push --force on main can silently delete teammates' committed work.
- Git snapshots your project with every commit – think of it as a save point with a note.
- One commit = one logical change; never mix unrelated changes in a single commit.
- Branches isolate work: never commit directly to main, always use feature branches.
- Use merge for shared branches (preserves history), rebase for private branches (clean linear history).
- Keep branches short-lived (1-2 days) to avoid nightmare merges.
- Write commit messages in imperative mood: "Fix login bug" not "Fixed login bug" or "fixes".
Imagine you're writing a 30-page school essay in Google Docs. Every time you finish a paragraph, Google secretly saves a snapshot — so if you accidentally delete three pages, you can rewind to yesterday's version in seconds. Version control is exactly that snapshot system, but for code. Instead of Google doing it automatically, you decide when to save a snapshot, what to name it, and who else can see it. Every professional software team on Earth uses this — and the habits you build around it will define how trustworthy you look as a developer.
Every software project eventually becomes a time machine problem. You ship a feature on Monday, a bug report lands on Wednesday, and by Friday you genuinely can't remember what the code looked like before you touched it. Without a disciplined approach to version control, that's not a inconvenience — it's a crisis. Teams lose work, bugs get shipped twice, and developers overwrite each other's changes without ever knowing it happened. This is not a rare edge case. It happens on real projects, at real companies, every single week.
Version control solves this by giving every change a permanent address in history. Every time you save a meaningful checkpoint (called a commit), the tool records exactly what changed, who changed it, and when. You can compare any two points in history, undo a disastrous change in seconds, and let ten developers work on the same codebase simultaneously without stepping on each other's toes. The tool most teams use today is Git — but the practices around Git are what separate senior engineers from people who just know the commands.
By the end of this article you'll understand what a commit really is and how to write one that makes sense six months later, how to use branches to work safely without breaking anything, how to think about merging versus rebasing, and the three common mistakes that silently wreck team codebases. You don't need any prior experience — just an appetite to build habits that will make every team you join immediately trust your work.
What a Commit Actually Is — And Why Tiny Commits Win Every Time
A commit is a permanent snapshot of your project at a specific moment. Think of it like a save point in a video game — except each save point also has a label you wrote, explaining exactly what changed and why. The label is called a commit message, and it's more important than most beginners realise.
The golden rule is: one commit, one logical change. Not one commit per day. Not one giant commit when the feature is done. One commit per idea. If you fixed a login bug and also tweaked a button colour, those are two separate commits — even if you did both in the same five minutes. Why? Because six months later, when a colleague hunts down the bug that the button-colour change introduced, they need to be able to isolate it instantly.
A good commit message follows this structure: a short subject line (under 50 characters) that completes the sentence 'If applied, this commit will...' — followed by a blank line and an optional body explaining why, not what. The diff already shows what changed. The message should explain the reasoning a future developer won't have access to.
Small, focused commits are also much easier to review in a pull request, much easier to revert if something goes wrong, and they make git blame (a command that shows who last touched each line) genuinely useful instead of pointing at one massive commit that changed 800 lines.
git add -p (patch mode) to stage individual chunks within a single file — not the whole file at once. This is the move when you've made two unrelated changes in the same file and want to split them into separate commits. Interviewers who dig into Git deeply will be impressed if you mention this unprompted.Branching Strategy — How to Work Without Breaking Everything
A branch is a parallel universe for your code. The main branch (usually called main or master) represents the code that's live, working, and trusted. A feature branch is a copy of that universe where you can experiment, break things, and rebuild them — without touching the version everyone else is relying on.
The core idea is simple: never commit directly to main. Every piece of work — no matter how small — gets its own branch. You work there, you test there, you get it reviewed there, and only then does it merge back into main. This keeps main in a permanently deployable state, which is the entire point.
Branch names should be descriptive and follow a consistent pattern. A common convention is type/short-description — for example: feature/user-profile-page, bugfix/cart-total-rounding-error, or hotfix/payment-gateway-timeout. This tells every teammate at a glance what type of work is happening and what it's about, without opening a single file.
Keep branches short-lived. A branch that lives for three weeks becomes a nightmare to merge because the main branch has moved on. Aim to open a pull request within a day or two of starting a branch. If a feature is too large to finish that quickly, break it into smaller deliverable pieces — that's a design skill, not just a Git skill, and it signals seniority.
main is moving forward without you. After two weeks, merging your branch can feel like defusing a bomb — conflict after conflict, context you've forgotten. The fix isn't to merge faster carelessly; it's to make branches smaller. If a feature takes three weeks, it should probably be three separate branches merged one at a time.Merge vs Rebase — Choosing the Right Way to Combine Work
Once your feature branch is ready, you need to bring it back into main. There are two ways to do this: merge and rebase. They both achieve the same end result — your code ends up in main — but they create very different histories, and understanding the difference is a genuine mark of seniority.
A merge takes both branches and creates a new 'merge commit' that ties them together. History is preserved exactly as it happened — parallel work looks parallel in the log. It's honest, non-destructive, and safe for branches that other people are also working on. The downside is that a project with lots of branches and merges can produce a git log that looks like a tube map — hard to read linearly.
A rebase replays your branch's commits on top of the latest main, one by one, as if you had started your branch today instead of a week ago. The history comes out perfectly linear — no merge commits, no diverging lines. It's much easier to read. The downside: rebase rewrites commit hashes, which means if anyone else has your branch checked out, their history will conflict. The rule of thumb is: never rebase a branch that other people are working on.
The most common professional workflow is: rebase your feature branch on top of main before opening a pull request (to keep history clean), then use a regular merge (or a 'squash merge') when the pull request is approved. This gives you the readability of rebase with the safety of merge at the critical moment.
--force-with-lease to avoid accidentally overwriting others' work.--force-with-lease when you must force push..gitignore, README, and the Habits That Make Teammates Love You
The practices covered so far — clean commits, short-lived branches, thoughtful merging — are the big ones. But there's a set of smaller habits that separate developers who 'know Git' from developers who 'use Git professionally'. These habits are often what interviewers probe for when they ask 'tell me about your version control workflow.'
First: every repository needs a .gitignore file before the first commit. This file tells Git which files to completely ignore — things like compiled binaries, log files, API keys stored in .env files, and IDE configuration folders like .idea/ or .vscode/. Committing these files is at best noise and at worst a security disaster. The website gitignore.io generates ready-made .gitignore files for any language or framework.
Second: never commit credentials. Not even for a second. Even if you delete them in the next commit, they are permanently in Git history and can be extracted. Use environment variables or secret management tools instead. If you accidentally commit a secret, rotate the credential immediately — assume it's compromised.
Third: write a meaningful README. It should answer four questions: what does this project do, how do I run it locally, how do I run the tests, and who do I contact if something is broken. A project with a clear README signals a professional codebase. A project without one signals chaos.
Finally: tag your releases. When code goes to production, run git tag -a v1.4.0 -m "Release 1.4.0 — adds avatar upload and search filters". Tags create permanent, named markers in history so you can always check out exactly what was running in production on any given day.
git log -p and see every version of every file ever committed. If you commit a secret, treat it as fully compromised — rotate it immediately, then use a tool like git filter-repo to scrub the history if it's a private repo. If it's a public repo, assume the key has already been harvested by automated scanners.Handling Merge Conflicts Like a Pro
Merge conflicts happen when two branches modify the same part of the same file in different ways. Git can't decide which version to keep, so it stops and asks you to resolve it manually. This is not a sign of failure — it's a normal part of collaborative development. But how you handle conflicts separates a smooth workflow from a chaotic one.
When a conflict occurs, Git marks the conflicted file with special markers: <<<<<<<, =======, and >>>>>>>. The section between <<<<<<< and ======= is your current branch's version; between ======= and >>>>>>> is the incoming branch's version. You edit the file to produce the correct final state, remove the markers, and stage the file.
The most common mistake during conflict resolution is to blindly accept one side without understanding the context. Always consider both changes — the answer is often a combination, not a choice. If you're unsure, talk to the developer who made the conflicting change. A five-minute conversation saves an hour of debugging later.
To reduce conflicts in the first place, keep branches short, communicate with your team about what files you're working on, and rebase frequently to stay close to main. Conflict resolution is a skill, like debugging — the more you do it deliberately, the faster you get.
git merge --abort to go back to the state before the merge attempt. No harm done. You can then re-plan your approach – maybe rebase first to reduce conflicts.Force Push to Main Wiped Out Teammate's Commits
git push --force origin main overwrites the remote main branch regardless of who committed to it.--force instead of --force-with-lease, which would have aborted if the remote had unexpected commits. Also, branch protection on main was not enabled.git reflog on one of the affected developer's local repositories to find the lost commits. Cherry-pick those commits back onto main. Re-enable branch protection to require pull requests and prevent force pushes.- Never use bare
--forceon shared branches – always use--force-with-lease. - Enable branch protection on main to block force pushes and require PRs.
- Educate team on the difference between
--forceand--force-with-lease. - Keep local clones of teammates as recovery points.
git log to find the commit hash, then git cherry-pick <hash> onto the correct branch and git reset HEAD~1 on the wrong branch.git mergetool to open a visual diff tool, or manually edit files to resolve markers, then git add and git commit.git reflog to find the commit hash before the reset, then git reset --hard <hash>.git reflog on the remote (if accessible) or ask the teammate to git push --force-with-lease with their commits. Otherwise, recover from local clones.git reset HEAD <file> to unstage if needed.Key takeaways
Common mistakes to avoid
3 patternsCommitting directly to main
Writing vague commit messages like 'fix bug' or 'changes'
Committing node_modules, .env, or build artifacts
Interview Questions on This Topic
Walk me through your typical Git workflow when starting a new feature — from the moment you get the ticket to the moment the code is in production.
Frequently Asked Questions
That's Software Engineering. Mark it forged?
7 min read · try the examples if you haven't