In Progress
Unit 1, Lesson 1
In Progress

Part 1: Patching with Git

Have you ever had to wade through a large git commit full of miscellaneous unrelated changes in order to find the changes you were interested in? And then realized that the culprit for this junk-drawer of updates… was you?

In today’s episode, guest chef Nadia Odunayo is going to show you how you how to use git like a pro in order to make cohesive, laser-focused changesets. Enjoy!

Video transcript & code

Part 1 — git add --patch


We've just finished coding up a new feature at work and it's time to commit all of our changes.


➜  rent-a-bike git:(master) ✗ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   lib/docking_station.rb
    modified:   spec/docking_station_spec.rb

no changes added to commit (use "git add" and/or "git commit -a")
➜  rent-a-bike git:(master) ✗ 

What do we do?

Well, we might add all of our changes using git add .:


$ git add .

Then we type in our commit message using git commit -m:


➜  rent-a-bike git:(master) ✗ git commit -m "Add capacity and availability to docking station"
[master 4d4f18a] Add capacity and availability to docking station
 2 files changed, 44 insertions(+), 4 deletions(-)
➜  rent-a-bike git:(master) 

And then we push up our code with git push:


$ git push

Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 1023 bytes | 1023.00 KiB/s, done.
Total 6 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:nodunayo/rent-a-bike-rt-pt-1.git
 + 96999c5...d2fc8f4 master -> master)

But, it turns out that we accidentally committed a typo.

And it has caused the build to break.


➜  rent-a-bike git:(master) rspec
....F...

Failures:

  1) DockingStation releases a bike
     Failure/Error: @bikes.delete(biek)
     
     NameError:
       undefined local variable or method `biek' for #
       Did you mean?  bike
     # ./lib/docking_station.rb:18:in `release'
     # ./spec/docking_station_spec.rb:16:in `block (2 levels) in '

Finished in 0.00681 seconds (files took 0.14202 seconds to load)
8 examples, 1 failure

Failed examples:

rspec ./spec/docking_station_spec.rb:14 # DockingStation releases a bike
➜  rent-a-bike git:(master) 

On top of that, we didn't want to own up to it at the time, but some of the work we committed wasn't relevant to the feature we were working on…

list of commits

…and it would have been better for our commit history, and our team members, if we'd divided up the work.


➜  rent-a-bike git:(master) ✗ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   lib/docking_station.rb
    modified:   spec/docking_station_spec.rb

no changes added to commit (use "git add" and/or "git commit -a")
➜  rent-a-bike git:(master) ✗ 

So what should we have done instead?

Let's go back to just before we committed our work.

This time, instead of git add ., we decide to use git's --patch option. This gives us a lot more flexibility when we're putting together our commits:


➜  rent-a-bike git:(master) ✗ git add --patch
diff --git a/lib/docking_station.rb b/lib/docking_station.rb
index 2413994..0e4e566 100755
--- a/lib/docking_station.rb
+++ b/lib/docking_station.rb
@@ -1,6 +1,7 @@
 class DockingStation
 
-  def initialize
+  def initialize(capacity = 10)
+    @capacity = capacity
     @bikes = []
   end
 
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y

We're shown all of our changes in smaller groups of code, called 'hunks'.

We can decide to stage any given hunk by typing in y into the command line.


 
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y
@@ -9,7 +10,20 @@ class DockingStation
   end
 
   def dock bike
+    raise "Station is full" if full?
     @bikes << bike
   end
 
+  def release bike
+    @bikes.delete(biek)
+  end
+
+  def full?
+    bike_count == @capacity
+  end
+
+  def available_bikes
+    @bikes.reject { |bike| bike.broken? }
+  end
+
 end
Stage this hunk [y,n,q,a,d,/,K,g,s,e,?]? e

With the 'patch' option, we're more likely to spot errors in our code.

And we can fix them using the e option.


Stage this hunk [y,n,q,a,d,/,K,g,s,e,?]? e

diff --git a/spec/docking_station_spec.rb b/spec/docking_station_spec.rb
index fffb642..a967b82 100755
--- a/spec/docking_station_spec.rb
+++ b/spec/docking_station_spec.rb
@@ -2,12 +2,38 @@ require 'docking_station'
 
 describe DockingStation do
 
-  it 'should accept a bike' do
-    bike = Bike.new
-    station = DockingStation.new
+  let(:bike) { Bike.new }
+  let(:station) { DockingStation.new(20) }
+
+  it 'accepts a bike' do
     expect(station.bike_count).to eq 0
     station.dock(bike)
     expect(station.bike_count).to eq 1
   end
 
+  it 'releases a bike' do
+    station.dock(bike)
+    station.release(bike)
+    expect(station.bike_count). to eq 0
+  end
+
+  it "knows when it's full" do
+    expect(station).not_to be_full
+    20.times { station.dock(bike) }
+    expect(station).to be_full
+  end
+
+  it "does not accept a bike if it's full" do
+    20.times { station.dock(bike) }
+    expect(lambda { station.dock(bike) }).to raise_error(RuntimeError)
+  end
+
+  it 'provides the list of available bikes' do
+    working_bike, broken_bike = Bike.new, Bike.new
+    broken_bike.break!
+    station.dock(working_bike)
+    station.dock(broken_bike)
+    expect(station.available_bikes).to eq ([working_bike])
+  end
+
 end
Stage this hunk [y,n,q,a,d,/,s,e,?]? ?

And there are a handful of other options too. We can see these by hitting the ? key.


Stage this hunk [y,n,q,a,d,/,s,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
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help
@@ -2,12 +2,38 @@ require 'docking_station'

The options include:

  • deciding that we don't want to include a hunk in our current commit, the n option;
  • staging a hunk and all other hunks in the current file, the a option;
  • not staging the current hunk or any others in the file, with the d option;
  • splitting up the current hunk into a smaller one, with the s option;
  • or quitting the patch process all together, by hitting q.

Using git add --patch this time around, means we'll end up with two separate commits…

list of commits

…each with a commit message that clearly explains what changes they contain.


$ git log

...

42dc0f4 Add availability to docking station (HEAD -> master, origin/master)
25c4241 Add capacity to docking station

And, having spotted an error that we would have missed had we indiscriminately added all of the changes, we're now more confident that all of our tests will pass.


➜  rent-a-bike git:(master) rspec
........

Finished in 0.01663 seconds (files took 0.13248 seconds to load)
8 examples, 0 failures
➜  rent-a-bike git:(master) ✗ 

➜  rent-a-bike git:(master) ✗ git add --patch

Next time you find yourself reaching for git add ., go for the --patch option instead.

see Part 2 of this series, "Telling Git Your Intentions"

Responses