Git - What to do when things go wrong

June 05, 2021

I thought I’d detail some things that you can do when you get issues with git. Specifically, how you can deal with issues without having to resort to manually copying files around.

For this purpose of this post, I’ll assume that you have a git repository, and that some code has been pushed to it. I’ll be using git bash for the commands in this post.

Status and Log

Before we start working out how we can revert or fix issues that go wrong, let’s first see how we can reliably, and sensibly see what state we’re in.

Let’s start with having a look at the story so far. For this we can use the git log command:



git log --oneline

git

Our history has some commits in already - these have been pushed to the remote repository.

To demonstrate these commands, firstly, we’ll change a file:



$ echo "some test" >> test.txt

This file hasn’t been staged yet; we can prove this by using the git status command:



$ git status -s

This gives us a nice display of the state of our pending changes:

git

What git status -s gives you is a report of which files are staged, and which are committed in the following manner:



XY filename

Where X is a staged change, and Y is an unstaged, but tracked change. For example, if we stage the change to test.txt:

git

Of course, a file can have both staged, and unstaged changes:

git

Finally, let’s commit and push this change, and then view the log again:



git log --oneline

git

When things go wrong

Okay, so what we’ll cover here is how to revert changes that are in various states. The states that a file can exist in in git are:

1. Untracked
2. Tracked and unchanged
3. Tracked and changed

Let’s start with (1) - untracked. We’ll create a new file:



$ echo "new file 2" >> newfile2.txt
$ git status -s

This shows up like so:

git

The question mark here is telling us that git doesn’t know about this file - it’s untracked. In fact, if you just call git status it will tell you that the file is untracked.

Remove Untracked files

Okay, so by default, git will not remove newfile2.txt for you - you’re working away and you type the wrong command, you don’t really want git to go and wipe your files; however, sometimes, you do.

Because we aren’t tracking the new file, we’ll need the command:



git clean

However, if you run this, you’ll get the error:

fatal: clean.requireForce defaults to true and neither -i, -n, nor -f given; refusing to clean

The reason here is that, by default, git tries to prevent you from breaking things - and removing all the files in a directory that git doesn’t know about it quite drastic. We’ll therefore need the -f parameter to force the issue:



git clean -f

This will remove any untracked files that it finds; however, it will leave directories, and also anything that you’ve explicitly told it to ignore. To remove absolutely everything, we’ll include two extra parameters:



git clean -f -d -x

That will remove anything that git doesn’t know about, whether or not you’ve told it to ignore the files. Obviously, use this with caution - it WILL delete files and you WILL NOT be able to restore them - however, it will ONLY remove local files - any remote repository will be unaffected.

Let’s now consider a situation where you have made a change to a file, but you wish to revert to the unchanged state.

Reverting a tracked change

There is more than one category of tracked change; they are:

1. Changes that are unstaged
2. Changes that are staged
3. Changes that are committed
4. Changes are committed and pushed to a remote repository

All four of these can represent different changes in the same file. Let’s change a file so that it’s different in all four stages:



echo "change type 4" >> test.txt
git add .
git commit -m "change type 4"
git push
echo "change type 3" >> test.txt
git add .
git commit -m "change type 3"
echo "change type 2" >> test.txt
git add .
echo "change type 1" >> test.txt

We can see the status of the local repository by issuing a status command:



git status -s


This will show that the file has both stages and unstaged changes:

git

And of the remote by viewing the tree:



git log --all --graph --decorate --oneline

git

So, we can see that each category of change is represented. Let’s start by reverting the unstaged change.

Unstaged

This is perhaps the easiest to revert, as there’s a command designed specifically for it:



git restore .

This will leave staged changes, but will discard any unstaged changes to the state of HEAD. If you wish the keep these changes for later, then stash might be a better answer.

Staged

One way of unstaging changes is to use the git restore command with the parameter staged:



git restore --staged .

Doing this will unstage the change that you’ve made; for example:

git

Similarly, you can issue a reset command for the same purpose:



git reset HEAD

Which has the same effect.

However, if you wish to completely discard the staged change, you can issue a reset with the hard parameter; for example:

git

None of these commands will affect any of the commits made:

git

Let’s look at how you can revert a local commit.

Undoing a local commit

If we look at the result of git log above, we get a clue as to how this may be possible, commit 335cf90 points to the local master branch, but origin/master points to a remote master branch. So we can just point our local HEAD back to the remote. There’s a few ways that this can be done; they are quite subtly different.

git revert

To move to the state of a previous commit, you can issue a git revert. However, this does not actually revert the commit, it simply creates a new commit which is the reverse of the one that’s already there.

This can result in merge conflicts, depending on the state of the local and remote branch.

Once you’ve started a git revert, you can issue a **```

git revert —abort

** to abandon the revert.

#### git reset

A quick way to discard a local commit is to simply reset HEAD to the **origin/master**:


$ git reset —hard origin/master HEAD is now at 00f3c8e change type 4




## Undoing a remote change

Because you're now manipulating the remote repository, this carries a very real risk of causing some serious damage.

The first step here is to set your HEAD, as before, to a previous commit; for example:


$ git reset —hard origin/master~1




This will set your HEAD to the commit 1 before the current remote **master** (to go back two, you would use **master~2**

If you now look at your log, you'll see that it now looks a little strange:

![git](images/git-12.png)

There's a couple of things to note here - the first is that you now have a **detached HEAD** - this means that your HEAD no longer refers to a **branch**.  Secondly, your branch and the remote branch are now different.  To complete this process, you can push the change; however, if you just call **git push** you'll get something like the following error:


$ git push fatal: You are not currently on a branch. To push the history leading to the current (detached HEAD) state now, use

git push origin HEAD:<name-of-remote-branch>



What's needed here is a bit of force:


$ git push -f origin master




Looking at the history now, we can see that the latest commit is gone:

![git](images/git-13.png)

It's worth bearing in mind that, one of the great things about git is that, nothing is ever really _gone_.  If we use **git reflog** we'll see that the "lost" commit is, in fact, safe and well - just orphaned:


git reflog




![git](images/git-14.png)

You can even see this in the tree structure:


$ git log —graph —all —oneline —reflog




![git](images/git-15.png)

If we now add a new commit, we can use this command to see where we went back to and forked:

![git](images/git-16.png)


Profile picture

A blog about one man's journey through code… and some pictures of the Peak District
Twitter

© Paul Michaels 2024