collaborated on an ML challenge — rebasing characteristic branches, merging experiment notebooks, or cleansing up commits earlier than a pull request — did you ever get to a degree the place you stated: “uh-oh, what did I simply do?”
For any information scientist who works in a group, having the ability to undo Git actions generally is a life saver.
This publish provides you with the instruments to rewrite historical past with confidence.
Notes earlier than we begin
- I additionally gave a stay speak masking the contents of this publish. In the event you favor a video (or want to watch it alongside studying) — you will discover it right here.
- 📕 My ebook “Gitting Issues Finished” is formally out!
Get it on Amazon, as an eBook, or learn it without spending a dime at https://www.freecodecamp.org/information/gitting-things-done-book/
Recording modifications in Git
Earlier than understanding the way it undo issues in Git, you must first perceive how we document modifications in Git. In the event you already know all of the phrases, be at liberty to skip this half.
It is rather helpful to consider Git as a system for recording snapshots of a filesystem in time. Contemplating a Git repository, it has three “states” or “bushes”:

Often, once we work on our supply code we work from a working dir. A working dir(ectrory) (or working tree) is any listing on our file system which has a repository related to it. It accommodates the folders and recordsdata of our challenge, and likewise a listing known as .git. I described the contents of the .git folder in additional element in a earlier publish.
After you make some modifications, you could wish to document them in your repository. A repository (in brief: repo) is a set of commits, every of which is an archive of what the challenge’s working tree regarded like at a previous date, whether or not in your machine or another person’s. A repository additionally contains issues aside from our code recordsdata, similar to HEAD, branches and so on.
In between, have the index or the staging space, these two phrases are interchangeable. After we checkout a department, Git populates the index with all of the file contents that had been final checked out into our working listing and what they regarded like once they had been initially checked out. After we use git commit, the commit is created primarily based on the state of the index.
So the index or the staging space is your playground for the subsequent commit. You may work and do no matter you need with the index, add recordsdata to it, take away issues from it, after which solely when you find yourself prepared prepared, you o forward and decide to the repository.
Time to get palms on 🙌🏻 Use git init to initialize a brand new repository. Write some textual content right into a file known as 1.txt.

Out of the three tree states described above, the place is 1.txt now?
Within the working tree, because it hasn’t but been launched to the index.
-> git init
Initialized empty Git repository in
/house/omerr/repos/my_repo/.git/
-> echo "Whats up world" > 1.txt

As a way to stage it, so as to add it to the index, use git add 1.txt.

Now we are able to use git commit to commit our modifications to the repository.
-> git add 1.txt
-> git commit -m "Commit 1"
[main (root-commit) c49f4ba] Commit 1
1 file modified, 1 insertion(+)
create mode 100644 my_file.txt

You created a brand new commit object, which features a pointer to a tree describing your complete working tree. On this case, it’s gonna be solely 1.txtinside the root folder. Along with a pointer to the tree, the commit object contains metadata, similar to timestamp and creator’s data. For extra details about the objects in Git (similar to commits and bushes), take a look at my earlier publish.
(Sure, “take a look at”, pun meant 😇)
Git additionally tells us the SHA-1 worth of this commit object. In my case it was c49f4ba (that are solely the primary 7 characters of the SHA-1 worth, to avoid wasting house). In the event you ran this command in your machine, you’ll get a special SHA-1 worth, as you’re a totally different creator, and likewise you’ll create the commit on a special timestamp.
After we initialize the repo, Git creates a brand new department (named essential by default). And a department in Git is only a named reference to a commit. So by default, you might have solely the essential department. What occurs you probably have a number of branches? How does Git know which department is the lively department?
Git has one other pointer known as HEAD, which factors (often) to a department, which then factors to a commit. By the way in which, beneath the hood, HEAD is only a file. It contains the identify of the department with some prefix.
Time to introduce extra modifications to the repo!
Now I wish to create one other one. So let’s create a brand new file, and add it to the index, as earlier than:
-> echo "second file" > 2.txt
-> git add 2.txt

Now, it’s time to make use of git commit. Importantly, git commit does two issues:
First, it creates a commit object, so there’s an object inside Git’s inner object database with a corresponding SHA-1 worth. This new commit object additionally factors to the mother or father commit. That’s the commit that HEAD was pointing to while you wrote the git commit command.
-> git commit -m "Commit 2"
[main 43c8b29] Commit 2
1 file modified, 1 insertion(+)
create mode 100644 2.txt

