Handy Git Commands for Fixing & Avoiding Issues
By: Cassidy Schaufele
Tags: Git, Github, Guild Education, Software Development

When it comes to version control, Git is a complex tool — it’s incredibly full featured, and it gives you the power to shoot yourself in the foot. Anybody who’s used Git has probably done this more than once!

With Git, it can be easy to accidentally delete your work, or make unintentional changes that we don’t know how to fix. Sometimes, our attempts to make fixes can even make a situation even worse!

The good news is that Git also gives us the tools and power to undo our mistakes. As complicated as Git can be, it’s pretty difficult to get into a state that’s unrecoverable.

My goal is to show you a few of those tools. Below, I’ve picked a handful of my favorite commands to get you out of or away from Git disasters.

git reflog

We’ll start with the Swiss Army knife of getting a repository back to being kosher. The reflog is a record of your local repository’s changes on a commit-by-commit basis — where every change you make locally is visible, along with a local commit hash.

Running git reflog lets you view that record, which makes it an amazing tool for undoing git changes. Essentially, it’s the most powerful safety net your repository has.

Let’s see an example. I’ve set up a new repository and pushed several commits within a branch. I’ve also run several rebases to alter the original commits.

By running git reflog , I have access to the entire history of my repository’s state. I can see every time that state has changed, and I can revert to those states via the commit hashes in the reflog.

If I want to go back to the state my branch was in at my initial commit, I can run git reset --hard 95997ab to move my branch back to that commit, before I started rebasing.

Let’s say those rebases need to be removed from the remote of the branch. From here, I can force push to update the remote branch and remove the changes from the rebase. We’ll run git push --force-with-lease to destructively update our remote branch with our local reversion¹.

Looking with git status and git log , we can see that we only have our initial commits. Our force push has also updated the remote branch with our local state.

But wait! We went a little too far back in history and accidentally dropped two commits from our local branch before we force pushed. But that’s okay — the work isn’t lost, and we can get it back via the reflog. Let’s look at our original reflog again.

This time, we want to go back to 99c8cc4.

The reflog still has the commit hashes for our previous states — and we can use those hashes to go back before our force push. In this case, let’s move to 99c8cc4 , our commit hash for the Added second test function commit.

Just like that, the commits are back!

Phew! We’ve recovered the commits we accidentally dropped, and now we can do another force push to restore that work to our remote branch.

Our work is successfully restored!

git reflog can be a “get out of jail free” card, and it allows you to undo any changes you’ve made locally. It’s easily the most powerful command you can use when debugging or resetting your local repository.

git reset --hard @{u}

You might have used git reset --hard before, to reset modified files, or just to get back to a clean local state. But what if your local state isn’t clean?

We could try to retrace our steps via reflog, but depending on the situation, we might just want to get back to the state of our remote branch.

Here’s an example. In this branch, I’ve just run a rebase and resolved a merge conflict. As a result of this, two of my local commits are different from the commits on the remote branch.

If I was satisfied with my rebase, I would push --force-with-lease to update the remote branch with my local changes. But what if I wasn’t satisfied, and wanted to start over? I could use reflog to backtrack, or I could rebase again to drop those commits before pulling from the origin. Those are both viable — but what I’m trying to do is just reset to the remote.

Here’s where using @{u} can come in handy. @{u} is shorthand for the last fetched HEAD of the upstream branch. I can use this shorthand to quickly reset my local branch to the remote’s head.

Let’s run git reset --hard@{u} .

After running the reset, we’ve restored the state of our local repository to match the state of our remote branch.

If your local repository is in a bad state but you haven’t pushed anything to the remote, this is a great way to start over from scratch — perfect for a merge conflict gone awry, or for a branch that’s gotten so broken that you just want to start over.

git cherry-pick

There might be a bit of a gut reaction to seeing cherry-pick on this list, since cherry picking is a bit controversial, but there’s more to cherry-pick than grabbing commits from other branches. We can also cherry pick from the reflog .

This is very useful for retrieving commits that have been accidentally dropped.

Let’s go back to our previous example. I had two commits that differed from the remote, and I removed both when I reset the local repository.

What if I wanted one of those commits back after running the reset?

Running git reflog , we can see the commit hashes for both the reset and rebase commands.

I can then git cherry-pick the commit I want to restore from the original rebase, by running the command with the hash of the original commit.

Let’s run git cherry-pick 3290bb3 to restore that second commit.

After running the cherry-pick , I’ve successfully restored the original commit.

git commit --fixup <commit-hash>

git rebase -i --autosquash

The other commands I’ve shown are great for reacting to an issue. For a change, here’s a set of commands to be proactive instead of reactive.

Keeping commit histories clean and maintaining good commit order helps when it’s time to merge or rebase, but it can be difficult and tedious to move commits around or squash down revisions manually.

Let’s look at an example.

Here, we have a branch with multiple commits. I’ve made a series of commits, but I notice I’ve made in error in 1a6495f Added test function . I’m going to fix this by adding another commit containing my fix.

Now, I could leave that commit there, but to keep the history of commits clean, I want to move my fix into that original commit.

To do this, I’ll run git commit with the --fixup flag and that hash of 1a6495f .

This creates a commit with the prefix of fixup! . Now, we can run a rebase with the autosquash flag.

I’ll run git rebase -i --autosquash 95997ab to open an interactive rebase. We use a commit hash before the commit we want to alter to ensure that commit makes it into the rebase.

Our fixup commit is autoformatted and in the proper order.

By running with --autosquash , our fixup commit has already been formatted with the --fixup command and has been automatically placed in front of our original commit. Once we save and start the rebase, the fix will automatically squash into our original commit — nifty!

The rebase is complete, and we can see that our fix has been merged with the original commit.