Monday, July 04, 2011

Git: Undoing

Introduction

Git is a distributed version control system.  Each git working directory has a local copy of repository.  A common Git workflow includes the following:

  1. Make changes in working directory
  2. Add changes to staging area
  3. Commit to local repository
  4. Push to origin repository on remote

In real practice, we may regret of what we have done at any stages above.  We may want to revert or undo our works.

Revert untracked files

Untracked files are simply new files that have not added to staging area nor committed before.  The most easiest way to remove untracked file is delete it from working directory.  However, if there are bunch of untracked files exist in working directory, you may remove them using “git clean” command:

$ echo "This is new file" > newfile 
... // do some work and revert the work fianlly 
$ git status –s 
?? newfile 
$ git clean -f 
Removing newfile 
$ git status 
# On branch master 
nothing to commit (working directory clean) 
$ ls -a 
.  ..  .git  install  newfile  readme

Revert modified tracked files

Assume there are 2 files in a clean git repository:

$ ls -a 
.  ..  .git  install  readme 
$ git status 
# On branch master 
nothing to commit (working directory clean)

Make some changes to the working tree:

$ echo "modified" >> install 
$ echo "modified" >> readme

And the working tree is now become

$ ls -a 
.  ..  .git  install  newfile  readme 
$ git status –s 
M install 
M readme

Both readme and install file has modified status.

Revert a single file

To revert only a readme file, run

$ cat readme 
this is a readme 
modified 
$ git checkout readme 
$ cat readme 
this is a readme 
$ git status –s 
M install 
?? newfile

Revert a working directory

To revert whole working directory, run

$ git status –s 
M install 
M readme 
?? newfile 
$ git reset --hard 
HEAD is now at 5543e79 second commit 
$ git status –s 
?? newfile

Un-staging files

By using “git add” on new or modified files will move them into staging area for commit in later stage.  To un-stage files, use “git reset HEAD <file>...":

Add 3 new files and make changes to readme file:

$ ls -a 
.  ..  .git  install  readme 
$ touch newfile1 newfile2 newfile3 
$ ls -a 
.  ..  .git  install  newfile1  newfile2  newfile3  readme 
$ cat readme 
this is a readme 
$ echo "modified" >> readme 
$ git status -s 
M readme 
?? newfile1 
?? newfile2 
?? newfile3

Add files to staging area:

$ git add * 
$ git status 
# On branch master 
# Changes to be committed: 
#   (use "git reset HEAD <file>..." to unstage) 
# 
#       new file:   newfile1 
#       new file:   newfile2 
#       new file:   newfile3 
#       modified:   readme 
#

Un-stage files:

$ git reset HEAD 
Unstaged changes after reset: 
M       readme 
$ git status 
# On branch master 
# Changed but not updated: 
#   (use "git add <file>..." to update what will be committed) 
#   (use "git checkout -- <file>..." to discard changes in working directory) 
# 
#       modified:   readme 
# 
# Untracked files: 
#   (use "git add <file>..." to include in what will be committed) 
# 
#       newfile1 
#       newfile2 
#       newfile3 
no changes added to commit (use "git add" and/or "git commit -a")

Run this to un-stage only a single file, newfile3:

$ git reset HEAD newfile3

Revert committed changes in working directory

Assume 3 commits has been done on a working directory:

$ ls -a 
.  ..  .git  install  readme 
$ git status 
# On branch master 
nothing to commit (working directory clean) 
$ echo "commit 1" >> readme 
$ git commit -a -m "commit 1" 
[master fdaa7d3] commit 1 
1 files changed, 1 insertions(+), 0 deletions(-) 
$ echo "commit 2" >> readme 
$ git commit -a -m "commit 2" 
[master 7ad2d0a] commit 2 
1 files changed, 1 insertions(+), 0 deletions(-) 
$ echo "commit 3" >> readme 
$ git commit -a -m "commit 3" 
[master 8a56cd9] commit 3 
1 files changed, 1 insertions(+), 0 deletions(-) 
$ git log --oneline 
8a56cd9 commit 3 
7ad2d0a commit 2 
fdaa7d3 commit 1 
5543e79 second commit 
f9d7ae7 this is first commit

Revert to last commit

$ git reset --hard HEAD^ 
HEAD is now at 7ad2d0a commit 2 
$ cat readme 
this is a readme 
commit 1 
commit 2 
$ git log --oneline 
7ad2d0a commit 2 
fdaa7d3 commit 1 
5543e79 second commit 
f9d7ae7 this is first commit

Revert to specific commit

We may revert to specific commit by specify sha1 hash of the commit in “git reset

$ git log --oneline 
07187ef commit 3 
ca79a77 commit 2 
37194a2 commit 1 
5543e79 second commit 
f9d7ae7 this is first commit 
$ git reset --hard 37194a2 
HEAD is now at 37194a2 commit 1 
$ git log --oneline 
37194a2 commit 1 
5543e79 second commit 
f9d7ae7 this is first commit

Revert to last few commit

Using “HEAD~n” with “git reset” allow us to revert to last n committed:

$ git log --oneline 
fdbde67 commit 3 
54d5cef commit 2 
37194a2 commit 1 
5543e79 second commit 
f9d7ae7 this is first commit 
$ git reset --hard HEAD~3 
HEAD is now at 37194a2 commit 1 
$ git log --oneline 
5543e79 second commit 
f9d7ae7 this is first commit

