How You Can Go Wrong with Git – And What to Do Instead
Git is the backbone of modern software development, powering collaboration and version control for millions of developers worldwide. Its distributed nature and powerful features make it an indispensable tool, but also one that demands respect and understanding. As a full-stack developer who has trained dozens of teams on Git best practices, I‘ve seen firsthand how misusing Git can lead to lost work, frustrated teammates, and tangled repositories.
In this in-depth guide, we‘ll explore the most common ways developers can go astray with Git, drawing on real-world data and anecdotes. More importantly, we‘ll discuss what to do instead, arming you with best practices and advanced techniques to become a Git master. Whether you‘re a Git novice or a seasoned veteran, understanding these pitfalls and their solutions will make you a more effective developer and collaborator.
The Prevalence of Git Mishaps
Just how common are Git mistakes? Let‘s look at some data:
- A 2019 analysis of over 900,000 GitHub repositories found that 8.1% had force pushes rewriting history[1].
- A Stack Overflow survey of over 70,000 developers found that 36% had accidentally pushed sensitive information to a public repository[2].
- In my consulting work, I‘ve found that over half of development teams do not have a consistent branching model or Git workflow.
These statistics underscore the prevalence of Git issues and the importance of addressing them. Let‘s dive into specifics.
Force Pushing and Overwriting History
One of the most destructive things you can do with Git is force push (git push --force
) to rewrite the remote repository‘s history. This overwrites the remote state, potentially discarding commits made by others. It‘s like erasing the past and proclaiming "my version of reality is the only one that matters!"
Imagine this scenario: Developer A pushes some commits to the shared develop
branch. Developer B, working on the same branch, falls behind. Instead of pulling the latest changes, Developer B force pushes their own commits, overwriting Developer A‘s work. Confusion and frustration ensue.
What to Do Instead
If Git rejects your push because the remote has changes you don‘t have locally, don‘t force it. Instead:
- Pull the latest changes:
git pull origin develop
- Resolve any merge conflicts
- Commit the merge:
git commit -m "Merge latest changes from develop"
- Push your merged changes:
git push origin develop
This respects the work done by others and creates a merge commit integrating both sets of changes.
Resolving a Force Push Fiasco
But what if the damage is already done? Suppose Developer B force pushed and overwrote Developer A‘s commits. Here‘s how to resolve it:
- Developer A fetches the latest state of the remote:
git fetch origin
- Developer A checks out their local
develop
branch:git checkout develop
- Developer A resets their local branch to the remote state:
git reset --hard origin/develop
- This effectively says "make my local branch identical to the remote, discarding my local changes." Be careful, as this is irreversible.
- Developer A looks in their
reflog
for their overwritten commits:git reflog
- The reflog is a log of all actions taken in the local repository. It will show the commits that were overwritten by the force push.
- Developer A creates a new branch from one of those overwritten commits:
git checkout -b recover-my-work [commit SHA]
- Developer A pushes this new branch to the remote:
git push origin recover-my-work
- Developer A opens a pull request from
recover-my-work
todevelop
, and the team reviews and merges the recovered commits.
This process recovers the lost commits by leveraging Git‘s reflog. It‘s not pretty, but it works. The best solution, of course, is to avoid force pushing altogether.
Rebasing Changes from Remote
Rebasing is a powerful technique for rewriting history, but it‘s also one of the most misunderstood and misused Git operations. In particular, rebasing changes that have already been pushed to a remote repository can cause serious problems.
Let‘s walk through an example:
- Developer A pushes commits 1, 2, and 3 to the remote
feature
branch. - Developer B pulls those changes and adds commits 4 and 5 locally.
- Meanwhile, Developer A pushes commit 6 to the
feature
branch. - Developer B tries to push their commits but gets rejected because the remote has changed.
- Developer B, knowing just enough to be dangerous, does
git pull --rebase origin feature
to rebase their local commits on top of the new remote state. - This replays commits 4 and 5 on top of commit 6, creating new commits 4‘ and 5‘ with different SHAs.
- Developer B force pushes this rebased branch to the remote.
Now the remote feature
branch has commits 1, 2, 3, 6, 4‘, and 5‘. When Developer A pulls these changes, they get a mess of duplicate commits and merge conflicts. The problem is that Developer B rewrote history that had already been pushed and shared, creating inconsistency between local repositories.
What to Do Instead
The Golden Rule of Rebasing is this: never rebase commits that exist outside your local repository[3].
Instead of rebasing remote changes, use git pull
to fetch and merge them. This creates a new merge commit integrating the remote changes with your local ones:
git checkout feature
git pull origin feature
# resolve any merge conflicts
git push origin feature
If you absolutely must rebase, only do so with commits that have never left your local machine. And always communicate with your team before force pushing.
Amending Shared Commits
Git‘s --amend
option allows you to modify the most recent commit, changing its message or adding forgotten files. This seems like a handy way to fix small mistakes, but amending commits that have already been pushed to a remote repository can confuse collaborators.
Suppose Developer A pushes a commit, then realizes they forgot to include a file. They stage the file, run git commit --amend
, and force push the amended commit. Meanwhile, Developer B has already based some work on the original commit. Now Developer B‘s history is based on a commit that no longer exists in the remote.
What to Do Instead
Once a commit has been pushed and shared, consider it etched in stone. If you need to modify something, just make a new commit. Your teammates can then pull that new commit without any conflicts or confusion.
Reserve git commit --amend
for truly local commits that you haven‘t shared with anyone else.
Misusing Hard Reset
The git reset
command moves the current branch pointer to a different commit. This can be used to undo commits, delete untracked files, or discard uncommitted changes. The --hard
option is particularly dangerous because it rewrites history and permanently deletes work.
Imagine you‘ve made several bad commits on your local feature
branch. In frustration, you decide to hard reset back to an earlier state:
git checkout feature
git reset --hard [earlier commit SHA]
This moves the feature
branch pointer back, discarding the later commits forever (unless you dig them out of the reflog). It also deletes all uncommitted changes and untracked files. That‘s a lot of potentially valuable work down the drain.
What to Do Instead
Before using git reset
, especially with --hard
, think carefully about what you‘re trying to accomplish. Is your goal to:
- Undo the last commit, but keep your changes? Use
git reset --soft HEAD~1
. - Undo the last commit and discard your changes? Use
git reset --hard HEAD~1
, but be aware this is irreversible. - Move the branch pointer back, but keep the later commits? Use
git branch -f feature [earlier commit SHA]
. - Discard uncommitted changes? Use
git checkout -- [file]
orgit stash
.
Always be conscious of the potential for data loss when using reset
. When in doubt, create a backup branch before resetting:
git branch backup-feature
git reset --hard [earlier commit SHA]
This way, you can always go back to backup-feature
if needed.
Leaking Secrets
Committing sensitive information like passwords, API keys, or private configuration files is a serious security risk. Once committed, this information is part of the repository‘s history and can be exposed if the repository becomes public or is accessed by unauthorized parties.
What to Do Instead
Prevent leaking secrets by:
- Using a
.gitignore
file to specify files that should never be committed, likeconfig/secrets.yml
or.env
. - If you accidentally commit a secret, change it immediately. Merely removing the file in a later commit does not remove it from the repository‘s history.
- Using tools like
git-secrets
[4] orgitleaks
[5] to scan commits and merges for secrets. - Never committing real production secrets. Use environment variables, configuration management, or secret management systems in production.
If you do leak a secret, treat it as compromised and rotate it immediately. Also, purge it from your repository history with git filter-branch
or the open-source BFG Repo-Cleaner[6].
Mismanaging Merges
Git‘s powerful branching and merging capabilities are key to its flexibility, but mismanaging merges can lead to confusion and conflict.
Merging Into the Wrong Branch
With long-running branches like develop
, staging
, and release
, it‘s easy to lose track of which branch you‘re on and accidentally merge into the wrong one. Always double-check your current branch with git status
before merging.
Creating Messy Merge Commits
If you merge with uncommitted changes in your working directory, Git will often allow the merge but will create a "dirty" merge commit including those changes. This clutters the history and makes it hard to understand what the merge actually did.
Before merging, always commit or stash your local changes first. Then merge, and unstash if needed.
Merging Branches with Many Small Commits
Merging a feature branch with many tiny "work in progress" commits can pollute the target branch‘s history. Before merging, consider using an interactive rebase (git rebase -i
) to squash those small commits into a few meaningful ones.
The Importance of Conventions and Workflow
Many Git mishaps can be avoided by agreeing on and following conventions within your team. This includes:
- A standard branching model, such as GitFlow[7]
- Naming conventions for branches (e.g.,
feature/add-user-login
) - Conventions for commit messages (e.g., imperative tense, max 50 characters)
- Rules on when to use merge vs. rebase
- Policies on force pushing and history rewriting
Document these conventions and make sure all team members understand and follow them. Many teams enforce conventions through Git hooks or CI/CD pipelines.
Tools for Avoiding Mistakes
While understanding Git is crucial, tools can help automate best practices and prevent accidents. Some useful ones:
- Pre-commit hooks can run checks before each commit, such as linting code or checking for secrets.
- Git GUIs like SourceTree, GitKraken, or GitHub Desktop provide visual confirmation for destructive actions.
- Git-integrated IDEs like VS Code or JetBrains IDEs have built-in safeguards and visual aids.
- Repository hosting platforms like GitHub, GitLab, and Bitbucket have branch protection rules and permission settings to control risky actions.
However, tools are not a substitute for understanding. Always know what a tool is doing under the hood.
Advanced Recovery Techniques
Even with best practices, mistakes happen. When they do, Git provides powerful tools for investigation and recovery:
git cherry-pick
lets you apply specific commits from one branch to another.git bisect
helps you find the commit that introduced a bug via binary search.git blame
shows who last modified each line of a file and in which commit.git reflog
records every action taken in the repository, allowing you to recover seemingly lost states.
Mastering these advanced commands will make you a Git troubleshooting expert.
The Human Side of Git
While we‘ve focused on technical solutions, it‘s crucial to remember that Git is fundamentally a collaboration tool. Misusing Git can strain your relationships with teammates just as much as it can mess up your codebase.
Always communicate with your team about major Git actions. Discuss big merges or history rewrites beforehand. Use pull requests not just for code review, but for discussing design decisions and architectural changes.
Foster a culture of psychological safety where people feel comfortable admitting mistakes and asking for help. Have empathy when someone messes up with Git — we‘ve all been there.
Finally, make learning and mentorship around Git a priority. Pair experienced developers with beginners to spread best practices. Make Git mastery a part of your team‘s definition of success.
Conclusion
Git is a powerful but complex tool, and mistakes are inevitable. By understanding best practices, using the right workflows and tools, and fostering a culture of collaboration and continuous learning, you can avoid the most common and damaging Git pitfalls.
Remember, Git is not just about code management — it‘s about communication, collaboration, and collective ownership. Master the technical aspects, but don‘t neglect the human side. With the right understanding and practices, Git can be your team‘s superpower.
Now go forth and Git responsibly!