Second, git commit strikes the pointer of the lively department — in our case, that might be essential, to level to the newly created commit object.

Undoing the modifications
To rewrite historical past, let’s begin with undoing the method of introducing a commit. For that we’ll get to know the command git reset, an excellent highly effective software.
git reset --soft
So the final step you probably did earlier than was to git commit, which really means two issues — Git created a commit object, and moved essential, the lively department. To undo this step, use the command git reset --soft HEAD~1.
The syntax HEAD~1 refers back to the first mother or father of HEAD. If I had a couple of commit within the commit graph, say “Commit 3” pointing to “Commit 2”, which is, in flip, pointing to “Commit 1”. And sayHEAD was pointing to “Commit 3”. You might use HEAD~1 to discuss with “Commit 2”, and HEAD~2 would discuss with “Commit 1”.
So, again to the command: git reset --soft HEAD~1
This command asks Git to alter no matter HEAD is pointing to. (Observe: within the diagrams beneath I exploit *HEAD for “no matter HEAD is pointing to”). In our instance, HEAD is pointing to essential. So Git will solely change the pointer of essential to level to HEAD~1. That’s, essential will level to “Commit 1”.
Nonetheless, this command did not have an effect on the state of the index or the working tree. So when you use git standing you will note that 2.txt is staged, identical to earlier than you ran git commit .
-> git reset --soft HEAD~1
-> git standing
On department essential
Adjustments to be dedicated:
(use "git restore --staged ..." to unstage)
new file: 2.txt
-> git log
commit 935987552aa5f8d3358f89... (HEAD -> essential)
Writer: Omer Rosenbaum <[email protected]>
Date: ...
Commit 1

What about git log? It’ll begin from HEAD , go to essential, after which to “Commit 1”. Discover that which means that “Commit 2” is now not reachable from our historical past.
Does that imply the commit object of “Commit 2” is deleted? 🤔
No, it’s not deleted. It nonetheless resides inside Git’s inner object database of objects.
In the event you push the present historical past now, by utilizing git push, Git won’t push “Commit 2” to the distant server, however the commit object nonetheless exists in your native copy of the repository.
Now, commit once more — and use the commit message of “Commit 2.1” to distinguish this new object from the unique “Commit 2”:
-> git commit -m "Commit 2.1"
[main dc0cb50] Commit 2.1
1 file modified, 1 insertion(+)
create mode 100644 2.txt

Why are “Commit 2” and “Commit 2.1” totally different? Even when we used the identical commit message, and despite the fact that they level to the identical tree object (of the basis folder consisting of 1.txt and 2.txt ), they nonetheless have totally different timestamps, as they had been created at totally different occasions.
Within the drawing above I stored “Commit 2” to remind you that it nonetheless exists in Git’s inner object database. Each “Commit 2” and “Commit 2.1” now level to “Commit 1″, however solely “Commit 2.1” is reachable from HEAD.
git reset –combined
It’s time to go even backward and undo additional. This time, use git reset --mixed HEAD~1 (be aware: --mixed is the default swap for git reset).
This command begins the identical as git reset --soft HEAD~1. Which means it takes the pointer of no matter HEAD is pointing to now, which is the essential department, and units it to HEAD~1, in our instance — “Commit 1”.
-> git commit -m "Commit 2.1"
[main dc0cb50] Commit 2.1
1 file modified, 1 insertion(+)
create mode 100644 2.txt
-> git reset --mixed HEAD~1

Subsequent, Git goes additional, successfully undoing the modifications we made to the index. That’s, altering the index in order that it matches with the present HEAD, the brand new HEAD after setting it in step one. If we ran git reset --mixed HEAD~1 , it means HEAD could be set to HEAD~1 (“Commit 1”), after which Git would match the index to the state of “Commit 1” — on this case, it signifies that 2.txt will now not be a part of the index.

