Up your git game with --patch
jess unrein
Posted on October 22, 2018
We all strive for clean, single purpose commits with meaningful messages. This can be difficult in practice if you’ve done a lot of debugging since your last commit. Many people I know use git commit -A
or git commit .
when developing and maintaining features. This is fine when making small changes, but I don’t typically like to use these options when committing more than a line or two. When I suggest my favorite git add option instead, many people tell me that they’ve never heard of or used it.
git add --patch
Rather than staging all your recent changes, git add --patch
(or git add -p
) allows you to stage changes in related hunks. It pops you into an interactive menu that allows granular control over staging changes. The interactive interface initially shows you the same hunks of code that git diff
outputs. As you see each change in the interactive menu, you can choose y
to stage the hunk, n
to skip, or s
to separate the hunk out into even more granular pieces.
Example
Say I'm starting a new project and I want a basic User class:
class User:
"""User class for my app"""
def __init__(self, fist_name, last_name, role):
self.first_name = fist_name
self.last_name = last_name
self.role = role
def display_name(self):
print("{} {}".format(self.fist_name, self.last_name))
def display_role(self):
print("User is a {}".format(self.role))
After I commit this code and come back to it, I notice there are a few things I want to change.
First, I realize that I've spelled "first" as "fist" a number of times, and that needs to be corrected. Second, I realize that I don't like having "role" be a string variable on my class. I would rather have different user roles be subclasses of User. So I change the file to look like this.
class User:
"""Base User class for my app"""
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def display_name(self):
print("{} {}".format(self.first_name, self.last_name))
class Admin(User):
'Admin level user'
def __init__(self, first_name, last_name, email):
super(Admin, self).__init__(first_name, last_name)
self.email = email
class Guest(User):
'Guest user'
def display_name(self):
print("Guest {} {}".format(self.first_name, self.last_name))
These are two separate thoughts I want to capture, so I should save these changes in separate commits. Using git add -p
makes this easy even though the two separate thoughts occur in the same file.
When I type in git add -p
I see the following.
diff --git a/user.py b/user.py
index e0bb61a..1b59b32 100644
--- a/user.py
+++ b/user.py
@@ -1,13 +1,24 @@
class User:
- """User class for my app"""
+ """Base User class for my app"""
- def __init__(self, fist_name, last_name, role):
- self.first_name = fist_name
+ def __init__(self, first_name, last_name):
+ self.first_name = first_name
self.last_name = last_name
- self.role = role
def display_name(self):
- print("{} {}".format(fist_name, last_name))
+ print("{} {}".format(self.first_name, self.last_name))
- def display_role(self):
- print "User is a {}".format(role)
+
+class Admin(User):
+ """Admin level user"""
+
+ def __init__(self, first_name, last_name, email):
+ super(Admin, self).__init__(first_name, last_name)
+ self.email = email
+
+
+class Guest(User):
+ """Guest user"""
+
+ def display_name(self):
+ print("Guest {} {}".format(self.first_name, self.last_name))
Stage this hunk [y,n,q,a,d,/,s,e,?]?
Since my user.py
file is relatively short, it all appears in one hunk. I can separate this out by choosing the s
option.
Stage this hunk [y,n,q,a,d,/,s,e,?]? s
Split into 5 hunks.
@@ -1,3 +1,3 @@
class User:
- """User class for my app"""
+ """Base User class for my app"""
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? n
@@ -3,4 +3,4 @@
- def __init__(self, fist_name, last_name, role):
- self.first_name = fist_name
+ def __init__(self, first_name, last_name):
+ self.first_name = first_name
self.last_name = last_name
Stage this hunk [y,n,q,a,d,/,K,j,J,g,e,?]? e
@@ -6,4 +6,3 @@
self.last_name = last_name
- self.role = role
def display_name(self):
Stage this hunk [y,n,q,a,d,/,K,j,J,g,e,?]? n
@@ -8,4 +7,4 @@
def display_name(self):
- print("{} {}".format(fist_name, last_name))
+ print("{} {}".format(self.first_name, self.last_name))
Stage this hunk [y,n,q,a,d,/,K,j,J,g,e,?]? y
@@ -11,3 +10,15 @@
- def display_role(self):
- print("User is a {}".format(role))
+
+class Admin(User):
+ """Admin level user"""
+
+ def __init__(self, first_name, last_name, email):
+ super(Admin, self).__init__(first_name, last_name)
+ self.email = email
+
+
+class Guest(User):
+ """Guest user"""
+
+ def display_name(self):
+ print("Guest {} {}".format(self.first_name, self.last_name))
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? n
git commit -m "Fix typos: 'fist' -> 'first'"
[master f91e0eb] Fix typos: 'fist' -> 'first'
1 file changed, 3 insertions(+), 3 deletions(-)
By splitting the diff of this file into 5 distinct hunks, I was able to choose only the changes that related to fixing the fist
-> first
typo without impacting the functionality of the code.
One tricky bit you might notice is that there are two separate changes in the User.__init__()
method that I don't want in the same commit. The first is to correct the fist -> first
typo. The second is to remove the role
parameter from the function declaration. Even using s
to hunk out the changes won't solve this problem, since the changes are on the same line.
You'll notice that instead of y
, n
, or s
, I used the e
option. The e
option pops you into your default terminal editor (probably vim unless you've changed it to something else), where you can manually edit the file to reflect the changes you want to stage. Save the file and exit, and you can see the changes that are staged for commit with git diff --cached
.
In this instance, I modified this hunk
- def __init__(self, fist_name, last_name, role):
- self.first_name = fist_name
+ def __init__(self, first_name, last_name):
+ self.first_name = first_name
to look like this before committing
- def __init__(self, fist_name, last_name, role):
- self.first_name = fist_name
+ def __init__(self, first_name, last_name, role):
+ self.first_name = first_name
I can use git add -p
again to review the changes for the next commit, or I can use git add user.py
since I know I'm staging all of the remaining changes in the file anyway. For the sake of finishing out the example, I'll use git add -p
again.
diff --git a/user.py b/user.py
index 2a09e35..1b59b32 100644
--- a/user.py
+++ b/user.py
@@ -1,13 +1,24 @@
class User:
- """User class for my app"""
+ """Base User class for my app"""
- def __init__(self, first_name, last_name, role):
+ def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
- self.role = role
def display_name(self):
print("{} {}".format(self.first_name, self.last_name))
- def display_role(self):
- print("User is a {}".format(role))
+
+class Admin(User):
+ """Admin level user"""
+
+ def __init__(self, first_name, last_name, email):
+ super(Admin, self).__init__(first_name, last_name)
+ self.email = email
+
+
+class Guest(User):
+ """Guest user"""
+
+ def display_name(self):
+ print("Guest {} {}".format(self.first_name, self.last_name))
Stage this hunk [y,n,q,a,d,/,s,e,?]? y
git commit -m "Break user roles out into different classes."
[master dea4c79] Break user roles out into different classes.
1 file changed, 16 insertions(+), 5 deletions(-)
When I look back at my git log
I see two separate, concise commits. If I decide I need to revert the commit that breaks out the User.role
into Admin
and Guest
classes, I won't lose the typo fixes in the process.
I use this option to stage all of my changes for commit, even if they are only one or two lines. It helps me catch typos. I never have to worry about mixing typo fixes or de-linting with important functionality changes. It also helps me keep my ideas separate, even if I developed those ideas at the same time.
Posted on October 22, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.