DEV Community

jess unrein
jess unrein

Posted on

Up your git game with --patch

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.

Top comments (13)

Collapse
 
biros profile image
Boris Jamot ✊ /

Thanks for that !
Git is really powerful.
But I wonder who really needs this feature.
If you want clean commits, you better change things one by one. First the typo, then the others.
I can't imagine using this frequently. It would waste a lot of time.
But it's my opinion.
Thanks again for your post!

Collapse
 
thejessleigh profile image
jess unrein

That's so interesting! For me, waiting until you've completely finished one thought to start working on something else feels like it would be more time consuming, basically turning each individual thought process into a blocking task. Separating out the changes into clean commits after my ideas have come together makes way more sense for the way my brain works.

I guess it just goes to show that people's brains work in wildly different ways!

Collapse
 
biros profile image
Boris Jamot ✊ /

Maybe that's because I don't (always) do clean commits 😌
Sometime, I'm in the middle of an atomic commit and I come across a typo of my colleague. I better correct it now because I won't open a ticket for that and if I don't do it now, I'll forget it.
If I'm lucky it's in a separate file and I can skip it in the commit, otherwise...
How do we call that ? Laziness ? ☺️

Collapse
 
darksmile92 profile image
Robin Kretzschmar

I also never heard of the patch command, thanks for that!
Really needed this to cleaner commits.

Collapse
 
vlasales profile image
Vlastimil Pospichal • Edited

You changed (destroyed) functionality in first hunk - you deleted role parameter and you left assignment into role attribute.

Collapse
 
thejessleigh profile image
jess unrein

Article has been updated to reflect this, and I added a bit about popping into your terminal's default editor to manually stage changes for commit. Thanks again for the catch!

Collapse
 
thejessleigh profile image
jess unrein

Oh, that was an oversight on my part. Thanks for catching that. I will fix that in a bit!

Collapse
 
vlasales profile image
Vlastimil Pospichal

Thanks for your article!

Collapse
 
0xyasser profile image
Yasser A

I never heard of that, nice!

Collapse
 
mnorbi profile image
Norbert Madarasz

Did you try creating animated gif(s) from the above steps?
It might be easier to follow.

Collapse
 
thejessleigh profile image
jess unrein

I haven't. I actually have never made an animated gif, but that's something worth looking into, for sure!

Collapse
 
biros profile image
Boris Jamot ✊ /

Have a look at ttyrec/ttygif !

Collapse
 
jsrn profile image
James

Nice post! I've been doing this for ages using the GitHub Desktop interface, but I'd never learned to do it with the CLI.