DEV Community

Cover image for A Quick Guide to Hunky Git
Jacob Herrington (he/him)
Jacob Herrington (he/him)

Posted on • Edited on

A Quick Guide to Hunky Git

Cover Image: Two People Holding Macbook Pro by Christina Morillo
When it comes to git, many developers are comfortable with making a change in one or more files, staging those files, then creating a commit.

Generally, that workflow holds up just fine. However, occasionally thinking about git in terms of files can result in commits that are not atomic.

Sometimes you make multiple changes in a single file that should be split into different commits. For example, if you're working on adding some functionality to a class, but decide to refactor another method during your work, it might make sense to split those two changes into two commits.

You could easily do this by undoing and replaying changes, but if your changes were more significant than just a few lines, or you did this in multiple files, it would be more convenient to lean on git here.

This is the use case for git add --patch. As most developers using git know git add is the command to stage files for a future commit. I've found that many developers using git are less familiar with the --patch flag, so I'll explain that here.

When you encounter a new git command or flag, the first thing you should do is ask git about it:

git add --help
Enter fullscreen mode Exit fullscreen mode

The --help flag will spit out quite a bit of useful documentation (that you should read, not skim) describing exactly what a given git command does.

Chiefly, we'll see that the git add command, "updates the index using the current content found in the working tree, to prepare the content staged for the next commit."

That pretty much confirms what we already know, but what about --patch?

Scroll down a bit, and you'll see the description of --patch (which can be shortened to -p):

-p, --patch
Interactively choose hunks of patch between the index and the work tree and add them 
to the index. This gives the user a chance to review the difference before adding 
modified contents to the index.
Enter fullscreen mode Exit fullscreen mode

This description explains one or two important things about using the --patch flag. First, it allows the user to "interactively choose hunks" to be staged. Second, it "gives the user a chance to review the difference before adding modified contents to the index."

While the first benefit of git add --patch is incredibly useful, I also find a lot of value in the practice of reviewing the changes you are making before creating a commit.

Using the --patch flag is incredibly simple.

For this example, say I wanted to change a few things in the Solidus gemspec file. I'm going to update the gem description and include a dependency on solidus_graphql_api.

After making those changes, I run git add solidus.gemspec --patch, and I encounter the UI for staging hunks.

solidus master % git add solidus.gemspec --patch
diff --git a/solidus.gemspec b/solidus.gemspec
index 3d8d40ed4..d585334e1 100644
--- a/solidus.gemspec
+++ b/solidus.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
   s.name        = 'solidus'
   s.version     = Spree.solidus_version
   s.summary     = 'Full-stack e-commerce framework for Ruby on Rails.'
-  s.description = 'Solidus is an open source e-commerce framework for Ruby on Rails.'
+  s.description = 'Solidus is a neat open source e-commerce framework for Ruby on Rails.'

   s.files        = Dir['README.md', 'lib/**/*']
   s.require_path = 'lib'
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?
Enter fullscreen mode Exit fullscreen mode

I guess this is the right time to define a hunk.

The GNU diffutils manual describes a hunk with this language:

When comparing two files, diff finds sequences of lines common to both files, interspersed with groups of differing lines called hunks.

So, put simply, it's one or more lines that changed. The great thing about staging hunks instead of files is that it gives you fine-grained control over what gets staged and when. You can review your code while staging, and structure your commits to be even more atomic.

If you're in this interface, you might notice that the prompt is pretty opaque.

Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?
Enter fullscreen mode Exit fullscreen mode

Who in their right mind decided we need 11 options to answer a yes/no question!? Thankfully, we can just ask git to tell us why it is the way it is, by answering its question with a question mark:

Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? ?
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
e - manually edit the current hunk
? - print help
Enter fullscreen mode Exit fullscreen mode

That's handy, thanks git!

Most of these options are convenience features. I particularly like e, because it allows me to see mistakes and fix them before they are staged. Taking advantage of this tool should help eliminate future commits like "Fix whitespace" or "Remove typo" because that sort of thing can be handled while I'm staging my changes.

This looks good, so I'll go ahead and stage this one with y.

Then, I'm progressed to the next hunk:

@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
   s.add_dependency 'solidus_core', s.version
   s.add_dependency 'solidus_frontend', s.version
   s.add_dependency 'solidus_sample', s.version
+  s.add_dependency 'solidus_graphql', s.version
 end
Stage this hunk [y,n,q,a,d,K,g,/,e,?]?
Enter fullscreen mode Exit fullscreen mode

I don't want to stage this, because I'd like to make a different commit for adding the new dependency, so I respond to the prompt with q to exit the prompt without staging anything else.

Now I make my commit for the description change:

git commit -m "Update gem description."
Enter fullscreen mode Exit fullscreen mode

And I can start looking at hunks again for my next commit:

git add solidus.gemspec --patch
Enter fullscreen mode Exit fullscreen mode
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
   s.add_dependency 'solidus_core', s.version
   s.add_dependency 'solidus_frontend', s.version
   s.add_dependency 'solidus_sample', s.version
+  s.add_dependency 'solidus_graphql', s.version
 end
Stage this hunk [y,n,q,a,d,K,g,/,e,?]?
Enter fullscreen mode Exit fullscreen mode

I see a mistake here. The gem is actually called solidus_graphql_api, so I need to make a quick edit. Answering the prompt with e drops me into Vim so I can make a quick change.

# Manual hunk edit mode -- see bottom for a quick guide.
  @@ -26,4 +26,5 @@ Gem::Specification.new do |s|
   s.add_dependency 'solidus_core', s.version
   s.add_dependency 'solidus_frontend', s.version
   s.add_dependency 'solidus_sample', s.version
+  s.add_dependency 'solidus_graphql_api', s.version
  end
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
#·
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging.
# If it does not apply cleanly, you will be given an opportunity to
# edit again.  If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.
~
Enter fullscreen mode Exit fullscreen mode

After making and saving the change, I'm dropped back into my terminal, and I can create a commit:

git commit -m "Add solidus_graphql_api dependency"
Enter fullscreen mode Exit fullscreen mode

Now if we ask for the log:

commit 2eff835db6807163f395529cfb89643a17b9b817 (HEAD -> test-git-patch)
Author: jacobherrington <redacted@gmail.com>
Date:   Tue Aug 13 10:39:56 2019 -0500

    Add solidus_graphql_api dependency

commit 70c9aab9d7a829042a35ebc9153b1b85c4e35292
Author: jacobherrington <redacted@gmail.com>
Date:   Tue Aug 13 09:30:09 2019 -0500

    Update gem description
Enter fullscreen mode Exit fullscreen mode

The --patch flag isn't always the right tool, sometimes it's totally fine to just stage files for your commits, but especially in cases of more substantial, more complex changes, I think using the --patch flag is a smart check on yourself.

It's always smart to take an extra step to verify that you made the right decisions.

A good programmer is someone who always looks both ways before crossing a one-way street. - Doug Linder

Unrelated, if anyone can tell me who Doug Linder is, and where that quote originated from in the first place, that'd be cool. 🤠

Top comments (0)