Git tips: keep your branch clean and clear for a PR

When contributing to a open source project, we should keep commits clean and clear in the pull request that we create. In this case, rebase, reset are important git operations that we’d better get familiar with.

Get to know about git-reset and git-rebase

git-reset

git-reset works with refs, on your working directory and the index, without touching any commit objects (or other objects).

git-rebase

git-rebase on the other hand is used to rewrite previously made commit objects. So if you want to rewrite the history, git-rebase is what you want. Note that you should never rewrite history that was pushed and was available to someone else, as rebasing rewrites the objects making them incompatible with the old objects, resulting in a mess for anyone else involved.

Some use case examples

Once we’ve seen how basic operations work in git, there are a number of things that we can achieve with them.

Update a fork without pushing merge commit

1
2
3
4
5
6
7
8
# fetch
$ git fetch upstream
# rebase branch on top of upstream/master
$ git checkout myBranch git rebase upstream/master
# force a push to update
$ git push -f

Deal with local commits (unpushed)

Undo commit(s)

Undo last commit and abandon all the changes (never see them again)

1
$ git reset --hard HEAD~1

Undo last commit and keep all the changes (to be reedited later)

1
2
3
4
5
# leave the files alone
$ git reset HEAD~1
# leave the files and the index alone
$ git reset --soft HEAD~1

Deal with pushed commits (rewrite commit history)

ATTENTION, for all the operations below, don’t use them if someone has already pulled your change. Only for your personal repository. Else, we should use revert and create a relative commit for it.

1
2
3
4
5
6
# revert one single commit
$ git revert <commit_hash>
# revert a range of commits
# <oldest_commit_hash> is the oldest commit that we want to keep and won't be removed
$ git revert <oldest_commit_hash>..<latest_commit_hash>

Suppose we work on the branch myBranch and command git log gives:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
commit eb6e1791690c2498891071ecc944bb381b413732
Author: AAA <github.com@ccc.com>  Date: ...
Commit 6
commit 8c9e5dbe40fe4dfd83aa1425c6a71c9b271d33e7
Author: AAA <aaa@gmail.com>  Date: ...
Commit 5
commit 53b0b82e8754e5e74dda2dedd9f7da726b068df7
Author: BBB <bbb@gmail.com>  Date: ...
Commit 4
commit 2a7cda649ebea7997d5a6ac74c23569f2b0b83d9
Author: CCC <ccc@scummvm.org>  Date: ...
Commit 3
commit c788642b72ea524d720e36b28bf9e87e7b06fd28
Author: DDD <github.com@ddd.com>  Date: ...
Commit 2
commit b7638982c2a7b619494108224e956a95bec88f08
Author: EEE <github@bbb.n>  Date: ...
Commit 1

Squash several commits

Squash last 3 commits:

1
2
# rewrite history for last 3 commits
$ git rebase -i HEAD~3

Then, text editor will show you:

1
2
3
pick eb6e179 Commit 6
pick 8c9e5db Commit 5
pick 53b0b82 Commit 4

Change it to:

1
2
3
4
# squash commit 4 5 into commit 6
pick eb6e179 Commit 6
squash 8c9e5db Commit 5
squash 53b0b82 Commit 4

Save and quit. Done.

Squash all commits in a branch into one commit :

1
2
3
4
$ git checkout myBranch
$ git reset $(git merge-base master myBranch)
$ git add -A
$ git commit -m "one commit on myBranch"

Squash commits 2 4 6 into one single commit (non consecutive commits):

1
2
# rewrite history for last 5 commits(2 3 4 5 6)
$ git rebase -i HEAD~5

Then, text editor will show us:

1
2
3
4
5
pick eb6e179 Commit 6
pick 8c9e5db Commit 5
pick 53b0b82 Commit 4
pick 2a7cda6 Commit 3
pick c788642 Commit 2

Reorder commits to be squashed together:

1
2
3
4
5
pick 8c9e5db Commit 5
pick 2a7cda6 Commit 3
pick eb6e179 Commit 6
squash 53b0b82 Commit 4
squash c788642 Commit 2

Save and quit. Done

Remove commit(s)

Remove last commit(s) :

1
2
# Get back to the last known good commit(Commit 4 for example, so 5 6 are removed)
$ git push -f origin 53b0b82e8754e5e74dda2dedd9f7da726b068df7:myBranch

Remove commits 2 4 6 (use rebase) :

1
2
# rewrite history for last 5 commits(2 3 4 5 6)
$ git rebase -i HEAD~5

Then, text editor will show us:

1
2
3
4
5
pick eb6e179 Commit 6
pick 8c9e5db Commit 5
pick 53b0b82 Commit 4
pick 2a7cda6 Commit 3
pick c788642 Commit 2

Delete all the lines of unwanted commits:

1
2
pick 8c9e5db Commit 5
pick 2a7cda6 Commit 3

Save and quit. Done.

Remove commits 2 4 6 (use cherry-pick):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ git checkout myBranch
# checkout the last usable commit (Commit 1)
$ git checkout b7638982c2a7b619494108224e956a95bec88f08
# create a new branch to work on
$ git checkout -b repair
# cherry pick commits that we want to keep (Commmit 3 5)
$ git cherry-pick 2a7cda649ebea7997d5a6ac74c23569f2b0b83d9
$ git cherry-pick 8c9e5dbe40fe4dfd83aa1425c6a71c9b271d33e7
# back to myBranch
$ git checkout myBranch
# reset myBranch to last usable commit (Commit 1)
$ git reset --hard b7638982c2a7b619494108224e956a95bec88f08
# merge our repair branch onto myBranch
$ git merge repair
# push to update
$ git push --hard origin myBranch

 

Resources: