Up your git game with --patch

jess unrein - Oct 22 '18 - - Dev Community

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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,?]?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
git commit -m "Fix typos: 'fist' -> 'first'"
[master f91e0eb] Fix typos: 'fist' -> 'first'
 1 file changed, 3 insertions(+), 3 deletions(-)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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(-) 
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . .