Can't Remember That Slippery Git Trick? Try a Rhyme to Make It Stick!

Can't Remember That Slippery Git Trick? Try a Rhyme to Make It Stick!

Tony Distinti
Tony Distinti

March 31, 2022

Are you having a hard time remembering that one Git command? Or maybe you need new nursery rhymes to read the kids before bed? If you're like me, it's easier to commit details to memory when they're in a song or rhyme, so this blog can help you with both!

The verses below focus on the Git tricks that slip my mind from time to time. My attempt to describe them in rhyming stanzas is an attempt to make them easier to recall. I hope there's something in these rhymes that you can take away to assist you as well, whether it gets you out of a jam in Git or just helps you get the children to sleep faster.

How to Read the Rhymes

The rhymes assume you are familiar with Git already; they go beyond basic commands. The heading above each rhyme gives a sense of the situation or problem where the rhyme might come in handy. To hide the ball a bit, the actual command inspiring the rhyme is "hidden" away at the bottom of this page. Each rhyme (or series of related rhymes) is followed by a link to the command and a bit of discussion. These links are appropriately entitled "What?"

I tried to keep a consistent cadence with each rhyme. But that can be tricky when incorporating command-line flags or options. I suggest that when you see something like -p, you read it as "dash p," and when you see something like --patch, you read it simply as "patch."


Table of Contents

  1. Simple SHA Tricks for Various Commands
  2. 'git log' Tricks
  3. Further Wonders Using the `-p` Option (aka, `--patch`)
  4. `git diff` Tricks
  5. An Ode to `git reset --soft`, `git reset`, and `git reset --hard`
  6. Commands to Use If We Need to Iterate
  7. Apocalypse Now
  8. Examples and Discussions


1. Simple SHA Tricks for Various Commands


A. Do We Need That Whole Complicated SHA to Do Something?

Typing out SHAs
is a complicated pain.
Try using four letters
to keep yourself sane.

Git can figure it out
without characters ridiculous.
And Git will tell us
when the short SHA's ambiguous.

What?


B. We Don't Even Want a SHA! How Can We Quickly Refer to a Nearby Ancestor?

To refer to a parent
that's nearby a little,
append one of those characters
that looks like a squiggle.

And if you get scared
because there's no second parent,
remember to add a "2"
right after a caret.

What?


2. git log Tricks


A. Don't Need Your Life Story: How Can We See Commits That Belong Only to This Branch?

There are commits on two branches,
but one branch we'll pooh-pooh.
Put the excluded branch first
and then use the dots two.

Got more branches to exclude,
and you need a better format?
There can be another way,
using the character that's a top hat.

What?


B. Do These Branches Have a Different History? How So?

There are commits on two branches.
You want commits with exclusivity.
Put both branches in your `git log`
and use three dots, a trinity.

A list of unique commits
can be a muddled sight.
When you use the three dots,
make sure to add `--left-right`.

What?


C. We Did Something Somewhere. How Can We Search for It?

Some change happened
for which we must search.
We can use the command
that sounds like it's birch.

What?

To search for a change
in your branch tree,
reach for the flag
that's a capital G.

What?

Want to search through the messages
that somebody wrote?
Use the option that sounds
like an amphibian croak.

What?

Want to search all branches,
both the big and the small?
It's easy, but long-lasting:
append the option `--all`.

What?


D. These Commit Messages are Unhelpful

`git log -p` is one
for the ages.
It will show each commit
with accompanying changes.

What?


3. Further Wonders Using the -p Option (aka, --patch)


When manipulating the index
or the working directory,
`-p` can make commands
work incrementally.


A. We Can't Possibly Stage ALL These Changes for One Huge Commit!

To stage bits of changes
in a way that is partial,
`git add -p`
should be in your aresnal.


B. Ugh, We Staged Too Many Changes Anyway

With an overloaded index
that's unfit for commit,
`git restore -p`
will unstage bit by bit.


C. Can We Half-Stash this Half-Baked Idea?

To put aside hunks
you don't want to destroy,
`git stash save -p`
is what you'll employ.


D. Let's Go Back to the Way We Were, Just a Little.

To restore chunks of history
you think are worthwhile,
try `git checkout -p`
with a `<branch>` and a `<file>`

What?


4. git diff Tricks


A. Uh, Did I Leave any Improper Whitespace?

Leaving lots of whitespace
and your team says "What the heck!?"
Take a peek before you commit
with the helpful `git diff --check`

What?


B. This Diff Is Too Big To Understand :(

If the diff is a
bloated, swollen, lamentation,
exclude files with a
single-quoted colon, exclamation!

What?


C. We're About to Merge, but the Diff Makes No Sense!

You're eager to combine,
but fight the urge!
Diff with three dots
before you merge!

The target may have changed
since the days of yester,
but three dots create a diff
against the common ancestor.

What?


D. Time To Commit! Now What Did We Do Again?

Frustrated by the changes
you can’t see?
When you commit,
use `-v`!

What?


5. An Ode to git reset --soft, git reset, and git reset --hard


HEAD points to a branch
And a branch points to a commit.
Think of it like
A triple-decker sandwich.

`checkout` repoints HEAD.
It's safe without a file.
`reset` repoints the branch.
Learning the options is worthwhile:

Repoint your branch,
work and index you keep?
Use `git reset --soft`,
like you're not saying a peep.

Repoint your branch
And revert your index?
Keep `git reset` clean,
like you're spreadin' windex.

Repoint your branch,
blow all work to char?
Use the all powerful
`git reset --hard`

What?


6. Commands to Use If We Need to Iterate


A. List Tracked Files

What exactly is Git tracking,
After all these endless miles?
To get this list of data
you need `git ls-files`.

What?


B. List Commits

You want a list of SHAs,
just this itty bitty gist.
You can traverse a commit's history this way
by passing it to `git rev-list`.

What?


7. Apocalypse Now


A. I Messed Up My Interactive Rebase. Again.

After interactive rebasing,
myself I am kicking.
I messed things up badly
with my squashing and picking.
I know I can reset
with help from `reflog`
but when I see the log list
I'm all in a fog.
Many reflogs say "rebase",
but I need to have smarts.
For undoing, the HEAD I need is
right BEFORE "rebase" starts.

What?


B. I Think I Just Lost A Commit and reflog is No Help

You've lost a commit and
you're crazy with stress!
Use a Git curse word,
but replace `u` with `s`.

What?


C. I Need to Change Everything Since the Dawn of Time

All the history's messed up!
Apocalyptic avalanche!
To change every single commit,
you need to research `filter-branch`.

What?



Examples and Discussions



Example:

git show 9c4e

If we want to git show SHA 9c4368c, we can just type in the command above. If Git doesn't understand or needs more characters, it will let us know.



Examples:

git show HEAD~
git show HEAD~3
git show HEAD^
git checkout HEAD~^2

Let's say we want to git show the commit before HEAD, and then the commit before that, and then the commit before that. Appending a tilde (~), along with the number of steps back up the chain, is very handy.

# See the commit before HEAD:
git show HEAD~
# And see the commit before that:
git show HEAD~2
# And see the commit before that one:
git show HEAD~3

A caret (^) will also show us a parent commit. The key difference between ^ and ~ is that ~ only shows the "first" parent or a chain of "first" parents. This works fine if we only do fast-forward merges, where each commit only has one parent. But if this is not the case, the caret can be used to see any other parent commit that participated in a merge.

# HEAD was the result of a merge.
# Let's see the first parent of HEAD:
git show HEAD^ # git show HEAD~ would produce the same result

# But we also want to see the second parent of HEAD,
# the sibling of `HEAD^`
git show HEAD^2

You can even combine the tilde and the caret:

# Checkout the second grandparent of HEAD:
git checkout HEAD~^2



Examples:

git log branchOne..branchTwo

The syntax above with git log — using a two dot range operator ("the dots two") between branches — will list the commits on branchTwo and exclude those on branchOne. Using a caret (or "hat": ^) will do the same thing, but it requires this format:

git log branchTwo ^branchOne

And we can exclude a third branch (or more) using the caret format. Below we'll list commits on branchTwo but exclude commits reachable by branchOne or main:

git log branchTwo ^branchOne ^main



Example:

git log branchOne...branchTwo --left-right

This command will list the commits reachable from both branchOne and branchTwo that are NOT reachable by both of them. In other words, the command will list only commits exclusive to either branch. Getting a list of unique commits without more, however, can be confusing. The option --left-right will add a < or a > next to each SHA listed to indicate whether the commit belongs to the left or right branch in your command.



Birch?

Birches are hardwood trees with long, thin logs — and you can search through history with git log. There's also git grep, but:

That's a rhyme
for another time.

There's only so much we can do in one post, folks.



Example:

git log -p -G <regex>

The command above will search for a regex pattern in the diff associated with each commit on our current branch (we've added the -p option so git log will show us the diff along with each commit).



Croak?

grep sounds like a frog croaking to me...

Anyway, use --grep with git log to search for a pattern in the commit messages only. This will search the commit messages on your current branch:

git log --grep <regex>



Example:

git log -p -G <regex> --all

This command will search for a pattern in the diffs associated with every commit on every branch. It might take a while.



Example:

git log -p

Adding a -p flag to git log will cause the diff associated with each commit to be displayed beneath the commit.



Examples:

git stash save -p
git add -p
git restore -p
git checkout -p <branch> <file>

The -p option, or --patch, is incredibly handy when we want to perform a Git action with only some of the changes we have made. All in all, where applicable, -p allows us to take manipulative actions incrementally instead of wholesale.

For example, let's say we've made a lot of changes to our working directory. Some changes we want to add to our index to commit, and some changes we want to stash for later. The git add -p command will look at all the unstaged changes in our working directory and break them into "hunks" for us. A prompt from Git will show us each hunk one by one, asking if we'd like to stage the hunk, disregard it, split it into smaller hunks, etc.

Once we are done adding our selected changes to the index and we commit them, we can run git stash save -p to go through a similar process to add selected bits of changes to save in a single stash.

With most of the commands here, we're not destroying any work; we're either committing it or stashing it. The big exception is:

git checkout -p <branch> <file>

It is NOT "working directory safe." We could lose some uncommitted work if we're not careful.



Examples:

git diff --check
git diff --cached --check

These commands will tell us where there are whitespace errors in our unstaged changes or staged changes, respectively. We can configure the types of whitespace errors to display via our Git config file.



Example:

git diff main branchOne ':!*Tests.cs' ':!*Mock.cs' ':!*Mother.cs'

Using single quotes, a colon, and an exclamation point, we can tell git diff to ignore certain files. For example, the line above will compare the main and branchOne branches, but the diff will exclude files ending in Tests.cs, Mock.cs, or Mother.cs. The asterisks are wildcard operators.



Could you say that again?

To get a diff against
common ancestry,
use the dots --
one, two, three!

Example:

git diff main...branchOne

Here's the problem this command solves: Let's say we branched off main, creating branchOne and committing work to the new branch. We want to run a diff to double-check all the changes we made. But others may have made merged changes into main since we branched off (and let's assume those updates are reflected on our local main). Running git diff main branchOne (without the three dot operator) will generate a literal diff against the current state of main. As a result, we might see changes we don't recognize at all.

Using the three dot operator between branches when diff'ing is meant to solve this problem. It's a shortcut to ensure that the base of comparison is the common ancestor between the relevant branches. Here, using the three dots will diff against the original commit when we branched off of main. In other words...

git diff main...branchOne

...will show only our own changes since the moment we branched off.

More about this problem and the command can be found here.



Example:

git commit -v

Technically, this is not a trick using git diff. It's a trick using git commit. But adding the -v option displays a diff of the changes you are commiting when the commit text editor opens. The diff will not be saved as part of the commit message.



On git reset, there's a lot to unpack. I can never remember the difference between git reset --soft and plain ol' "clean" git reset. (I find the effects of git reset --hard easy to remember thanks to a healthy sense of self-preservation). "Pro Git" has a fantastic chapter on demystifying reset. It's worth a read or two or seven.

The story I tell myself to keep everything straight basically starts with remembering that HEAD and a branch are two different pointers:

HEAD points to a branch
And a branch points to a commit.

git checkout repoints HEAD somewhere else — usually another branch. (If we point to another commit, we enter the mystical "detached HEAD" state). git checkout is "working directory safe", and you won't lose work UNLESS you tell Git to checkout a specific file or files:

`checkout` repoints HEAD.
It's safe without a file.

Meanwhile, using the git reset command keeps HEAD pointing at your current branch, but it changes the commit to which the current branch points. What happens to your current working state? git reset --soft leaves your working directory and index alone. Plain old git reset without any options (i.e., "git reset clean") leaves your working directory alone, but resets your index to look the same as the target commit (without specifying a commit, the default is HEAD). As "Pro Git" notes, if you're slick with git reset --soft and git reset, you can use them to quickly undo a commit or to quickly squash a series of commits.

git reset --hard is the only reset command here that is not working directory safe. It's a nuclear option, resetting both the index and working directory to a previous commit (defaulting to HEAD). Any uncommitted or unstashed work-in-progress will be lost.



Example:

git ls-files

This command will list files that Git tracks, including files that are staged in our index but not in a commit yet. This command can be particularly helpful if we need to iterate over a list of files in our project but we want to ignore anything subject to the .gitignore file. git ls-files will default to the files tracked as-of our current branch. But the command can take as an argument a particular branch or commit, and the command will list which files are tracked at that particular point.



Example:

git rev-list

git log lists commits on the current branch in reverse chronological order, including each commit's SHA and commit message. But let's say we just want a list of the SHAs, nothing else? That's what git rev-list will do. We can pass a particular branch or commit as an argument to see the SHAs from that point backwards. Otherwise git rev-list will default to starting with the last commit on our current branch.



This particular poem addresses a problem that is hopefully unique to me: I've just done an interactive rebase, squashing commits, editing others, etc. Now I realize I messed something up, and I want to go back to the way things were before I touched git rebase -i <commit>.

I know I can use git reflog to see where HEAD was at various stages in time. And I know I can use git reset -hard <reflog entry> to return my working directory and index to the way things were at a particular time. But when I look at the results of git reflog, I see a ton of HEAD@{_} entries that contain the word rebase. Which one do I pick??

The answer is to use the HEAD@{_} entry before the ones with the rebase flag. And because the reflog list is in reverse chronological order, the correct reflog will be just below the rebase entries and have a higher index number in between the curly brackets ({_}).

I've learned over time that the reflog entry I need might indicate a checkout just took place, or perhaps a commit...it can be different, which can be confusing. But what I have to keep telling myself is, "You want to reset to the reflog before 'rebase' starts to appear."

As an example, here's a reflog after I've done some rebasing on a branch called gitPoetryRebase. If I wanted to undo all my rebasing, I'd do

git reset --hard HEAD@{10}

because HEAD@{10} is the first reflog entry that came before the first rebase reflog entry. Remember, the reflog entries are in reverse chronological order.

5dfaa82e (HEAD -> gitPoetryRebase) HEAD@{0}: rebase (continue) (finish): returning to refs/heads/gitPoetryRebase
5dfaa82e (HEAD -> gitPoetryRebase) HEAD@{1}: rebase (continue) (pick): Add iteration commands
2c5f6331 HEAD@{2}: rebase (continue) (squash): Refactor
06898e54 HEAD@{3}: rebase (continue) (squash): # This is a combination of 3 commits.
2c740175 HEAD@{4}: rebase (continue) (squash): # This is a combination of 2 commits.
47e7dbd7 HEAD@{5}: rebase (edit): Hiding answers down below
e2352864 HEAD@{6}: rebase (pick): Playing with title
88b13ed5 HEAD@{7}: rebase (squash): Corrections
88a1cb46 HEAD@{8}: rebase (start): checkout 8d6685d0
f144dddf HEAD@{9}: reset: moving to HEAD
f144dddf HEAD@{10}: checkout: moving from gitPoetry to gitPoetryRebase
94b751ef (gitPoetry) HEAD@{11}: commit: About to do rebase demo
f144dddf HEAD@{12}: checkout: moving from gitPoetryRebase to gitPoetry
f144dddf HEAD@{13}: checkout: moving from gitPoetry to gitPoetryRebase
f144dddf HEAD@{14}: commit: Add iteration commands
b365f2c2 HEAD@{15}: commit: Add reset



Example: git fsck

That's a real command! I've learned recently that fsck stands for file system check.

But what's it good for? Let's say we just used git reset --hard <commitXYZ> to go way back in time. Unfortunately, we went too far; there's a commit after commitXYZ that we need. And, although unlikely, let's assume git reflog is of no help. What can we do?

Running git fsck will check the integrity of your Git "database" (that is, the Git objects associated with your project). The command will provide a list of any "dangling" commits; each one will have no other Git objects pointing to it. Hopefully this list will have our missing commit, which we can then checkout and review to bring down our blood pressure.

You can read more about such nightmarish scenarios here.



Base command:

git filter-branch

This command is the true nuclear option to change all of your repository's Git history in a permanent way. Even the Git documentation says "[the command's] use is not recommended." The documentation goes so far as to suggest using another tool, git filter-repo.

Hopefully you'll never have to use either one. At the very least, may this rhyme help us to remember filter-branch and what a hot button command it is.