Introduction: When Things Go Sideways
Welcome to Chapter 15! So far, we’ve explored the incredible power of Git and GitHub for managing code, collaborating with teams, and building amazing projects. But let’s be honest: even the most experienced developers sometimes face a hiccup or two. Git, while powerful, can sometimes feel a bit like a puzzle when things don’t go exactly as planned.
This chapter is your trusty toolkit for those “uh-oh” moments. We’re going to dive deep into diagnosing and fixing the most common Git and GitHub issues you’ll encounter in real-world development. From dreaded merge conflicts to accidental changes and mysterious “detached HEAD” states, we’ll cover it all. Our goal isn’t just to give you solutions, but to help you understand why these problems occur and how to confidently navigate them yourself.
Before we jump in, make sure you’re comfortable with the core Git concepts we’ve covered: committing, branching, merging, rebasing, and interacting with remote repositories. These fundamentals are your foundation for effective troubleshooting. Ready to become a Git problem-solving guru? Let’s get started!
Core Concepts: Understanding Git’s Safety Nets
One of Git’s unsung heroes is its robust safety net. It’s designed to make it very hard to permanently lose work, even when you make mistakes. Understanding how Git tracks changes and references (like branches and commits) is key to troubleshooting. Most “problems” are just situations where Git needs your explicit instructions to proceed.
The Dreaded Merge Conflict: A Clash of Changes
Merge conflicts occur when Git attempts to combine two divergent sets of changes and finds that both sets have modified the same lines in the same file, or one has deleted a file that the other has modified. Git is smart, but it can’t read minds – it doesn’t know which change you intend to keep. This is where you, the developer, step in to resolve the ambiguity.
Why do they happen?
Imagine Alice and Bob both edit line 10 of index.html. Alice changes it to “Hello World!” and Bob changes it to “Welcome Home!”. When their branches merge, Git sees two different changes to the same line and pauses, asking you to decide.
How Git presents a conflict: When a merge conflict occurs, Git will tell you which files are conflicted. If you open a conflicted file, you’ll see special markers:
<<<<<<< HEAD
This is the line from your current branch (HEAD).
=======
This is the line from the branch you're trying to merge.
>>>>>>> feature-branch-name
<<<<<<< HEAD: Marks the beginning of the conflicting change from your current branch (whereHEADis pointing).=======: Separates the changes from the two branches.>>>>>>> feature-branch-name: Marks the end of the conflicting change from the other branch (the one you’re merging in).
Let’s visualize the merge conflict resolution process:
Figure 15.1: Flowchart of Merge Conflict Resolution
Step-by-Step: Resolving a Merge Conflict
Let’s simulate a conflict and resolve it.
Preparation: First, ensure you have a clean working directory. If you have any pending changes, commit or stash them.
git status # Should say "nothing to commit, working tree clean"Create a new repository and make an initial commit:
mkdir git-troubleshooting cd git-troubleshooting git init echo "Initial content" > file.txt git add file.txt git commit -m "Initial commit"Create Divergent Branches: Now, let’s create two branches,
feature-aliceandfeature-bob, and make conflicting changes.# Create and switch to Alice's branch git switch -c feature-alice echo "Alice's important change on line 2" >> file.txt echo "Common line 3" >> file.txt # This line will also be changed by Bob git add file.txt git commit -m "Alice adds her feature" # Switch back to main and create Bob's branch git switch main git switch -c feature-bob echo "Bob's critical change on line 2" >> file.txt echo "Bob's version of common line 3" >> file.txt # Bob's change git add file.txt git commit -m "Bob adds his feature"What just happened? We’ve created two separate timelines where both Alice and Bob modified
file.txtin a way that Git can’t automatically reconcile. Specifically, they both added a second line, and both modified the third line.Initiate the Merge and Encounter Conflict: Let’s try to merge
feature-bobintomain, thenfeature-aliceintomain. We’ll start by merging Bob’s changes intomain(which should be clean).git switch main git merge feature-bobThis should merge cleanly. Now, let’s try to merge
feature-aliceintomain.git merge feature-aliceYou should now see output similar to this:
Auto-merging file.txt CONFLICT (content): Merge conflict in file.txt Automatic merge failed; fix conflicts and then commit the result.Aha! A merge conflict!
Inspect the Conflict: Use
git statusto see which files are conflicted:git statusOutput will show
file.txtasboth modified.Now, open
file.txtin your favorite text editor. You’ll see:Initial content <<<<<<< HEAD Bob's critical change on line 2 Bob's version of common line 3 ======= Alice's important change on line 2 Common line 3 >>>>>>> feature-aliceObserve: Git clearly shows you the
HEAD(which ismainafter mergingfeature-bob) and thefeature-aliceversion.Resolve the Conflict: Decide which parts you want to keep. Let’s say we want to keep Bob’s line 2, but combine Alice’s and Bob’s changes for line 3. We’ll manually edit the file to look like this:
Initial content Bob's critical change on line 2 Bob's version of common line 3 (and Alice's too!)Explanation: We removed all the
<<<<<<<,=======, and>>>>>>>markers and combined the content as desired.Mark as Resolved and Commit: After saving the file, tell Git the conflict is resolved by adding the file to the staging area:
git add file.txtNow, commit the resolution. Git often provides a default commit message; you can use it or customize it.
git commit -m "Resolved merge conflict in file.txt, keeping Bob's line 2 and combining line 3"Congratulations! You’ve successfully resolved your first merge conflict!
Rebase Conflicts: A Different Kind of Challenge
Remember git rebase from Chapter 13? It rewrites history by applying your branch’s commits on top of another branch’s latest state. While powerful for keeping history clean, it can also lead to conflicts.
How rebase conflicts differ: During a merge, Git tries to combine two branches at once. During a rebase, Git applies each of your branch’s commits one by one. If a conflict occurs, it happens per commit as Git tries to apply it. This means you might resolve the same conflict multiple times if multiple commits touch the same lines.
Rebase Conflict Resolution Commands:
git status: Always your first friend to see what’s conflicted.git rebase --abort: Stops the rebase entirely and rewinds your branch to its state before the rebase started. This is your “get out of jail free” card if you get overwhelmed.git rebase --continue: After you’ve resolved a conflict in a file and staged it (git add <file>), this command tells Git to continue applying the remaining commits.git rebase --skip: If you decide a particular commit that’s causing a conflict is no longer needed, you can skip applying it. Use with caution, as it will drop that commit from your history.
Step-by-Step: Handling a Rebase Conflict
Let’s set up a scenario for a rebase conflict.
Preparation: Ensure you’re in the
git-troubleshootingdirectory. Let’s start with a freshmainand afeature-rebasebranch.git switch main git reset --hard HEAD~2 # Go back to initial commit for a clean slate echo "Initial content" > file.txt git add file.txt git commit -m "Fresh initial commit for rebase demo" # Create feature-rebase branch git switch -c feature-rebase echo "Feature line 1" >> file.txt git add file.txt git commit -m "feat: added line 1" echo "Feature line 2" >> file.txt git add file.txt git commit -m "feat: added line 2" # Go back to main and make a conflicting change git switch main echo "Main line A" >> file.txt git add file.txt git commit -m "main: added line A" echo "Main line B" >> file.txt git add file.txt git commit -m "main: added line B"What just happened?
feature-rebasehas two commits (feat: added line 1,feat: added line 2).mainalso has two new commits (main: added line A,main: added line B) that touch the same area offile.txt.Initiate the Rebase: Now, let’s try to rebase
feature-rebaseontomain.git switch feature-rebase git rebase mainYou’ll likely see a conflict:
Applying: feat: added line 1 Auto-merging file.txt CONFLICT (content): Merge conflict in file.txt error: could not apply 1234567... feat: added line 1 Resolve all conflicts manually, mark them as resolved with "git add/rm <conflicted/removed_files>", then run "git rebase --continue".Git tells you exactly what to do! It paused on the first commit from
feature-rebase(feat: added line 1).Resolve and Continue/Abort: Open
file.txt. It will look something like this:Initial content <<<<<<< HEAD Main line A ======= Feature line 1 >>>>>>> 1234567... feat: added line 1Let’s say we want to keep both. Edit
file.txt:Initial content Main line A Feature line 1Save the file.
Now, stage the changes and continue the rebase:
git add file.txt git rebase --continueGit will try to apply the next commit (
feat: added line 2). You might encounter another conflict! If so, repeat the resolve,git add,git rebase --continuesteps.If at any point you realize you’ve made a mess or don’t want to continue, simply run:
git rebase --abortThis will take you back to the state before
git rebase mainwas executed. Super helpful!
Detached HEAD State: Where Am I?
A “detached HEAD” state sounds scary, but it just means your HEAD pointer (which normally points to a branch name) is directly pointing to a specific commit, not a branch.
When does it happen?
- Checking out an old commit directly:
git checkout <commit-hash> - Checking out a remote branch’s commit (without creating a local tracking branch):
git checkout origin/main - During certain rebase operations or when inspecting specific points in history.
Why is it an issue? If you make new commits while in a detached HEAD state, those commits won’t belong to any branch. If you then check out a different branch, those “orphan” commits might become unreachable and could be garbage-collected by Git later!
How to fix it: The solution is simple: create a new branch from your current detached HEAD position.
Simulate Detached HEAD:
git switch main git log --oneline # Copy the hash of an earlier commit, e.g., "Initial commit" git checkout <paste-commit-hash>You’ll see a message like:
You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branch by switching back to a branch. ...Your prompt might also indicate
(HEAD detached at <commit-hash>).Make a new commit (optional, for demonstration):
echo "New content in detached state" >> detached-file.txt git add detached-file.txt git commit -m "Commit made in detached HEAD"Now,
git logwill show this new commit, butgit branchwon’t show it as part of any branch.Recover by creating a new branch:
git switch -c my-new-featureNow, your
HEADpoints tomy-new-feature, which includes the commit you just made. You’re no longer detached! You can then mergemy-new-featureintomainor another branch as usual.
Undoing Changes: The Art of Reversal
Mistakes happen. Git provides several powerful commands to undo changes, each with different implications for your working directory, staging area (index), and commit history.
git restore: Undoing Local Changes
Introduced in Git 2.23 (circa 2019), git restore is the modern, safer way to undo local changes in your working directory or staging area. It’s more intuitive than git checkout for this purpose.
- Discard unstaged changes: Reverts a file in your working directory to its state in the staging area (or the last commit if not staged).
git restore <file> - Unstage changes (move from staging to working directory): Reverts a file in the staging area back to its state in the last commit, but keeps your working directory changes.
git restore --staged <file> - Discard both staged and unstaged changes: Reverts a file to its state in the last commit, discarding all local modifications.
git restore --source=HEAD <file> # Or simply: git restore <file> # if the file is not staged, it restores from HEAD. # if staged, you'd use --staged first, then restore again.
git reset: Rewriting History (Use with Caution!)
git reset is a powerful command that moves your HEAD pointer and, depending on the mode, can also affect your staging area and working directory. It’s often used to undo commits or unstage files.
The three main modes:
git reset --soft <commit>:- Moves
HEADto the specified commit. - Keeps all changes from the undone commits in your staging area.
- Your working directory remains untouched.
- Useful for combining multiple small commits into one larger, more meaningful commit.
graph TD A[Original State] --> B(HEAD points to Commit D); B --> C[Working Directory: No changes]; B --> D[Staging Area: Empty]; subgraph Commits C1(Commit A) --> C2(Commit B) --> C3(Commit C) --> C4(Commit D); end B --> C4; style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px style C fill:#ccf,stroke:#333,stroke-width:2px style D fill:#ddf,stroke:#333,stroke-width:2px E[git reset --soft C2] --> F(New State); F --> G(HEAD points to Commit B); F --> H[Working Directory: Unchanged]; F --> I[Staging Area: Contains changes from C3 and C4]; G --> C2;Figure 15.2:
git reset --softimpact- Moves
git reset --mixed <commit>(default):- Moves
HEADto the specified commit. - Unstages changes from the undone commits (moves them from staging area to working directory).
- Your working directory remains untouched.
- Useful for completely undoing commits but keeping the changes to re-work them.
graph TD A[Original State] --> B(HEAD points to Commit D); B --> C[Working Directory: No changes]; B --> D[Staging Area: Empty]; subgraph Commits C1(Commit A) --> C2(Commit B) --> C3(Commit C) --> C4(Commit D); end B --> C4; style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px style C fill:#ccf,stroke:#333,stroke-width:2px style D fill:#ddf,stroke:#333,stroke-width:2px E[git reset --mixed C2] --> F(New State); F --> G(HEAD points to Commit B); F --> H[Working Directory: Contains changes from C3 and C4]; F --> I[Staging Area: Empty]; G --> C2;Figure 15.3:
git reset --mixedimpact- Moves
git reset --hard <commit>:- DANGER! Moves
HEADto the specified commit. - Discards all changes from the undone commits from both your staging area and working directory.
- Irreversible local data loss if not committed elsewhere. Only use if you are absolutely sure you want to throw away all local changes since that commit.
- Never use
--hardon commits that have been pushed to a shared remote repository, as it rewrites history and can cause major problems for collaborators.
graph TD A[Original State] --> B(HEAD points to Commit D); B --> C[Working Directory: No changes]; B --> D[Staging Area: Empty]; subgraph Commits C1(Commit A) --> C2(Commit B) --> C3(Commit C) --> C4(Commit D); end B --> C4; style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px style C fill:#ccf,stroke:#333,stroke-width:2px style D fill:#ddf,stroke:#333,stroke-width:2px E[git reset --hard C2] --> F(New State); F --> G(HEAD points to Commit B); F --> H[Working Directory: Discarded changes from C3 and C4]; F --> I[Staging Area: Empty]; G --> C2;Figure 15.4:
git reset --hardimpact- DANGER! Moves
git revert: A Safer Way to Undo History
git revert is the safest way to undo a commit that has already been pushed to a shared remote repository. Instead of rewriting history (like git reset), git revert creates a new commit that undoes the changes introduced by a previous commit.
- It’s non-destructive and preserves project history.
- It’s ideal for shared branches where rewriting history is problematic.
git revert <commit-hash>
This will open your editor to allow you to write a commit message for the new “revert” commit.
Remote Repository Issues: Pushing and Pulling Problems
Sometimes, the issues aren’t just local. Interacting with GitHub (or GitLab, Bitbucket) can also present challenges.
Authentication Errors:
- Problem:
Authentication failed for 'https://github.com/...' - Cause: Incorrect username/password, expired personal access token (PAT), or SSH key issues. GitHub stopped supporting password authentication for Git operations in 2021, requiring PATs or SSH keys.
- Solution:
- PAT: Generate a new PAT with the necessary scopes on GitHub (Developer Settings -> Personal access tokens -> Tokens (classic) or Fine-grained tokens) and use it when prompted for a password, or configure Git Credential Manager.
- SSH: Ensure your SSH key is added to your GitHub account and your SSH agent is running. Test with
ssh -T [email protected]. - Refer to GitHub’s official documentation for detailed setup.
- Problem:
Non-Fast-Forward Push (Rejected Push):
- Problem:
! [rejected] main -> main (non-fast-forward) - Cause: Someone else pushed changes to the remote branch you’re trying to push to, and your local branch is behind. Git prevents you from overwriting their work.
- Solution: Pull the latest changes from the remote (
git pull origin main) and resolve any merge conflicts locally, then try pushing again.
- Problem:
Force Pushing (
git push --forcevs.--force-with-lease):git push --force: Overwrites the remote branch with your local branch’s history, regardless of what’s on the remote. Dangerous! Can permanently erase collaborators’ work.git push --force-with-lease: A much safer alternative. It will only force push if the remote branch hasn’t been updated since you last pulled. If someone else pushed changes, it will fail, preventing accidental overwrites. Always prefer--force-with-leaseif you must force push.
When to use force-with-lease? Only when you’ve rebased your local branch and want to update a feature branch on the remote that only you are working on. Never force push to shared branches like
main/developunless explicitly coordinated with the entire team.Stale Remote-Tracking Branches:
- Problem: You see old branches listed locally that no longer exist on GitHub.
- Cause: When a remote branch is deleted (e.g., after a pull request merge), your local Git repository still has a reference to it.
- Solution: Use
git remote prune originto remove these stale references.git remote prune origin
Mini-Challenge: The Accidental Deletion
You’re working on a feature branch. In a moment of distraction, you accidentally delete my_important_file.js and then continue working on other files without realizing the deletion. Later, you remember the file and need it back, but you haven’t committed the deletion.
Challenge:
- Create a new file
my_important_file.jswith some content. - Add and commit it.
- Simulate accidentally deleting
my_important_file.js(e.g.,rm my_important_file.js). - Create another file
another_file.txtwith some content. - Without committing the deletion of
my_important_file.jsor the creation ofanother_file.txt, restoremy_important_file.jsto its last committed state. Ensureanother_file.txtremains in your working directory as a new, unstaged file.
Hint: Think about which git restore command specifically targets unstaged changes for a particular file without affecting other files.
What to observe/learn: How git restore can precisely target and undo uncommitted changes for individual files, leaving other pending changes untouched.
Common Pitfalls & Troubleshooting
Ignoring
.gitignoreissues:- Pitfall: Files you expect to be ignored are still showing up in
git statusor being committed. - Cause:
- The file was already tracked (committed) before you added it to
.gitignore. Git won’t ignore files it’s already tracking. - Incorrect
.gitignoresyntax or placement. - Global
.gitignoreconflicts.
- The file was already tracked (committed) before you added it to
- Solution:
- If already tracked:
git rm --cached <file>to remove it from Git’s index (but not your local filesystem), then commit the removal. After this,.gitignorewill work. - Verify
.gitignoresyntax (one pattern per line,!for exceptions,/for directory,**for arbitrary depth). - Check
git check-ignore -v <file>to see which rule is (or isn’t) matching.
- If already tracked:
- Pitfall: Files you expect to be ignored are still showing up in
Large Files in Git History:
- Pitfall: Your repository is huge, slow to clone, and GitHub gives warnings about large files.
- Cause: Accidentally committed large binaries, archives, or media files directly into Git’s history. Git stores every version of every file, so even if you delete it later, it’s still in the history.
- Solution: This is complex and involves rewriting history, which should only be done before pushing or on branches that haven’t been shared.
- Git LFS (Large File Storage): The recommended modern solution for tracking large files by storing pointers in Git and the actual content on a remote LFS server.
git filter-repo: A powerful tool (successor togit filter-branch) for rewriting history to remove large files. This is a drastic measure and requires careful execution.- Never commit large files directly. Add them to
.gitignorebefore committing.
- Recommendation: Use Git LFS from the start for any binary assets. For existing repos, consult
git filter-repodocumentation or seek expert help.
Accidentally force-pushing and losing work:
- Pitfall: You used
git push --forceand overwrote someone else’s (or your own) work on the remote. - Cause: Misunderstanding
git push --forceor using it carelessly. - Solution:
- Immediate action: If you realize quickly, you might be able to recover the lost commits from the reflog of a collaborator who had the correct history, or from your own local reflog (
git reflog) if you had a copy of the “correct” remote state before the force push. Find the hash of the lost commit andgit cherry-pickorgit resetto it. - Prevention: Always prefer
git push --force-with-lease. Educate your team on its dangers. Never force push tomain/developwithout explicit team coordination.
- Immediate action: If you realize quickly, you might be able to recover the lost commits from the reflog of a collaborator who had the correct history, or from your own local reflog (
- Pitfall: You used
Summary
Phew! We’ve covered a lot of ground in this troubleshooting chapter. You’ve learned to:
- Resolve merge conflicts by understanding conflict markers and manually editing files.
- Handle rebase conflicts using
git rebase --continueandgit rebase --abort. - Recover from a detached HEAD state by creating a new branch.
- Undo changes safely with
git restore,git reset(understanding itssoft,mixed, andhardmodes), andgit revert. - Address common remote issues like authentication and non-fast-forward pushes, and learned the critical difference between
git push --forceand--force-with-lease. - Identify and prevent pitfalls like
.gitignoreproblems and large files in history.
Troubleshooting is an essential skill for any developer. The more you practice these techniques, the more confident and efficient you’ll become. Remember, Git is designed to be resilient, and with these tools, you can fix almost anything!
What’s Next?
Now that you’re a Git troubleshooting expert, it’s time to put all your knowledge into practice in more complex, real-world scenarios. In the next chapter, we’ll explore advanced Git internals and real-world project scenarios, deepening your understanding of how Git truly works under the hood and how to apply these skills in large-scale team environments. Get ready to master Git!
References
- Git Official Documentation
- GitHub Docs: Resolving a merge conflict on GitHub
- GitHub Docs: Resolving merge conflicts after a Git rebase
- GitHub Docs: Managing large files with Git LFS
- Atlassian Git Tutorial: Undoing Changes
- Git-SCM: Git Restore documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.