Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Git Tip of the Week: Interactive Adding

2011, git, gtotw

This week's Git Tip of the Week is about interactive adding. You can subscribe to the feed if you want to receive new instalments automatically.


This week's tip of the week is about interactive adding. Up until now, whenever you've added a file with git add, it has taken the entire file and added that into the index.

However, it is possible to add only parts of a file to the index; in Git terminology, these are called hunks. A hunk is merely a set of changes to a file, involving lines added (those prefixed with +) and lines removed (those prefixed with -). As well as being a way of textually showing the differences, when it comes to packfiles (which we've covered previously), it uses these hunks to store multiple versions of the same content whilst using significantly less space.

There is an interactive menu that can be brought up with git add --interactive, or git add -i for short. However, most of the options here are not very useful; the one that you'll find yourself using most frequently is the [p]atch command. There is a shorter way of solving this, with the git add --patch command, or just git add -p.

What does this do? Well, it allows you to selectively choose which diffs get added into the index. Although this might not sound particularly useful, it lets you break down changes to one file into multiple smaller changes, provided that you commit them each time. Let's look at an example, with a Person class that we're adding a first name and last name to:


(master) $ git show Person.java | tail -4
@@ -0,0 +1,3 @@
+public class Person {
+
+}
(master) $ git diff
@@ -1,2 +1,17 @@
 public class Person {
+  private String firstName;
+  public String getFirstName() {
+    return firstName;
+  }
+  public void setFirstName(String firstName) {
+    this.firstName = firstName;
+  }

+  private String lastName;
+  public String getLastName() {
+    return lastName;
+  }
+  public void setLastName(String lastName) {
+    this.lastName = lastName;
+  }
 }

Normally, if we do git add, it will put the change for both the firstName and lastName into the index. What if we wanted to segregate these out? Well, we could just take a copy of the file, edit out one change, add it, copy the file back, and then add the second change. But we can use Git to help us here with git add -p:


@@ -1,2 +1,17 @@
 public class Person {
+  private String firstName;
+  public String getFirstName() {
+    return firstName;
+  }
+  public void setFirstName(String firstName) {
+    this.firstName = firstName;
+  }

+  private String lastName;
+  public String getLastName() {
+    return lastName;
+  }
+  public void setLastName(String lastName) {
+    this.lastName = lastName;
+  }
 }
Stage this hunk [y,n,q,a,d,/,e,?]? 

The 'stage this hunk' is asking us what we want to do. If we wanted to add it, we'd say 'y' or 'a'. If we didn't, we could say 'n' or 'd'. (The former is 'just this one' whilst the latter is 'and all the rest'.)

What if we wanted to add it piecemeal? Well, there's a [s]plit command we can use, which breaks this hunk down into smaller hunks:


Stage this hunk [y,n,q,a,d,/,e,?]? s
Split into 2 hunks.
@@ -1,2 +1,9 @@
 public class Person {
+  private String firstName;
+  public String getFirstName() {
+    return firstName;
+  }
+  public void setFirstName(String firstName) {
+    this.firstName = firstName;
+  }
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y

@@ -2,2 +9,9 @@
 
+  private String lastName;
+  public String getLastName() {
+    return lastName;
+  }
+  public void setLastName(String lastName) {
+    this.lastName = lastName;
+  }
 }
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? n
 

What we've done is to add the first 7 lines of the change into the git index, whilst leaving the last 7 outside. If we do a git diff, we'll just see the unstaged difference:


(master) $ git diff
diff --git a/Person.java b/Person.java
index a6d00fd..23dd325 100644
--- a/Person.java
+++ b/Person.java
@@ -7,4 +7,11 @@ public class Person {
     this.firstName = firstName;
   }
 
+  private String lastName;
+  public String getLastName() {
+    return lastName;
+  }
+  public void setLastName(String lastName) {
+    this.lastName = lastName;
+  }
 }

We can see in the Git status that we have a staged change and an unstaged change:


(master) $ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#	modified:   Person.java
#
# Changes not staged for commit:
#   (use "git add ..." to update what will be committed)
#   (use "git checkout -- ..." to discard changes in working directory)
#
#	modified:   Person.java
#

Normally you'd be concerned if you saw this – as if you had forgotten to add something. However, it's exactly the right effect in this case; we've added part of our change, and we have part of the change still to go. From here, we can commit as usual:


(master) $ git commit -m "Added firstname"
[master 2e0d5f8] Added firstname
 1 files changed, 7 insertions(+), 0 deletions(-)
(master) $ git add Person.java
(master) $ git commit -m "Added lastname"
[master 1d09387] Added lastname
 1 files changed, 7 insertions(+), 0 deletions(-)

If we look at the blame for the file, we can see we've committed the changes as separate commits:


(master) $ git blame Person.java | cut -c 1-9,50-80
391f64ef  1) public class Person {
2e0d5f8b  2)   private String firstName;
2e0d5f8b  3)   public String getFirstNam
2e0d5f8b  4)     return firstName;
2e0d5f8b  5)   }
2e0d5f8b  6)   public void setFirstName(
2e0d5f8b  7)     this.firstName = firstN
2e0d5f8b  8)   }
391f64ef  9) 
1d093875 10)   private String lastName;
1d093875 11)   public String getLastName
1d093875 12)     return lastName;
1d093875 13)   }
1d093875 14)   public void setLastName(S
1d093875 15)     this.lastName = lastNam
1d093875 16)   }
391f64ef 17) }

Being able to add changes in parts, rather than in their entirety, is a useful technique when you have subsets of changes that have been made to a file without interening commits.


Come back next week for another instalment in the Git Tip of the Week series.