Gittin' Out of Trouble
Git is one of those tools where you can get by on a surprisingly small set of commands most of the time, and then occasionally find yourself in a situation where you’re not sure what state anything is in or how to get back to somewhere safe. This is a reference for both of those scenarios.
The Basics
The commands I use most often, roughly in order of frequency:
git status
git add <file>
git add -p
git commit -m "message"
git push
git pull
git log --oneline
git log --oneline --graph
git diff
git diff --staged
git stash
git stash pop
git add -p lets you stage changes interactively, hunk by hunk.
This is helpful when you make several unrelated changes to a file and want to split them across commits.
A few others I reach for often enough to include:
git show <commit>
git blame <file>
git log -S "some string"
The last one searches commit history for changes that added or removed a specific string. Useful when you want to know where something came from or when it disappeared.
Undoing Commits
Git’s history model gives you a lot of flexibility here.
The key command is git reset, which moves the branch pointer backward.
How far it goes beyond that depends on the flag.
--soft undoes the commit but leaves your changes staged:
git reset --soft HEAD~1
No flag (the default, --mixed) undoes the commit and unstages the changes, but keeps them in your working tree:
git reset HEAD~1
--hard undoes the commit and throws the changes away entirely:
git reset --hard HEAD~1
HEAD~1 means “one commit before HEAD.” HEAD~3 would go back three.
The important thing to keep in mind with any of these: if you’ve already pushed the commit to a remote, you’ll need to force push after resetting, which rewrites the remote’s history. If you’re the only one using your branch, that shouldn’t be a problem, but be very careful force pushing to shared branches.
Amending
For smaller fixes to the most recent commit (a typo in the message, a file you forgot to stage), --amend is
cleaner than resetting and recommitting:
git add <forgotten-file>
git commit --amend
This opens your editor to modify the commit message and folds any newly staged changes into the existing commit. Same caveat as above: if the commit is already pushed, amending rewrites history.
Cleaning Up History
Before merging a feature branch, I usually want to tidy up the commits with an interactive rebase.
git log --oneline # to count how many commits back I want to go
git rebase -i HEAD~4
This opens an editor showing the last four commits, each prefixed with pick.
Changing the prefix changes what happens to that commit:
squashfolds it into the commit above itrewordlets you edit the messagedropremoves it entirelyeditpauses the rebase so you can make changes before continuing
Just :wq when you’re ready.
If you squashed commits, it’ll open the editor again to let you write a combined message.
Interactive rebase rewrites commit hashes, even for commits you didn’t touch, so the same force push caveat applies if the branch is already on the remote.
Rebasing a Branch
When a feature branch has fallen behind main, you have two options: merge or rebase.
Rebasing replays your branch’s commits on top of the latest main, which keeps history linear
and avoids a merge commit.
git fetch origin
git rebase origin/main
If there are conflicts, Git pauses at each conflicting commit and lets you resolve before continuing:
git rebase --continue
If things get complicated and you want to bail:
git rebase --abort
After the rebase, push with --force-with-lease rather than --force.
The difference is that --force-with-lease checks whether anyone else has pushed to the branch since your last fetch.
git push --force-with-lease
Merge Conflicts
Conflicts come up when two branches changed the same lines in the same file. Git marks them directly in the file:
<<<<<<< HEAD
what your branch has
=======
what the other branch has
>>>>>>> feature/foo
The resolution is editing the file to what it should actually say and removing the conflict markers. You can keep one side, the other, or write something new entirely. Then stage the resolved file and continue:
git add <file>
git merge --continue
To get a quick list of everything currently conflicted:
git diff --name-only --diff-filter=U
I usually work through conflicts directly in my editor.
For anything particularly messy, git mergetool can help; it opens a three-way diff view
using whatever tool you’ve configured.
Stashing
Stash is for when you need to set aside work in progress without committing. Basic usage is just:
git stash
git stash pop
By default, stash only touches tracked files. To include untracked files:
git stash -u
If you find yourself with multiple stashes, naming them helps:
git stash push -m "wip: some description"
git stash list
git stash pop stash@{1}
The Reflog
The reflog is probably the least-known safety net Git provides.
It records every position HEAD has occupied, including commits that no longer belong to any branch.
If you’ve done a hard reset, dropped commits during a rebase, or otherwise lost work, check here first:
git reflog
The output looks something like:
a1b2c3d HEAD@{0}: reset: moving to HEAD~1
e4f5g6h HEAD@{1}: commit: the commit you thought you lost
To recover, just create a branch at that hash:
git checkout -b recovery-branch e4f5g6h
The reflog is local and does expire (90 days by default for reachable commits, 30 for unreachable), but for the common case of realizing you made a mistake shortly after, it’s almost always there.
Detached HEAD
A detached HEAD happens when you check out a specific commit hash instead of a branch name. Git is telling you that HEAD is pointing at a commit rather than a branch, so any new commits you make won’t be attached to anything permanent.
If you haven’t committed anything in this state, just switch to a branch and move on:
git checkout main
If you have committed and want to keep that work, create a branch before leaving:
git checkout -b new-branch-name
That gives the commits somewhere to live.