It’s time to create a brand new commit with the state of the unique “Commit 2”. This time we have to stage 2.txt once more earlier than creating it:
-> git standing
on department essential
Untracked recordsdata:
(use `git add file<...>" to incorporate in what might be dedicated)
2.txt
-> git add 2.txt
-> git commit -m "Commit 2.2"

git reset –exhausting
Go on, undo much more!
Go forward and run git reset --hard HEAD~1
Once more, Git begins with the --soft stage, setting no matter HEAD is pointing to (essential), to HEAD~1 (“Commit 1”).
-> git reset --hard HEAD~1

To this point so good.
Subsequent, shifting on to the --mixed stage, matching the index with HEAD. That’s, Git undoes the staging of 2.txt.

It’s time for the --hard step, the place Git goes even additional and matches the working dir with the stage of the index. On this case, it means eradicating 2.txt additionally from the working dir.

(**Observe: on this particular case the file is untracked so it received’t be deleted from the file system, it isn’t actually essential in an effort to perceive git reset although).
So to introduce a change to Git, you might have three steps. You alter the working dir, the index or the staging space, and then you definitely commit a brand new snapshot with these modifications. To undo these modifications:
- If we use
git reset --soft, we undo the commit step. - If we use
git reset --mixed, we additionally undo the staging step. - If we use
git reset --hard, we undo the modifications to the working dir.
Actual-life situations!
State of affairs #1
So in a real-life state of affairs, write “I like Git” right into a file ( love.txt ), as all of us love Git 😍. Go forward, stage and commit this as properly:
-> echo "I like Git" > love.txt
-> git add love.txt
-> git commit -m "Commit 2.3"

Oh, oops!
Truly, I didn’t need you to commit it.
What I really wished you to do is, is write some extra love phrases on this file earlier than committing it.
What are you able to do?
Properly, one method to overcome this could be to make use of git reset --mixed HEAD~1, successfully undoing each the committing and the staging actions you took:
-> echo "I like Git" > love.txt
-> git add love.txt
-> git commit -m "Commit 2.3"
-> git reset --mixed HEAD~1

So essential factors to “Commit 1” once more, and love.txt is now not part of the index. Nonetheless, the file stays within the working dir. Now you can go forward and add extra content material to it:
-> git reset --mixed HEAD~1
-> echo And I like Transient Channel >> love.txt

Go forward, stage and commit your file:
-> git add love.txt
-> git commit -m "Commit 2.4"

Properly finished 👏🏻
You bought this clear, good historical past of “Commit 2.4” pointing to “Commit 1”.
We now have a brand new software in our toolbox, git reset 💪🏻

This software is tremendous, tremendous helpful, and you’ll accomplish nearly something with it. It’s not all the time probably the most handy software to make use of, nevertheless it’s able to fixing nearly any rewriting-history state of affairs when you use it rigorously.
For rookies, I like to recommend utilizing solely git reset for nearly any time you wish to undo in Git. As soon as you’re feeling snug with it, it’s time to maneuver on to different instruments.
State of affairs #2
Allow us to contemplate one other case.
Create a brand new file known as new.txt, stage and commit:
-> git add love.txt
-> git commit -m "Commit 2.4"
-> echo "A brand new file" > new.txt
-> git add new.txt
-> git commit -m "Commit 3"

Oops. Truly that’s a mistake. You had been on essential and I wished you to create this commit on a characteristic department. My dangerous 😇
There are two most essential instruments I would like you to take from this publish. The second is git reset. The primary and by way more essential one is to whiteboard the present state versus the state you wish to be in.
For this state of affairs, the present state and the specified state appear like so:

You’ll discover three modifications:
essentialfactors to “Commit 3” (the blue one) within the present state, however to “Commit 2.4” within the desired state.characteristicdepartment doesn’t exist within the present state, but it exists and factors to “Commit 3” within the desired state.HEADfactors toessentialwithin the present state, and tocharacteristicwithin the desired state.
In the event you can draw this and you know the way to make use of git reset, you may positively get your self out of this example.
So once more, a very powerful factor is to take a breath, and draw this out.
Observing the drawing above, how can we get from the present state to the specified one?
There are just a few alternative ways after all, however I’ll current one possibility just for every state of affairs. Be at liberty to mess around with different choices as properly.
You can begin by utilizing git reset --soft HEAD~1. This may set essential to level to the earlier commit, “Commit 2.4”:

Peeking on the current-vs-desired diagram once more, you may see that you simply want a brand new department, proper? You should utilize git swap -c characteristic for it, or git checkout -b characteristic (which does the identical factor):
-> git reset --soft HEAD~1
-> git swap -c characteristic
Switched to a brand new department 'characteristic'

This command additionally updates HEAD to level to the brand new department.
Because you used git reset --soft, you didn’t change the index, so it at present has precisely the state you wish to commit — how handy! You may merely decide to characteristic department:
-> git commit -m "Commit 3.1"

And you bought to the specified state 🎉
State of affairs #3
Prepared to use your data to further circumstances?
Add some modifications to love.txt, and likewise create new file known as cool.txt. Stage them and commit:
-> echo Some modifications >> love.txt
-> echo Git is cool > cool.txt
-> git add love.txt
-> git add cool.txt
-> git commit -m "Commit 4"

Oh, oops, really I wished you to create two separate commits, one with every change 🤦🏻
Need to do that one your self?
You may undo the committing and staging steps:
-> git reset --mixed HEAD~1

Following this command, the index now not contains these two modifications, however they’re each nonetheless in your file system. So now when you solely stage love.txt , you I can commit it individually, after which do the identical for cool.txt:
-> git add love.txt
-> git commit -m "Love"
-> git add cool.txt
-> git commit -m "Cool"

Good 😎
State of affairs #4
Create a brand new file (new_file.txt) with some textual content, and add some textual content to love.txt. Stage each modifications and commit them:
-> echo A brand new file > new_file.txt
-> echo Some extra textual content >> love.txt
-> git add .
-> git commit -m "Extra modifications"

Oops 🙈🙈
So this time I wished it to be on one other department, however not a new department, moderately an already-existing department.
So what are you able to do?
I’ll provide you with a touch. The reply is absolutely quick and very easy. What can we do first?
No, not reset. We draw. That’s the very first thing to do, as it might make every little thing else a lot simpler. So that is the present state:

And the specified state?

How do you get from the present state to the specified state, what could be best?
So a method could be to make use of git resetas you probably did earlier than, however there’s one other means that I want to you to strive.
First, transfer HEAD to level to present department:
-> git swap present

Inuitively, what you wish to do, is take the modifications launched within the blue commit, and apply these modifications (“copy-paste”) on high of present department. And Git has a software only for that.
To ask Git to take the modifications launched between this commit and its mother or father commit and simply apply these modifications on the lively department, you need to use git cherry-pick. This command takes the modifications launched within the specified revision, and apply them to the lively commit. It additionally creates a brand new commit object, and updates the lively department to level to this new object.
-> git cherry-pick b8d1a0

Within the instance above I specified the SHA-1 identifier of the created commit, however you could possibly additionally use git cherry-pick essential, because the commit whose modifications we’re making use of is the one essential is pointing to.
However we don’t need these modifications to exist on essential department. git cherry-pick solely utilized the modifications onto the present department. How will you take away them from essential?
A method could be to swap again to essential, after which use git reset --hard HEAD~1:

You probably did it! 💪🏻
Observe that git cherry-pick really computes the distinction between the required commit and its mother or father, after which applies them on the lively commit. Because of this typically, Git received’t be capable of apply these modifications as you could get a battle, however that’s a subject for an additional publish. Additionally be aware that you may ask Git to cherry-pick the modifications launched in any commit, not solely commits referenced by a department.
We now have acquired a brand new software, so now we have git reset in addition to git cherry-pick beneath our belt.

State of affairs #5
Okay, so one other day, one other repo, one other downside.
Create a commit:
-> echo That is extra tezt >> love.txt
-> git add love.txt
-> git commit -m "Extra modifications"

And push it to the distant server:
-> git push origin HEAD
Um, oops 😓…
I simply seen one thing. There’s a typo there. I wrote That is extra tezt as an alternative of That is extra textual content. Whoops. So what’s the large downside now? I pushed, which signifies that another person may need already pulled these modifications.
If I override these modifications by utilizing git reset, as we’ve finished thus far, we can have totally different histories and all hell may break unfastened. You may rewrite your individual copy of the repo as a lot as you want till you push it. When you push the change, you should be very sure nobody else has fetched these modifications if you’re going to rewrite historical past. Alternatively, you need to use one other software known as git revert. This command takes the commit you’re offering it with, compute the Diff from its mother or father commit, identical to git cherry-pick, however this time it computes the reverse modifications. So if within the specified commit you added a line, the reverse would delete the road, and vice versa.
-> git revert HEAD

git revert created a brand new commit object, which implies it’s an addition to the historical past. By utilizing git revert you didn’t rewrite historical past. You admitted your previous mistake, and this commit is an acknowledgement that you simply made had a mistake and now you fastened it. Some would say it’s the extra mature means. Some would say it’s not as clear a historical past you’ll get when you used git reset to rewrite the earlier commit. However it is a method to keep away from rewriting historical past.
Now you can repair the typo and commit once more:
-> echo That is extra textual content >> love.txt
-> git add love.txt
-> git commit -m "Extra modifications"

Your toolbox is now loaded with a brand new shiny software, revert:

State of affairs #6
Get some work finished, write some code, add it to love.txt . Stage this transformation, and commit it:
-> echo ...plenty of work... >> love.txt
-> git add love.txt
-> git commit -m "Commit 3"

I did the identical on my machine, and I used the Up arrow key on my keyboard to scroll again to earlier instructions, after which I hit Enter, and… Wow.
Whoops.
git reset --hard HEAD~1

Did I simply use git reset --hard? 😨
What really occurred? Git moved the pointer to HEAD~1, so the final commit, with all of my valuable work will not be reachable from the present historical past. Git additionally unstaged all of the modifications from the staging space, after which matched the working dir to the state of the staging space. That’s, every little thing matches this state the place my work is… gone.
Freak out time. Freaking out.
However, actually, is there a motive to freak out? Not realy… We’re relaxed folks. What can we do? Properly, intuitively, is the commit actually, actually gone? No. Why not? It nonetheless exists inside the inner database of Git. If I solely knew the place that’s, I might know the SHA-1 worth that identifies this commit, we may restore it. I may even undo the undoing, and reset again to this commit.
So the one factor I actually need right here is the SHA-1 of the “deleted” commit.
So the query is, how do I discover it? Would git log be helpful?
Properly, not likely. git log would go to HEAD, which factors to essential, which factors to the mother or father commit of the commit we’re searching for. Then, git log would hint again by way of the mother or father chain, that doesn’t embody the commit with my valuable work.

Fortunately, the very good individuals who created Git additionally created a backup plan for us, and that’s known as the reflog. When you work with Git, everytime you change HEAD, which you are able to do by utilizing git reset, but in addition different instructions like git swap or git checkout, Git provides an entry to the reflog.

We discovered our commit! It’s the one beginning with 0fb929e . We are able to additionally relate to it by its “nickname” — HEAD@{1}. So similar to Git makes use of HEAD~1 to get to the primary mother or father of HEAD, and HEAD~2 to discuss with the second mother or father of HEAD and so forth, Git makes use of HEAD@{1} to discuss with the primary reflog mother or father of HEAD, the place HEAD pointed to within the earlier step. We are able to additionally ask git rev-parse to indicate us its worth:
-> git reflog
-> git rev-parse HEAD
8b6da5273f...
-> git rev-parse HEAD@{1}
0fb929e8b2...

One other method to view the reflog is by utilizing git log -g, which asks git log to truly contemplate the reflog :

We see above that the reflog, simply as HEAD, factors to essential, which factors to “Commit 2”. However the mother or father of that entry within the reflog factors to “Commit 3”.
So to get again to “Commit 3”, you may simply use git reset --hard HEAD@{1} (or the SHA-1 worth of “Commit 3”):

And now if we git log:

We saved the day! 🎉👏🏻
What would occur if I used this command once more? And ran git commit --reset HEAD@{1}? Git would set HEAD to the place HEAD was pointing earlier than the final reset, which means to “Commit 2”. We are able to preserve going all day:
-> git reset --hard HEAD@{1}
HEAD is now at 0fb929e Commit 3
-> git reset --hard HEAD@{1}
HEAD is now at 8b6da42 Commit 2
-> git reset --hard HEAD@{1}
HEAD is now at 0fb929e Commit 3

our toolbox now, it’s loaded with instruments that may enable you resolve many circumstances the place issues go flawed in Git:

With these instruments, you now higher perceive how Git works. There are extra instruments that might can help you rewrite historical past particularly, git rebase), however you’ve already discovered lots on this publish. In future posts, I’ll dive into git rebase as properly.
Crucial software, much more essential than the 5 instruments listed on this toolbox, is to whiteboard the present state of affairs vs the specified one. Belief me on this, it’s going to make each state of affairs appear much less daunting and the answer extra clear.
Study extra about Git
I additionally gave a stay speak masking the contents of this publish. In the event you favor a video (or want to watch it alongside studying) — you will discover it right here.
Generally, my YouTube channel covers many facets of Git and its internals, you might be welcome to test it out (pun meant 😇)
Concerning the creator
Omer Rosenbaum is the creator of the Transient YouTube Channel. He’s additionally a cyber coaching professional and founding father of Checkpoint Safety Academy. He’s the creator of Product-Led Analysis, Gitting Issues Finished (in English) and Laptop Networks (in Hebrew).

















