Bash: A Simple Script for Automating Git Reset

Don't you just hate doing Git Log, sifting through a bunch of commit hashes and then copy-pasting--all so you can reset to an earlier commit? I do--particularly the part where I need to select, copy and then paste the stupid hash. Today was a breaking point, actually. I finally wrote a script that will find the hash I want and pass it to Git Reset for me. My day is finally looking up.

Background

I needed to un-merge a branch today (stakeholders backed out--it happens). This meant reverting my preview branch to a commit just before the merge. This is exactly the kind of thing, I think, that pushes so many devs to graphical Git clients (like, GitKraken, et. al.); doing a reset on the command line is kind of pain. You need to run git log, find the earlier hash for a commit you want to reset to, copy it, and then paste it into a git reset command. Really though, if you can run git log the output you need is pretty much staring you in the face. In order to pass the hash to the next command, all you really have to do is "parse" the output (i.e., narrow it down to the exact commit hash you're after) it and store it in a variable. That's actually not too difficult to do.

Use Case

If you've seen my Git-Add-Commit-Push post, you know I like to go big. I want a script for hard resets. Like the 80's guy from Futurama, I'm a shark: I never look back--not unless I've screwed things up so bad that I basically have to throw away all the work I've done since my last stable commit.

What I'd really like is to be able to do is count back in my commits to an earlier commit. I'd like to take that number--i.e., 3 commits back--and use the number to have my shell store the hash for that commit in a variable. I'll look at a way to simply reset to whatever the last commit was, too (basically, whatever the 2nd commit in log history is). Once the variable is stored, it gets passed to git reset --hard and I never look back.

Pro tip: if you'd like to number the output of git log so that it's easier to count back through your commits, try this $ git log -X --oneline --no-abbrev-commit | nl -- replace X with the number of commits you'd like to limit output to.

This script is actually really short. Don't get me wrong--it packs a lot of punch, and it leverages several command line tools. All in all, though, there are only two lines you really need. Before we look at the actual script, though, let's see it in action. Below, after reviewing git log, I decide I need to go back 3 commits. I call my script with an alias, reset, and tell it to go back 3 commits:

a bash script that handles git reset
"When it hurts so bad... why's it feel so good?"--Lauryn Hill

The Script

And, now, ladies & gentz: the script:

#! /bin/bash
# ~/Scripts/reset.sh

# grab hash for nth commit in log history
hash=$(echo "$(git log -$1 | grep commit | cut -b 8-)" | tail -n1)
# sanity check
echo "Hard reset to $hash ?" | lolcat
# prompt yes or no and store value in yn var
read yn
# if yn var equals 'y' run git reset; if any other value commence ridicule
[ "$yn" = y ] && git reset --hard $hash || echo "Chicken?" | lolcat

Short and sweet!--but there's actually quite a lot going on here. Let's look at the first line:

# grab hash for nth commit in log history
hash=$(echo "$(git log -$1 | grep commit | cut -b 8-)" | tail -n1)

Let's start with the inner parenthesis. The inner parenthesis consist of three different commands: git log, grep and cut--all piped together into one value. The parenthesis surrounding them "$()" is what groups them into a single value. So, "$(git log -$1 | grep commit | cut -b 8-)" is basically a placeholder for whatever these three commands output when combined. Let's look at each command, starting with git log.

You should be familiar with the output of git log. It's in the above screen grab, but here it is again--just in case:

git log output
So much to take in...

In this exact screen grab, I knew my last stable commit was three commits ago, so I limited the output to the last three entries, i.e., git log -3. You can run just git log if you don't know how far back the commit you're looking for is. In either case, you do need to count how far back in your history the commit is--that number is passed when I run the script: i.e., $ reset 3.  The value of '3' automatically gets stored in the $1 variable of the script. So, coming back to the git command in the parenthetical, git log -$1 would be equal, in this case, to git log -3. Since I know I want to reset to the third commit, I don't want git log to give me any output beyond that commit.

Glancing up at the output of git log again, what in the output can I use to isolate the hashes? Each hash is preceded by the word commit. So in order to show only the commits, I combine git log with grep. I tell grep to look for lines with the word commit, i.e., grep commit. By paring the two commands together, i.e., git log -3 | grep commit, I get a nice clean output with just the commit hashes... and the word "commit" (see screen grab below).

I still need to isolate just the hashes, though--since that's what I pass to the git reset command. I can clean my output up even more by using cut. In order to shave the first seven bytes from each line of the output (namely, "commit "), I tack on a third command: git log -3 | grep commit | cut -b 8-. Here's what the output looks like:

parsing output of git log
Things are starting to size down nicely.

So, the value of "$(git log -$1 | grep commit | cut -b 8-)" is basically three lines of hashes at this point. All that I need to do now is isolate the last one (number 3)--that's the one I'm after.

To isolate the last one, let's now look at the outer parenthesis: $(echo "$(git log -$1 | grep commit | cut -b 8-)" | tail -n1)

Here, I'm basically saying, show the value of the inner parenthesis (a. la. echo), and remove all but the last line with tail. Here's what that looks like on the command line:

a fully extracted hash from git log
There it is--the lone hash!

So we take all that crazy business and store it in a variable called hash--i.e., hash=$(echo "$(git log -$1 | grep commit | cut -b 8-)" | tail -n1). Then we can take our hash variable and pass it to git reset.

Looking at the script one more time, you should now be able to see everything that's going on:

#! /bin/bash
# ~/Scripts/reset.sh

# grab hash for nth commit in log history
hash=$(echo "$(git log -$1 | grep commit | cut -b 8-)" | tail -n1)
# sanity check
echo "Hard reset to $hash ?" | lolcat
# prompt yes or no and store value in yn var
read yn
# if yn var equals 'y' run git reset; if any other value commence ridicule
[ "$yn" = y ] && git reset --hard $hash || echo "Chicken?" | lolcat

Do you have to pipe the echo commands with lolcat? No. I just like a little color on my terminal. Make sure it's installed before running the script as is.

A 'Last' Option

Most of the time, when I need to do a reset, it's not because of a mistake I made 10 or 20 commits ago. It can happen, of course, but often it's a problem with the commit I just made. In that case, you really don't need to open git log and go looking for a hash. Let's, lastly, build out a way to abandon the counting scheme and instead just pass a value of "last" to our $1 variable: if the $1 variable is set to "last", we just reset to whatever the last commit is.

This sounds a lot harder than it actually is. But, since the last commit is always the second one in the log history, we just need to equate "last" with "2"; we actually don't have to mess with much of what we've already put in the script. We can just add a little logic above what we already have and swap out the $1 variable with a new one called num. Something like this:

#! /bin/bash
# ~/Scripts/reset.sh

if [ "$1 = last ]; then
  num="2"
else
  num="$1"
fi

# grab hash for nth commit in log history
hash=$(echo "$(git log -$num | grep commit | cut -b 8-)" | tail -n1)
# sanity check
echo "Hard reset to $hash ?" | lolcat
# prompt yes or no and store value in yn var
read yn
# if yn var equals 'y' run git reset; if any other value commence ridicule
[ "$yn" = y ] && git reset --hard $hash || echo "Chicken?" | lolcat

That should do it. With the script properly aliased (say, as 'reset') and set as executable, you could now run it one of two ways:

$ reset X where X = the number of commits back you'd like to reset to, or

$ reset last to simply reset to the last commit.

Without an alias, you would run it like so:

$ ./reset.sh X or $ ./reset.sh last

You don't have to use this script with a hard reset; nor do you even have to use it with the reset command. You could also use this with the checkout command. You could additionally pass the --hard option to the script as a second inline variable when calling the script--you'd need to do some light fidgeting with the script, though. Otherwise, enjoy!