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:

  1. Pull the latest changes: git pull origin develop
  2. Resolve any merge conflicts
  3. Commit the merge: git commit -m "Merge latest changes from develop"
  4. 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:

  1. Developer A fetches the latest state of the remote: git fetch origin
  2. Developer A checks out their local develop branch: git checkout develop
  3. 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.
  4. 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.
  5. Developer A creates a new branch from one of those overwritten commits: git checkout -b recover-my-work [commit SHA]
  6. Developer A pushes this new branch to the remote: git push origin recover-my-work
  7. Developer A opens a pull request from recover-my-work to develop, 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:

  1. Developer A pushes commits 1, 2, and 3 to the remote feature branch.
  2. Developer B pulls those changes and adds commits 4 and 5 locally.
  3. Meanwhile, Developer A pushes commit 6 to the feature branch.
  4. Developer B tries to push their commits but gets rejected because the remote has changed.
  5. 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.
  6. This replays commits 4 and 5 on top of commit 6, creating new commits 4‘ and 5‘ with different SHAs.
  7. 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] or git 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:

  1. Using a .gitignore file to specify files that should never be committed, like config/secrets.yml or .env.
  2. 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.
  3. Using tools like git-secrets[4] or gitleaks[5] to scan commits and merges for secrets.
  4. 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!

Similar Posts