Reachability and undoing commits

Now let's imagine this scenario: we have a new commit on the berries branch, but we realized it is a wrong one, so we want the berries branch to go back where master is. We actually want to discard the last commit on the berries branch.

First, check out the berries branch:

[23] ~/grocery (master)
$ git checkout -
Switched to branch 'berries'

New trick: using the dash (-), you actually are saying to Git: "Move me to the branch I was before switching"; and Git obeys, moving us to the berries branch.

Now a new command, git reset (please don't care about the --hard option for now):

[24] ~/grocery (berries)
$ git reset --hard master
HEAD is now at 0e8b5cf Add an orange

In Git, this is simple as this. The git reset actually moves a branch from the current position to a new one; here we said Git to move the current berries branch to where master is, and the result is that now we have all the two branches pointing to the same commit:

[25] ~/grocery (berries)
$ git log --oneline --graph --decorate --all
* 0e8b5cf (HEAD -> berries, master) Add an orange
* e4a5e7b Add an apple
* a57d783 Add a banana to the shopping list

You can double-check this looking at refs files; this is the berries one:

[26] ~/grocery (berries)
$ cat .git/refs/heads/berries
0e8b5cf1c1b44110dd36dea5ce0ae29ce22ad4b8

And this is the master one:

[27] ~/grocery (berries)
$ cat .git/refs/heads/master
0e8b5cf1c1b44110dd36dea5ce0ae29ce22ad4b8

Same hash, same commit.

A side effect of this operation is losing the last commit we did in berries, as we already said: but why? And how?

This is due to the reachability of commits. A commit is not more reachable when no branches points to it directly, nor it figures as a parent of another commit in a branch. Our blackberry commit was the last commit on the berries branch, so moving the berries branch away from it, made it unreachable, and it disappears from our repository.

But are you sure it is gone? Want to make a bet?

Let's try another trick: we can use git reset to move the actual branch directly to a commit. And to make things more interesting, let's try to point the blackberry commit (if you scroll your shell window backwards, you can see its hash, which for me is ef6c382) so, git reset to the ef6c382 commit:

[28] ~/grocery (berries)
$ git reset --hard ef6c382
HEAD is now at ef6c382 Add a blackberry

And then do the usual git log:

[29] ~/grocery (berries)
$ git log --oneline --graph --decorate --all
* ef6c382 (HEAD -> berries) Add a blackberry
* 0e8b5cf (master) Add an orange
* e4a5e7b Add an apple
* a57d783 Add a banana to the shopping list

That's magic! We actually recovered the lost commit!

Okay, jokes aside, there's no magic in Git; it simply won't delete unreachable commits, at least not immediately. It makes some housekeeping automatically at a given time, as it has some powerful garbage collection features (look at the git gc command help page if you are curious; I want you to remember that any Git command, followed by the --help option, will open for you the internal man page for it).

So, we have seen what reachability of commits means, and then learnt how to undo a commit using the git reset command, that is a thing to know to take advantage of Git features while working on a repository.

But let's continue experimenting with branches.

Assume you want to add a watermelon to the shopping list, but later you realize you added it to the wrong berries branch; so, add "watermelon" to the shoppingList.txt file:

[30] ~/grocery (berries)
$ echo "watermelon" >> shoppingList.txt

Then do the commit:

[31] ~/grocery (berries)
$ git commit -am "Add a watermelon"
[berries a8c6219] Add a watermelon
 1 file changed, 1 insertion(+)

And do a git log to check the result:

[32] ~/grocery (berries)
$ git log --oneline --graph --decorate --all
* a8c6219 (HEAD -> berries) Add a watermelon
* ef6c382 Add a blackberry
* 0e8b5cf (master) Add an orange
* e4a5e7b Add an apple
* a57d783 Add a banana to the shopping list

Now our aim here is: have a new melons branch, which the watermelon commit have to belong to, then set the house in order and move the berries branch back to the blackberry commit. To keep the watermelon commit, first create a melon branch that points to it with the well-known git branch command:

[33] ~/grocery (berries)
$ git branch melons

Let's check:

[34] ~/grocery (berries)
$ git log --oneline --graph --decorate --all
* a8c6219 (HEAD -> berries, melons) Add a watermelon
* ef6c382 Add a blackberry
* 0e8b5cf (master) Add an orange
* e4a5e7b Add an apple
* a57d783 Add a banana to the shopping list

Okay, so now we have both berries and melons branches pointing to the watermelon commit.

Now we can move the berries branch back to the previous commit; let's get advantage of the opportunity to learn something new.

In Git, you often have the need to point to a preceding commit, like in this case, the one before; for this scope, we can use HEAD reference, followed by one of two different special characters, the tilde~ and the caret^. A caret basically means a back step, while two carets means two steps back, and so on. As you probably don't want to type dozens of carets, when you need to step back a lot, you can use tilde: similarly, ~1 means a back step, while ~25 means 25 steps back, and so on.

There's more to know about this mechanism, but it is enough for now; for all the details check http://www.paulboxley.com/blog/2011/06/git-caret-and-tilde.

So, let's step back our berries branch using caret; do a git reset --hard HEAD^:

[35] ~/grocery (berries)
$ git reset --hard HEAD^
HEAD is now at ef6c382 Add a blackberry

Let's see the result:

[36] ~/grocery (berries)
$ git log --oneline --graph --decorate --all
* a8c6219 (melons) Add a watermelon
* ef6c382 (HEAD -> berries) Add a blackberry
* 0e8b5cf (master) Add an orange
* e4a5e7b Add an apple
* a57d783 Add a banana to the shopping list

Well done! We successfully recovered the mistake, and learnt how to use the HEAD reference and git reset command to move branches from here to there.

Just to remark concepts, let's take a look at the shoppingList.txt file here in the berries branch:

[37] ~/grocery (berries)
$ cat shoppingList.txt
banana
apple
orange
blackberry

Okay, here we have blackberry, other than the other previously added fruits.

Switch to master and check again; check out the master branch:

[38] ~/grocery (berries)
$ git checkout master
Switched to branch 'master'

Then cat the file:

[39] ~/grocery (master)
$ cat shoppingList.txt
banana
apple
orange

Okay, no blackberry here, but only fruits added before the berries branch creation.

And now a last check on the melons branch; check out the branch:

[40] ~/grocery (master)
$ git checkout melons
Switched to branch 'melons'

And cat the shoppingList.txt file:

[41] ~/grocery (melons)
$ cat shoppingList.txt
banana
apple
orange
blackberry
watermelon

Fantastic! Here there is the watermelon, other than fruits previously added while in the berries and master branches.

Quick tip: while writing the branch name, use Tab to autocomplete: Git will write the complete branch name for you.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.30.162