Break up changes into different commits with git add -p

This guy’s post led to this one.

I’m irresponsible about committing after each conceptual unit of work. Lots of time, I’ll edit a file to fix one bug, then while I’m in there, I’ll edit some other code because I see a better way to do something else. Then maybe I’ll add a few doctests to a completely different section because I feel like it.

After a few hours, I have edits in a single file that are related to multiple separate tasks. So back when I used svn, I would commit it with a message like “Fixed topics A, B, C”. Or I would say “Fixed A and a bunch of other stuff”.

Now with git, before I commit my changes, I run:

$ git add -p frob.py

Then git opens up an interactive session that walks through all the changes in that file and asks me if I want to stage each one. It is also possible to look at every change across a repository if you want to — don’t specify the file or files you want to see.

In the first pass, I stage all the hunks related to the first issue. Then I commit those changes. Then I repeat the process and stage chunks related to the next issue.

Keep in mind that I committed my changes after the first pass, so when I go through the file the second time, I won’t get prompted for those changes.

A real-world example

I’ve got two edits in mkinstall.py. One is a change to the list of files I want to ignore, and the other edit is a silly stylistic change. I want to commit them separately.

$ git diff mkinstall.py
diff --git a/mkinstall.py b/mkinstall.py
index 4c6bb4e..2cb43de 100644
--- a/mkinstall.py
+++ b/mkinstall.py
@@ -17,7 +17,8 @@ Otherwise, I'll add a symlink.
import os, shutil

# Anything you want to skip:
-skip_us = ["mkinstall.py", ".svn", "_vimrc"]
+skip_us = ["mkinstall.py", ".svn", "_vimrc", "diffwrap.sh", "lib",
+ "lynx_bookmarks.html", "ipythonrc-matt", ".git"]

# Anything you want to copy rather than symlink to:
copy_us = [".vim"]
@@ -60,7 +61,8 @@ for thing in copy_us:
if os.path.islink(homefile):
print "A symbolic link to %s exists already, so I'm not going to copy over it." % homefile

- elif os.path.exists(homefile): continue
+ elif os.path.exists(homefile):
+ continue

else:
svnfile = os.path.join(svnpath, thing)

This is what happens when I run git add -p:

$ git add -p mkinstall.py
diff --git a/mkinstall.py b/mkinstall.py
index 4c6bb4e..2cb43de 100644
--- a/mkinstall.py
+++ b/mkinstall.py
@@ -17,7 +17,8 @@ Otherwise, I'll add a symlink.
import os, shutil

# Anything you want to skip:
-skip_us = ["mkinstall.py", ".svn", "_vimrc"]
+skip_us = ["mkinstall.py", ".svn", "_vimrc", "diffwrap.sh", "lib",
+ "lynx_bookmarks.html", "ipythonrc-matt", ".git"]

# Anything you want to copy rather than symlink to:
copy_us = [".vim"]
Stage this hunk [y/n/a/d/j/J/?]?

At this point, I will hit y. Now that section of the file is staged to be committed. That is not the same as committing it.
Now git shows the next section of code that is different:

Stage this hunk [y/n/a/d/j/J/?]? y
@@ -60,7 +61,8 @@ for thing in copy_us:
if os.path.islink(homefile):
print "A symbolic link to %s exists already, so I'm not going to copy over it." % homefile

- elif os.path.exists(homefile): continue
+ elif os.path.exists(homefile):
+ continue

else:
svnfile = os.path.join(svnpath, thing)
Stage this hunk [y/n/a/d/K/?]?

I don’t want to stage this right now, so I hit n. That’s the last edit in the file, so the interactive session completes. Now when I run git diff –cached, which tells me what is about to be committed, look what I see:

$ git diff --cached mkinstall.py
diff --git a/mkinstall.py b/mkinstall.py
index 4c6bb4e..a348ee1 100644
--- a/mkinstall.py
+++ b/mkinstall.py
@@ -17,7 +17,8 @@ Otherwise, I'll add a symlink.
import os, shutil

# Anything you want to skip:
-skip_us = ["mkinstall.py", ".svn", "_vimrc"]
+skip_us = ["mkinstall.py", ".svn", "_vimrc", "diffwrap.sh", "lib",
+ "lynx_bookmarks.html", "ipythonrc-matt", ".git"]

# Anything you want to copy rather than symlink to:
copy_us = [".vim"]

So now I’ll commit this edit with an appropriate remark:

$ git commit -m "Added some more files to the list of files to be skipped"
Created commit ce0478d: Added some more files to the list of files to be skipped
1 files changed, 2 insertions(+), 1 deletions(-)

Now I’ll view the unstaged changes again in my file, and notice that the other change still remains:

$ git diff mkinstall.py
diff --git a/mkinstall.py b/mkinstall.py
index a348ee1..2cb43de 100644
--- a/mkinstall.py
+++ b/mkinstall.py
@@ -61,7 +61,8 @@ for thing in copy_us:
if os.path.islink(homefile):
print "A symbolic link to %s exists already, so I'm not going to copy over it." % homefile

- elif os.path.exists(homefile): continue
+ elif os.path.exists(homefile):
+ continue

else:
svnfile = os.path.join(svnpath, thing)

At this point, I can rerun git add -p and stage up more stuff to be committed. In this case, it is more realistic that I would run

git commit -a -m "Made a silly style change"

That will stage and commit that last edit in one swoop.

3 thoughts on “Break up changes into different commits with git add -p

Comments are closed.