You may also use something like “HEAD^” to revert a file to last commit:

$ git reset –hard HEAD^

Add more caret (^) symbol to indicate revert for last few commits.  One caret represent one backward commit.  For example, to revert to last 5 commits:

$ git reset –hard HEAD^^^^^

Revert committed pushed to remote

It is not encourage to revert pushed commit to origin repository if other has pulled what you have pushed.  However, human makes mistake.  An unwanted push may be reverted too if you are aware that nobody has pulled before.

There are few ways to revert pushed commit.  This may be the most simple example:

$ git push 
Everything up-to-date 
$ git log --oneline 
f6abcfa commit 3 
0660af9 commit 2 
37194a2 commit 1 
5543e79 second commit 
f9d7ae7 this is first commit 
$ git reset --hard HEAD^^^ 
HEAD is now at 5543e79 second commit 
$ git push origin +master 
Total 0 (delta 0), reused 0 (delta 0) 
To /tmp/test.git 
+ f6abcfa...5543e79 master -> master (forced update) 
$ git pull 
Already up-to-date. 
$ git push 
Everything up-to-date 
$ git log --oneline 
5543e79 second commit 
f9d7ae7 this is first commit

You may encounter error while revert pushed commit:

$ git push origin +master
Total 0 (delta 0), reused 0 (delta 0)
remote: error: denying non-fast-forward refs/heads/master (you should pull first)
! [remote rejected] e3fce6 -> master (non-fast-forward)

This is due to the origin repository has disable non fast forward (revert) commit.  Edit .git/config in origin repository to allow non fast forward commit:

$ cat .git/config

[receive]
        denyNonFastforwards = true false

Revert again and it should work:

$ git push origin +master

Prune loose committed objects

Even if committed works has been undone, it still exists in the repository as loose object.  Run reflog command shows loose objects:

# git reflog
704b63a HEAD@{0}: checkout: moving from 05fec4a9b062d7d9883969283a3ba18e5e06aad0
05fec4a HEAD@{1}: HEAD^ --: updating HEAD
9fa2a61 HEAD@{2}: checkout: moving from 806a6fd6ae4198a20af0f301d27c13d0dd052931
806a6fd HEAD@{3}: checkout: moving from master to 806a6fd6ae4198a20af0f301d27c13
704b63a HEAD@{4}: 704b63ab353cf32b5d6a9a54b8eb2d4d52b824e8: updating HEAD
9fa2a61 HEAD@{5}: merge origin/master: Merge made by recursive.
05fec4a HEAD@{6}: HEAD^ --: updating HEAD
5f3b0a1 HEAD@{7}: rebase finished: returning to refs/heads/master
5f3b0a1 HEAD@{8}: checkout: moving from master to 5f3b0a1e2167652b32e765b9431727
704b63a HEAD@{9}: checkout: moving from test to master
5f3b0a1 HEAD@{10}: checkout: moving from 806a6fd6ae4198a20af0f301d27c13d0dd05293
806a6fd HEAD@{11}: checkout: moving from master to 806a6fd6ae4198a20af0f301d27c1
704b63a HEAD@{12}: merge 704b63ab353cf32b5d6a9a54b8eb2d4d52b824e8: Fast-forward
806a6fd HEAD@{13}: checkout: moving from 704b63ab353cf32b5d6a9a54b8eb2d4d52b824e
704b63a HEAD@{14}: checkout: moving from master to 704b63ab353cf32b5d6a9a54b8eb2
806a6fd HEAD@{15}: 806a6fd6ae4198a20af0f301d27c13d0dd052931: updating HEAD
5f3b0a1 HEAD@{16}: 5f3b0a1e2167652b32e765b94317279868faa82b: updating HEAD
4441dc5 HEAD@{17}: HEAD^ --: updating HEAD
05fec4a HEAD@{18}: HEAD^ --: updating HEAD
5f3b0a1 HEAD@{19}: checkout: moving from 5f3b0a1e2167652b32e765b94317279868faa82
5f3b0a1 HEAD@{20}: checkout: moving from master to 5f3b0a1e2167652b32e765b943172
5f3b0a1 HEAD@{21}: checkout: moving from 5f3b0a1e2167652b32e765b94317279868faa82
5f3b0a1 HEAD@{22}: checkout: moving from master to 5f3b0a1e2167652b32e765b943172
5f3b0a1 HEAD@{23}: checkout: moving from 704b63ab353cf32b5d6a9a54b8eb2d4d52b824e
704b63a HEAD@{24}: checkout: moving from master to 704b63ab353cf32b5d6a9a54b8eb2
5f3b0a1 HEAD@{25}: merge 5f3b0a1e2167652b32e765b94317279868faa82b: Fast-forward
806a6fd HEAD@{26}: merge origin/master: Fast-forward

These objects may expire and will be pruned by “git gc” after 30 days (default).  It has no harm exist in local repository except occupy some hard drive space.

It is possible to prune loose objects immediately by executing

# git reflog expire --expire=now --all
# git gc
Counting objects: 35395, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (7225/7225), done.
Writing objects: 100% (35395/35395), done.
Total 35395 (delta 26533), reused 35393 (delta 26533)
# git reflog
#

No comments: