GIT - HowTo's about "commits"

mail

How to hide / remove empty commits ?

Situation

What I did :
  1. create + checkout a feature branch myBranch
  2. make commit do thing1
  3. make commit do thing2
  4. realize I don't want to keep the changes brought by my do thing1 commit. But since but I don't want to entirely "destroy" it yet, I revert it, ending with history like :
    do thing1
    do thing2
    REVERT "do thing1"
  5. continue working and add some more do thing3, 4, ..., n commits
Now I'm happy with my code, there is no point in keeping the do thing1 + REVERT "do thing1" commits as they bring nothing interesting to the history. So I've rebased (fixup) REVERT "do thing1" into do thing1, which ends as an empty do thing1 commit.

So the question is now : How can I remove this commit which changes nothing ?

Details

This can be done with git filter-branch (sources : 1, 2) :
git filter-branch --commit-filter 'git_commit_non_empty_tree "$@"' HEAD
This rewrites the history and comes with the usual warnings :
  • you can safely apply it to your own unpublished work
  • but you never know whether the common part of the history has an empty commit. This would result in rewriting part of the common history (which is BAD1000 )

Solution

The safe solution is then to alter the "private" part of the history only, by running git filter-branch on the commits of myBranch only. This can be done by specifying a range of commits : with : as a result :
git filter-branch --commit-filter 'git_commit_non_empty_tree "$@"' $(git merge-base master myBranch)..HEAD
mail

How to revert a single file from a commit that changed several files ?

If the file you're interested in is the only file affected by the commit you want to revert, git revert can do the job seamlessly.
  1. list the commits affecting myFile :
    git log myFile
  2. find the ID of the commit you want to revert by checking the changes made by the listed commits :
    git show commitId
  3. once you know which commit changed myFile, you just have to go back in time right before that commit, which is written : commitId^ :
    git checkout commitId^ -- myFile
mail

How to change the author of a commit ?

This comes with the usual warning :

Only rewrite that part of history which you alone possess (source) : don't amend any commit you have already pushed (source) !

If you made a commit as the wrong user (or with a wrong email value), you can fix the latest commit with :

To avoid committing as root, you can add into /root/.bash_profile :

alias git='echo "Please do NOT use git as root"'

mail

How to change the date of a commit ?

We'll start with the usual Git warning :
You may not alter history, except if it's known by you only.

If the commit to alter is the latest commit :

newDate='Fri Mar 13 09:24:32 2020 +0100'; GIT_COMMITTER_DATE="$newDate" git commit --amend --no-edit --date "$newDate"

Otherwise : not the latest commit / need to alter several commits / both :

The whole operation relies on an interactive git rebase, as already used to :

In the example below, let's say we want to alter the 2 latest commits.

  1. let's see these commits :
    git log
    commit e6fcd18d063a6624b5fdefd27c6df646280c2788
    Author: Thomas ANDERSON <thomas.anderson@metacortex.com>
    Date:   Fri Mar 13 14:57:12 2020 +0100		we're going to change this ...
    
    	a very interesting commit message
    
    commit 4e9c917085e0de16db01ae1b5d793e1a9013d3f3
    Author: Thomas ANDERSON <thomas.anderson@metacortex.com>
    Date:   Fri Mar 13 11:24:32 2020 +0100		... and this
    
    	another great commit message
  2. select the commits to alter :
    git rebase -i HEAD~2
    This opens the git-rebase-todo in my text editor :
    pick 4e9c917 another great commit message
    pick e6fcd18 a very interesting commit message
  3. declare both commits to be edited by changing pick into e (edit). Save and close.
  4. Back to the shell, you get :
    Stopped at 4e9c917...	another great commit message
    You can amend the commit now, with
    
    	git commit --amend
    
    Once you are satisfied with your changes, run
    
    	git rebase --continue
  5. you can now build the new date —at the appropriate format— with :
    Fri Mar 13 16:51:45 2020 +0100
  6. then apply it :
    newDate='Fri Mar 13 16:51:45 2020 +0100'; GIT_COMMITTER_DATE="$newDate" git commit --amend --no-edit --date "$newDate"; git rebase --continue
    [detached HEAD 4d24a66] another great commit message		done editing the 1st commit
     Date: Fri Mar 13 16:51:45 2020 +0100
     
    Stopped at e6fcd18...  a very interesting commit message	starting to edit the 2nd commit
    You can amend the commit now, with
    
    	git commit --amend
    
    Once you are satisfied with your changes, run
    
    	git rebase --continue
  7. build a new (posterior !) date for the 2nd commit, and repeat :
    newDate='Fri Mar 13 16:54:45 2020 +0100'; GIT_COMMITTER_DATE="$newDate" git commit --amend --no-edit --date "$newDate"; git rebase --continue
    [detached HEAD 75e964f] a very interesting commit message
     Date: Fri Mar 13 16:54:45 2020 +0100
     3 files changed, 39 insertions(+)
    Successfully rebased and updated refs/heads/master.
  8. git log
    commit 75e964f48eba2abee70a3530c9cd515fe2fbe155
    Author: Thomas ANDERSON <thomas.anderson@metacortex.com>
    Date:   Fri Mar 13 16:54:45 2020 +0100		as expected 
    
    	a very interesting commit message
    
    commit 4d24a66bf88b1c8504b31b540e51c97106851e9b
    Author: Thomas ANDERSON <thomas.anderson@metacortex.com>
    Date:   Fri Mar 13 16:51:45 2020 +0100		as expected 
    
    	another great commit message
  9. Enjoy !!!
mail

How to split a commit ?

  1. make sure the environment is clean :
    • no uncommitted changes
    • the commit you're about to split belongs to you only (never rewrite public history, remember ?)
  2. find the commit you'd like to split and "how many commits back" it is :
    git log
    (let's consider it is 2 commits back)
  3. "rewind" to this commit :
    git rebase -i HEAD~2
    pick 17b92bc a great commit message		this is the commit I want to split
    pick d426342 one more wonderful piece of commit message
  4. state what you want to do with this commit by changing pick into edit :
    edit 17b92bc a great commit message
    pick d426342 one more wonderful piece of commit message
    Save and close your editor.
  5. Undo this commit :
    git reset HEAD^
    Unstaged changes after reset:
    M       roles/logrotate/defaults/main.yml
    M       roles/logrotate/tasks/main.yml
  6. Now, you just have to make 2 or more distinct commits normally
  7. Finish the interactive rebasing :
    git rebase --continue
    Successfully rebased and updated refs/heads/master.
mail

How to refer to parent and ancestor commits ?

HEAD
reference to the latest commit of the current branch
commitId^n
reference to the nth parent of commitId. Parents are ancestors of the same level, since they are from the same "generation". (source):
  • commitId^0 : the commit itself
  • commitId^1 (shorthand : commitId^) : father of commitId
  • commitId^2 : mother of commitId
commitId~n
reference to the nth ancestor of commitId (source):
  • commitId~0 : this is commitId itself
  • commitId~1 (shorthand : commitId~) : father of commitId
  • commitId~2 : grandfather of commitId
  • these are equivalent :
    • commitId~3
    • commitId^^^
    • commitId^1^1^1
commitId_oldest..commitId_newest
range of commits between commitId_oldest and commitId_newest (source)

Example :

Parent commits are ordered left-to-right.
	G   H   I   J		older commits
	 \ /     \ /
	  D   E   F
	   \  |  / \
	    \ | /   |
	     \|/    |
	      B     C
	       \   /
	        \ /
	         A		HEAD
commit parent ^ ancestor ~
A = A^0
B = A^ = A^1 = A~1
C = A^2
D = A^^ = A^1^1 = A~2
E = B^2 = A^^2
F = B^3 = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2 = B^^2 = A^^^2 = A~2^2
I = F^ = B^3^ = A^^3^
J = F^2 = B^3^2 = A^^3^2
TO BE CONFIRMED :
X^1 = X~1


X~~...~~ = X~n = the father of the father of ... of X	(father / grandfather / grand-grandfather / ...)
 ^^^^^^^         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
 n times                  n times


X^^...^^ = X~n
 ^^^^^^^
 n times


X^^2 must be understood (X^)^2

there's nothing you can "simplify" with : X^^...^^y, depends on the tree
mail

How to push only some local commits ?

Situation

My status : git status
On branch master
Your branch is ahead of 'origin/master' by 5 commits.
	(use "git push" to publish your local commits)
nothing to commit, working directory clean
My log : git log --oneline
2261c3a Revert blah blah blah ... This reverts commit 190898e77999bf4041fdc6d57ec18618743302e8
a102d0a FIX (ticket-104, part 2/2)		this commit
2fec5db FIX (ticket-104, part 1/2)		and this one too
ee00f4b Revert blah blah blah ... This reverts commit 7e16fa7173f10a662eecedbe166f42ffabacb41a
190898e ADD 'server_clone' for tests
7f2c929 FIX blah blah blah ...
8007cc6 CHANGE blah blah blah ...
3d835df FIX blah blah blah ...
5b047a8 FIX blah blah blah ...
082f84c CHANGE details
I'd like to push only the 2 marked commits to my origin server.

A wise (best practice !) solution would have been to create a dedicated branch to work on "ticket-104", but if I knew then, I wouldn't be on the verge of learning some git push hack .

Details

Take me to the solution !

Detailed solution (source) :

  1. Let's setup a test environment :
    before=$PWD; tmpDir=$(mktemp -d --tmpdir=/run/shm playingWithGit.XXXXXXXX); repoName='myRepo'; repoDir="$tmpDir/$repoName.git"; workDir="$tmpDir/workDir"; mkdir -p "$repoDir" "$workDir"; cd "$repoDir"; git init --bare; cd "$workDir"; git clone "file://$repoDir/"; cd "$workDir/$repoName"; for i in {1..5}; do codeFile="file$i"; echo "this is some code : $i" > "$codeFile"; git add "$codeFile"; git commit -m "This is commit #$i ($codeFile)"; done; git log --oneline
    Initialized empty Git repository in /dev/shm/playingWithGit.0teQbNFk/myRepo.git/
    Cloning into 'myRepo'...
    warning: You appear to have cloned an empty repository.
    [master (root-commit) 0e0fe97] This is commit #1 (file1)
     1 file changed, 1 insertion(+)
     create mode 100644 file1
    [master 8488863] This is commit #2 (file2)
     1 file changed, 1 insertion(+)
     create mode 100644 file2
    [master af6011d] This is commit #3 (file3)
     1 file changed, 1 insertion(+)
     create mode 100644 file3
    [master 1503eac] This is commit #4 (file4)
     1 file changed, 1 insertion(+)
     create mode 100644 file4
    [master 828be7d] This is commit #5 (file5)
     1 file changed, 1 insertion(+)
     create mode 100644 file5
    828be7d (HEAD ->  master) This is commit #5 (file5)
    1503eac This is commit #4 (file4)
    af6011d This is commit #3 (file3)
    8488863 This is commit #2 (file2)
    0e0fe97 This is commit #1 (file1)
  2. Let's push :
    git push origin master
  3. Now we have a local + a remote repository with some commits. Both are up-to-date :
    git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
  4. Now let's add some more commits :
    for i in {6..8}; do codeFile="file$i"; echo "this is some code : $i" > "$codeFile"; git add "$codeFile"; git commit -m "This is commit #$i ($codeFile)"; done; git log --oneline
    [master 725708b] This is commit #6 (file6)
     1 file changed, 1 insertion(+)
     create mode 100644 file6
    [master 0021c6d] This is commit #7 (file7)
     1 file changed, 1 insertion(+)
     create mode 100644 file7
    [master 58ce856] This is commit #8 (file8)
     1 file changed, 1 insertion(+)
     create mode 100644 file8
    58ce856 (HEAD ->  master) This is commit #8 (file8)
    0021c6d This is commit #7 (file7)
    725708b This is commit #6 (file6)
    828be7d (origin/master) This is commit #5 (file5)
    1503eac This is commit #4 (file4)
    af6011d This is commit #3 (file3)
    8488863 This is commit #2 (file2)
    0e0fe97 This is commit #1 (file1)
  5. Now I'll try to push, providing the ID of "commit #7" :
    git push origin 0021c6d:master && git log --oneline
    58ce856 (HEAD ->  master) This is commit #8 (file8)
    0021c6d (origin/master) This is commit #7 (file7)
    725708b This is commit #6 (file6)
    828be7d This is commit #5 (file5)
    1503eac This is commit #4 (file4)
    af6011d This is commit #3 (file3)
    8488863 This is commit #2 (file2)
    0e0fe97 This is commit #1 (file1)

    I've actually pushed the commits This is commit #6 (file6) and This is commit #7 (file7)

    git push remoteName commitId:remoteBranchName
    actually pushes the full history from the beginning of times until commitId included.

  6. Let's check this with more commits to push :
    for i in {9..13}; do codeFile="file$i"; echo "this is some code : $i" > "$codeFile"; git add "$codeFile"; git commit -m "This is commit #$i ($codeFile)"; done; git log --oneline
    07b6bb8 (HEAD ->  master) This is commit #13 (file13)
    65100fc This is commit #12 (file12)
    48e1df2 This is commit #11 (file11)
    a51d31f This is commit #10 (file10)
    4abcf38 This is commit #9 (file9)
    58ce856 This is commit #8 (file8)
    0021c6d (origin/master) This is commit #7 (file7)
    725708b This is commit #6 (file6)
    828be7d This is commit #5 (file5)
    1503eac This is commit #4 (file4)
    af6011d This is commit #3 (file3)
    8488863 This is commit #2 (file2)
    0e0fe97 This is commit #1 (file1)
  7. Now we'll push commits 11 and 9, in this order (!!!). To do so, we'll have to stack them on top of the latest commit known by "origin" : #7
    git squash 7
    • In the command above, 7 is the number of commits we'll considering while squashing. This appears to be a 7 again, like the "head" of the remote branch, but this is purely coincidental.
    • The display below is the re-ordered list of commits during git rebase. It's in the opposite order of git log.
    pick 0021c6d This is commit #7 (file7)
    pick 48e1df2 This is commit #11 (file11)
    pick 4abcf38 This is commit #9 (file9)
    pick 58ce856 This is commit #8 (file8)
    pick a51d31f This is commit #10 (file10)
    pick 65100fc This is commit #12 (file12)
    pick 07b6bb8 This is commit #13 (file13)
  8. Checking the re-ordering :
    git log --oneline -8
    02c9072 (HEAD ->  master) This is commit #13 (file13)
    8082015 This is commit #12 (file12)
    cf3cda0 This is commit #10 (file10)
    e794a70 This is commit #8 (file8)
    926ad50 This is commit #9 (file9)
    117bb7f This is commit #11 (file11)
    0021c6d (origin/master) This is commit #7 (file7)
    725708b This is commit #6 (file6)
  9. And also :
    git status
    On branch master
    Your branch is ahead of 'origin/master' by 6 commits.
    	(use "git push" to publish your local commits)
  10. Now, let's push the 2 commits sitting on top of "origin/master", by giving the commit ID of commit #9 :
    git push origin 926ad50:master
    Counting objects: 6, done.
    Delta compression using up to 2 threads.
    Compressing objects: 100% (4/4), done.
    Writing objects: 100% (6/6), 579 bytes | 579.00 KiB/s, done.
    Total 6 (delta 2), reused 0 (delta 0)
    To file:///run/shm/playingWithGit.0teQbNFk/myRepo.git/
    	0021c6d..926ad50	926ad50 -> master
  11. Checking locally :
    git status
    On branch master
    Your branch is ahead of 'origin/master' by 4 commits.
    	(use "git push" to publish your local commits)
    And :
    git log --oneline
    02c9072 (HEAD ->  master) This is commit #13 (file13)
    8082015 This is commit #12 (file12)
    cf3cda0 This is commit #10 (file10)
    e794a70 This is commit #8 (file8)
    926ad50 (origin/master) This is commit #9 (file9)
    117bb7f This is commit #11 (file11)
    0021c6d This is commit #7 (file7)
    725708b This is commit #6 (file6)
    828be7d This is commit #5 (file5)
    1503eac This is commit #4 (file4)
    af6011d This is commit #3 (file3)
    8488863 This is commit #2 (file2)
    0e0fe97 This is commit #1 (file1)
  12. Checking on the "origin" side :
    cd "$repoDir"; git log --oneline; cd "$workDir/$repoName"
    926ad50 (HEAD ->  master) This is commit #9 (file9)
    117bb7f This is commit #11 (file11)
    0021c6d This is commit #7 (file7)
    725708b This is commit #6 (file6)
    828be7d This is commit #5 (file5)
    1503eac This is commit #4 (file4)
    af6011d This is commit #3 (file3)
    8488863 This is commit #2 (file2)
    0e0fe97 This is commit #1 (file1)
  13. Done playing, let's clean up :
    cd "$before"; [ -d "$tmpDir" ] && rm -rf "$tmpDir"

Solution

  1. Find the remote (origin) HEAD. Several methods to do so :
    • git status may display things like : Your branch is ahead of 'origin/master' by n commits.
    • on the remote side : git log --oneline will display this (by default on recent Git versions)
  2. Re-order commits so that the commits to be pushed are sitting right on top of the latest commit known to the remote (i.e. "remote HEAD")
  3. Push the n local commits by giving the commit ID of the last commit (in current history order) to be pushed (source) :
    git push remoteName commitId:remoteBranchName
    Which gives :
    git push origin 720aebc:master
mail

How to squash several commits into a single one ?

Usage

NEVER EVER rebase commits which you have already pushed to a remote repository !

Example

Preliminary :

testFile='./myFile'; testDir='./myDir'
mkdir "$testDir"; cd "$testDir"
git init; touch "$testFile"; git add "$testFile"; git co "$testFile" -m "Initial version (empty)"; git log
for i in {1..4}; do echo "This is edit number $i" >> "$testFile"; git co "$testFile" -m "Commit #$i"; done; git log

Merge 4th commit (latest) into 3rd :

git rebase -i HEAD~2 outputs :
pick 897931d Commit #3
pick 25d3585 Commit #4
Edit to squash the 4th commit into the 3rd (forget about commit messages for the moment) :
pick 897931d Commit #3
s 25d3585 Commit #4
Save and exit. Then you'll be prompted for the final commit message :
# This is a combination of 2 commits.
# The first commit message is:

Commit #3

# This is the 2nd commit message:

Commit #4
Enter your own commit message :
Squashed #4 into #3
Save and exit. Check the result with git log :
Squashed #4 into #3
Commit #2
Commit #1
Initial version (empty)

Squash commits 2, 3 and 4 into a single one :

Make sure you've run the cleaning then preliminary steps before going further.

git rebase -i HEAD~3 outputs :
pick 04f56e2 Commit #2
pick 0887de3 Commit #3
pick 5410e2c Commit #4
Edit to fixup the commits 2, 3 and 4 and use the message of the 2nd commit (this method won't prompt for a commit message):
pick 04f56e2 Commit #2
f 0887de3 Commit #3
f 5410e2c Commit #4
Save and exit. Check with git log :
Commit #2
Commit #1
Initial version (empty)
See changes made with this "new commit number 2" (that is also the latest so far) : git show HEAD~1..HEAD $testFile :
	Commit #2

diff --git a/myFile b/myFile
index 32671c8..4e96082 100644
--- a/myFile
+++ b/myFile
@@ -1 +1,4 @@
 This is edit number 1
+This is edit number 2
+This is edit number 3
+This is edit number 4

Merge commits 2 and 3 into a single one :

Make sure you've run the cleaning then preliminary steps before going further.

git log shows :
commit 402957e89a359ce2bd4c9c24cd658dbe25a4f155
	Commit #4

commit 331174ab513dda7861100997ae6e4dbf66d18eae
	Commit #3

commit de0db9b72fad2e52b23cbfdd51f8714561d9e0e6
	Commit #2

commit 8c93f6861a17388d691cfb5a985bbb9161c4a8f1
	Commit #1

commit 8ee9ad23f95c146d48c7c7a7f58d03a3140e5455
	Initial version (empty)
We're at Commit #4 (HEAD) and we want to make changes right after Commit #1, no matter where these changes stop. In other words, we want to start working from Commit #1 (excluded), which can be refereed to as HEAD~3 (details)
git rebase -i HEAD~3
pick de0db9b Commit #2
pick 331174a Commit #3
pick 402957e Commit #4

# These lines can be re-ordered; they are executed from top to bottom.
Edit (this is where the magic takes place ) Forget about commit message for the moment :
pick de0db9b Commit #2
s 331174a Commit #3
pick 402957e Commit #4
Save and exit. You'll be prompted for a new commit message :
# This is a combination of 2 commits.
# The first commit message is:

Commit #2

# This is the 2nd commit message:

Commit #3
Change this into :
Squashed #3 into #2
And check the result : git log :
commit ff477d71a329265bf2033d0d0ccd5a1c7e60c37c
	Commit #4

commit 394bc1c28075e6457a795bd69d58deda20f00a20
	Squashed #3 into #2

commit 8c93f6861a17388d691cfb5a985bbb9161c4a8f1
	Commit #1

commit 8ee9ad23f95c146d48c7c7a7f58d03a3140e5455
	Initial version (empty)
See changes made with this "new commit" : git show HEAD~2..HEAD~1 $testFile :
commit 394bc1c28075e6457a795bd69d58deda20f00a20
	Squashed #3 into #2

diff --git a/myFile b/myFile
index 32671c8..987d059 100644
--- a/myFile
+++ b/myFile
@@ -1 +1,3 @@
 This is edit number 1
+This is edit number 2
+This is edit number 3

Cleaning :

rm -rf .git; cd ..; [ -d "$testDir" ] && rm -r "$testDir"; unset testFile testDir
mail

How to view details of past commits ?

General rule

the diff only
git diff olderCommit..newerCommit
the diff + commit ID + author + commit message

To highlight the changes made between commits, use --word-diff (works with diff / show / log) :

git diff olderCommit..newerCommit --word-diff

Changes made to myFile in the latest n commits

git show HEAD~n..HEAD myFile
mail

How to alter old commits ?

To do so, you may :

Only rewrite that part of history which you alone possess (source) : don't alter a commit you have already pushed (source) !

Situation

First things first, here's my history : git log
commit 7ef72d73aa0ee4c67007e66a3351ed1ee2df6998
Author: Thomas ANDERSON <thomas.anderson@metacortex.com>
Date:	Fri Apr 24 15:31:11 2015 +0200

	commit message #3

commit 2b9bbbf3f7463e0c0526a69edec10a708dacada3
Author: root <root@localhost>				OOOPS ! 
Date:	Fri Apr 24 15:21:21 2015 +0200

	commit message #2

commit f93811c3dcaabed86df54916ea84fbbfcf521435
Author: Thomas ANDERSON <thomas.anderson@metacortex.com>
Date:	Fri Apr 24 11:44:25 2015 +0200

	commit message #1
The error we'll fix here is this commit made as root.

Solution

  1. Put away pending changes (if any) : git stash
  2. Get the current branch name : git branch
    * master
  3. Checkout the bad commit : git checkout 2b9bbbf3f7463e0c0526a69edec10a708dacada3, which outputs :
    You are in 'detached HEAD' state. You can look around, make experimental
    changes and commit them, and you can discard any commits you make in this
    state without impacting any branches by performing another checkout.
    
    ...
    
    HEAD is now at 2b9bbbf... commit message #2
  4. Make the necessary changes :
    • In this example, since I just committed with the wrong user, there's nothing to do, the fix will be made at the next step.
    • If the change I'd like to commit (1 single change in 1 single file) is currently stashed, I have to extract it first (source) : git checkout stash@{0} -- path/to/file
      path/to/file is what is reported by git s.
  5. Commit changes :
    • In the specific case of this example (changing the author of a commit) : git commit --amend --reset-author
    • In the general case : git commit --amend
  6. Now let's rebase with : git rebase --onto HEAD badCommitId branchName
    git rebase --onto HEAD 2b9bbbf3f7463e0c0526a69edec10a708dacada3 master
    First, rewinding head to replay your work on top of it...
    Applying: commit message #3
  7. Restore pending changes (if any) : git stash apply
mail

How to list files modified by a commit ?

mail

How to view git log of a renamed file ?

Situation

git log myFile returns no error, but I can remember this file was initially named my file. I have renamed + committed it, but now, how can I view its history when it was still named my file ?

Details

Solution

git log -S my file

will show the history of my file starting at the rename commit, back to the beginning of times.
mail

How to cancel a commit ?

Table of contents

If you included —by mistake— some changes into a commit they don't belong to, what you need is to split that commit.

Cancel the latest commit :

Available methods :

git revert commitId
create a new commit that "unchanges" what was changed by commit commitId (example). This can be used on any commit, not only the latest one.
git reset HEAD^
remove a commit from history and leave changes made by this commit as unstaged changes (example)

Examples :

git revert commitId :

tempDir=$(mktemp -d --tmpdir XXXXXXXX); commitMessage='This is commit '; cd "$tempDir"; git init; echo 'hello world' > myFile; git add myFile; git co -m 'Initial version'; for i in {1..2}; do echo "This line will be committed by commit #$i" >> myFile; git add myFile; git co -m "$commitMessage#$i"; done; git log --oneline; idOfLatestCommit=$(git log --oneline | awk "/$commitMessage#$i/ {print \$1}"); echo -e "\nID of latest commit : '$idOfLatestCommit'\nREVERTING...\n"; git revert --no-edit "$idOfLatestCommit"; git log --oneline; echo; git s; echo; cat myFile; cd -; [ -d "$tempDir" ] && rm -rf "$tempDir"

Outputs :

aa94778 This is commit #2
5915e56 This is commit #1
9baee48 Initial version

ID of latest commit : 'aa94778'
REVERTING...

[master 5ba510a] Revert "This is commit #2"
 1 file changed, 1 deletion(-)
5ba510a Revert "This is commit #2"
aa94778 This is commit #2
5915e56 This is commit #1
9baee48 Initial version

On branch master
nothing to commit (use -u to show untracked files)	nothing unstaged

hello world
This line will be committed by commit #1		changes are gone : working copy altered
git reset HEAD^ :

tempDir=$(mktemp -d --tmpdir XXXXXXXX); cd "$tempDir"; git init; echo 'hello world' > myFile; git add myFile; git co -m 'Initial version'; for i in {1..2}; do echo "This line will be committed by commit #$i" >> myFile; git add myFile; git co -m "This is commit #$i"; done; git log --oneline; echo -e '\nRESETTING...\n'; git reset HEAD^; git log --oneline; git s; cat myFile; cd -; [ -d "$tempDir" ] && rm -rf "$tempDir"

Outputs :

4caee4f This is commit #2
b78d9b4 This is commit #1
e4dd720 Initial version

RESETTING...

Unstaged changes after reset:
M	myFile
b78d9b4 This is commit #1
e4dd720 Initial version
On branch master
Changes not staged for commit:
	(use "git add <file>..." to update what will be committed)
	(use "git checkout -- <file>..." to discard changes in working directory)

 modified:	myFile

no changes added to commit (use "git add" and/or "git commit -a")
hello world
This line will be committed by commit #1
This line will be committed by commit #2	changes are still there : working copy unaltered
  • The commit This is commit #2 no longer exists
  • myFile is back to its state right before doing the This is commit #2 commit, i.e. with some unstaged changes. This is confirmed by git s as well as by the final cat.

Cancel another commit (not the latest)

In this section, we'll experiment with git reset and git rebase. Before doing so, we need some data to play with, which is created with :
tempDir=$(mktemp -d --tmpdir XXXXXXXX); cd "$tempDir"; git init; echo 'hello world' > myFile; git add myFile; git co -m 'Initial version'; for i in {1..5}; do echo "This line will be committed by commit #$i" >> myFile; git add myFile; git co -m "This is commit #$i"; done; git log --oneline
378563c (HEAD -> master) This is commit #5
0d40646 This is commit #4
45a0af4 This is commit #3
3cfb370 This is commit #2
afa7d15 This is commit #1
f280ab7 Initial version
We have created a repository with 5 commits, and we'll remove the commit #3.

With git reset :

This method does not work in real-life situations and you may lose some data. I mention it just as :
  • an example of git reset usage
  • a reminder of why I shouldn't use it (I've been back to this article several times wondering I should try with git reset ! )
  1. let's jump some commits back and see what happens
    git reset HEAD~3
    Unstaged changes after reset:
    M       myFile
    git log --oneline
    3cfb370 (HEAD -> master) This is commit #2
    afa7d15 This is commit #1
    f280ab7 Initial version
    Jumping 3 commits back took us to the commit #2. The commits #3, #4 and #5 don't exist anymore.
  2. Let's see these unstaged changes :
    git diff
    
     hello world
     This line will be committed by commit #1
     This line will be committed by commit #2
    +This line will be committed by commit #3
    +This line will be committed by commit #4
    +This line will be committed by commit #5
    The changes made by the commits #3, #4 and #5 are still there. The current situation is like :
    • I've just made commit #2
    • then I added the lines 3, 4 and 5 into myFile
    • then run git add myFile
  3. Now, I can :
    1. remove the line 3 from myFile
    2. re-commit lines 4 and 5 manually (as distinct commits or with a single one)
This method works here because we're in a trivial situation. It implies re-doing things manually, which is long and error-prone.

With git rebase :

If you've tried the example above, don't forget to delete + re-build the test repository. We still have 5 commits, with commit #3 being the faulty one.
  1. let's rebase interactively :
    git rebase -i HEAD~3
    Git opens your default editor and displays :
    pick 45a0af4 This is commit #3
    pick 0d40646 This is commit #4
    pick 378563c This is commit #5
    and a complete help message on what's going on.
  2. since we want to remove the commit #3, let's "forget" it by deleting the corresponding line + save + exit.
    Auto-merging myFile
    CONFLICT (content): Merge conflict in myFile
    error: could not apply 0d40646... This is commit #4				the problem
    Resolve all conflicts manually, mark them as resolved with			the solution 
    git add/rm conflicted_files, then run git rebase --continue.
    You can instead skip this commit: run "git rebase --skip".
    To abort and get back to the state before "git rebase", run "git rebase --abort".
    Could not apply 0d40646... This is commit #4
    this was expected
  3. while we are removing the commit #3, the changes made by this commit are still there. Here is how the history looks like for Git :
    • myFile has lines 1, 2, 3, 4 and 5
    • all lines were added + committed separately from others
    • rebase affects the commits #3, #4 and #5. The commits #1 and #2 are out-of-scope here, which means Git has to rewrite its history starting after commit #2.
    • after commit #2, myFile had lines 1 and 2. We added the line 3 and made the commit #3, then added the line 4 and made the commit #4, ...
    • the line 3 is still there (as unstaged change). Git rewrites history, skipping the commit #3. The commit #4 added 1 line, and we now have more added content than expected :
      git diff
      
        hello world
        This line will be committed by commit #1
        This line will be committed by commit #2
      ++<<<<<<< HEAD					delete this line to resolve the conflict
      ++=======					delete this line to resolve the conflict
      + This line will be committed by commit #3	delete this line to resolve the conflict
      + This line will be committed by commit #4
      ++>>>>>>> 0d40646... This is commit #4		delete this line to resolve the conflict
  4. to fix this conflict, let's edit myFile and delete the lines shown above
  5. then as told by Git :
    git add myFile
  6. and finally :
    git rebase --continue
    [detached HEAD 7550077] This is commit #4
     1 file changed, 1 insertion(+)
    Successfully rebased and updated refs/heads/master.
  7. git log --oneline
    d9a6cc4 (HEAD -> master) This is commit #5
    7550077 This is commit #4
    3cfb370 This is commit #2
    afa7d15 This is commit #1
    f280ab7 Initial version
    No commit #3 anymore. The IDs of commits #4 and #5 have changed since that part of the history has been re-written.