Behind the Scenes at Jungle Disk - Git Rebasing and Clean Pull Requests
Today, I would like to share some best practices for using Git rebasing and pull requests.
Development vs Pull Request Commits
When developing, the mantra of commit early and commit often may lead to an explosion at the patch factory. Prior to sending those patches upstream or opening a pull request (PR), the working branch should be cleaned up such that:
- Every patch is atomic
- Every commit message is properly formatted
- No extraneous commits (like merge commits)
- Based off as close to upstream
HEAD
as possible
Every Patch is Atomic
Each patch in a repository should be able to stand on its own. That is each
patch should not depend on the patches after it to produce a workable copy.
This will allow the use of tools like git bisect
to track down where a
bug was introduced. If there are a set of patches A
, B
, C
where patch B
requires patch C
in order to function properly, B
and C
should be
merged into a single patch.
Every Commit Message is Properly Formatted
Every commit message should be formatted in such a way to make it easy to
read in a variety of situations. For example a commit message that is
one long line makes it very hard to read via git log --oneline --graph
.
Generally following the guidelines of the kernel and git
itself leads
to very readable and usable commit messages:
area: short summary
Long summary that describes the change in detail. This area should wrap
at 72 characters in order to allow space for the standard indention
from `git log` such that it doesn't wrap over 80 characters.
Fixes: #53
Area should be the general area that or realm of the project that the patch
modifies. The area and short summary are separated by a :
and the first line
of the patch should not exceed 50 characters. (This allows plenty of space for
--oneline
formatting.) The rest of the commit message wraps at
72 characters. Following the long summary, any additional tags can be
added such as Fixes
, Signed-Off-By
, et al.
No Extraneous Commits
With some exceptions merge commits should never appear in a branch that is
intended to send a pull request for. Merge commits are useful when two
long running branches are merged together. Such as if there is a development
branch and a master
branch. The merge commit is the record of the true up
between the branches.
A---B---C development
/
D---E---F---G master
A---B---C development
/ \
D---E---F---G---H master
source: GIT-MERGE(1) man page
Generally when working on a topic branch, the branch will be forgotten or
removed after it is merged upstream, so there is no need to track the true up
between it and upstream. Prior to submitting a pull request upstream, any
merge commits introduced during development should be removed. By following a
rebase
procedure instead of merge
, merge commits can avoid them altogether.
Based Off as Close to Upstream HEAD
as Possible
Over time the branch that will receive the pull request to and the working
topic branch will diverge. Prior to submitting the pull request, all
conflicts should be resolved with the current upstream HEAD
.
Rebasing to Squash and Fix Up
The easiest way to clean up a branch and get it ready for submission is
a rebase (usually interactive). For this example assume that there is a
remote origin
with a branch master
that we will submit out pull request
to:
$ git fetch origin
$ git rebase -i origin/master
git
will open $EDITOR
with the file .git/rebase-merge/git-rebase-todo
.
This file controls the rebase
operation and allows fixing up the branch. Each
commit listed will be cherry-picked
on top of the HEAD
specified and the
current branch HEAD
will be rewritten to point to resulting last commit.
pick ddd461a a: commit a
pick 2107986 b: commit b
pick 1165f85 c: commit c
# Rebase ec9457b..1165f85 onto ec9457b (3 command(s))
#
# Commands:
+ p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
In the above rebase we see that there are three commits that are staged to be
cherry-picked
. If commit b
requires c
to be an atomic patch, then c
should be squashed into b
.
pick ddd461a a: commit a
pick 2107986 b: commit b
s 1165f85 c: commit c
Once the file is saved and exited the rebase
operation will begin. Since the
instruction to squash c
into b
might require updating the commit message,
the rebase
will halt temporarily and open $EDITOR
with the contents of
both commit messages to allow for message merging by the user.
# This is a combination of 2 commits.
# The first commit's message is:
b: commit b
# This is the 2nd commit message:
c: commit c
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Thu Nov 17 19:37:13 2016 +0000
#
# interactive rebase in progress; onto ec9457b
# Last commands done (3 commands done):
# pick 2107986 b: commit b
# s 1165f85 c: commit c
# No commands remaining.
# You are currently editing a commit while rebasing branch 'topic' on 'ec9457b'.
#
# Changes to be committed:
# new file: three
#
Once the message is satisfactory factory, saving and quitting the editor will continue the
rebase
operation.
# This is a combination of 2 commits.
# The first commit's message is:
b: commit b and commit c
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Thu Nov 17 19:37:13 2016 +0000
#
# interactive rebase in progress; onto ec9457b
# Last commands done (3 commands done):
# pick 2107986 b: commit b
# s 1165f85 c: commit c
# No commands remaining.
# You are currently editing a commit while rebasing branch 'topic' on 'ec9457b'.
#
# Changes to be committed:
# new file: three
#
If there are conflicts during a cherry-pick
operation, rebase
will halt
allowing the user to fix the commit as needed before continuing:
error: could not apply 46f66fc... a: commit a
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Could not apply 46f66fcb277282c84ed3e606bbb06140d8607e80... a: commit a
To begin resolving the conflict, first run git status
to see what the issue is:
$ git status
interactive rebase in progress; onto 01562b4
Last command done (1 command done):
pick 46f66fc a: commit a
Next command to do (1 remaining command):
pick d7b7fb1 b': commit b and commit c
(use "git rebase --edit-todo" to view and edit)
You are currently rebasing branch 'topic' on '01562b4'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: two
Unmerged paths:
(use "git reset HEAD <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: one
Here both master
and our topic branch modified the same line in the file
one
. Open the file and search for <<<<
:
one
<<<<<<< HEAD
two
=======
lulz
>>>>>>> 46f66fc... a: commit a
This indicated that HEAD
(master
) edited the second line and contains the
value two
. The commit from the topic branch also edited the second line and
contains the value lulz
. From here the file can be edited to produce the
correct result:
one
two
lulz
Once edited, the operation can continue with git rebase --continue
. At
any time, the rebase
operation can be aborted by the --abort
flag:
# bail bail bail
$ git rebase --abort
Until rebase
becomes second nature, it is recommended that rebase
operations
occur on a copy of the topic branch such that it is easy to refer to the topic
branch or roll back if a rebase goes wrong.