The Nuclear Squid Musings on stuff and other things.

git add --patch and --interactive

One of the things that is pretty much unique to Git is the index (also known as the cache or staging area).

The index is the place where you prepare your next commit. In other systems whenever you make a commit, all the changes you made in your working copy are committed.

Note: Depending on your version of git, your output might differ slightly. I’m using Git 1.6.0.4.766.g6fc4a right now.

git add –patch

With Git, on the other hand, you first add all the changes you want to be in the next commit to the index via git add (or remove a file withgit rm). Normally, calling git add <file> will add all the changes in that file to the index, but add supports an interesting option: --patch, or -p for short. I use this option so often that I’ve added a git alias for easy access: git config alias.ap "add --patch".

When you pass this option to add, instead of immediately adding all the changes in the file to the index, it goes through each change and asks you what you want to do, and looks like this:

@@ -24,7 +32,12 @@ module CoreMIDI
   ]

   functions.each do |func|
-    attach_function *func
+    begin
+      attach_function *func
+    rescue Exception => e
+      $stderr.puts "Couldn't attach function #{func.first}"
+      raise
+    end
   end

   def midi_read_proc()
Stage this hunk [y/n/a/d/K/j/J/e/?]?

At the top is some diff info about the current position in the file, followed by the actual diff of the source code (called “hunk”), and below that are your available options. Pressing ? will give you a short explanation for each one, but the ones I use most often are:

  • y - Yes, add this hunk
  • n - No, don’t add this hunk
  • d - No, don’t add this hunk and all other remaining hunks. Useful if you’ve already added what you want to, and want to skip over the rest
  • s - Split the hunk into smaller hunks. This only works if there’s unchanged lines between the changes in the displayed hunk, so this wouldn’t have any effect in the example above
  • e - Manually edit the hunk. This is probably the most powerful option. As promised, it will open the hunk in a text editor and you can edit it to your hearts content

Manually editing the hunk is immensely powerful, but also a bit complicated if you’ve never done it before. The most important thing to keep in mind: The diff is always indented with one character in addition to whatever other indentation is there. The character can either be a space (indicates an unchanged line), a - indicating that the line was removed, or a + indicating that the line was added. Nothing else. It must be either a space, a - or a +. Anything else, and you’ll get errors (there’s no character for a changed line, since those are handled by removing the old line, and adding the changed one as new).

Since you’ve got the diff open in your favorite text editor (you did configure Git to use your favorite text editor, right?), you can do whatever you want - as long as you make sure the resulting diff applies cleanly.

And therein lies the trick. If you’ve never done this before, Git will tell you “Your edited hunk does not apply. Edit again?” so often, you’ll start to hate yourself for your inability to figure this out, even though it seems so easy (or Git because it can’t figure out what you want).

One thing that tripped me up quite often was that I forgot the one character indent. I’d mark a line with a - to be removed, but in most text editors that inserts a -, it doesn’t overwrite the space that was there before. This means you’re adding an additional space to the whole line, which in turn means the diff algorithm can’t find/match the line in the original file, which in turn means Git will yell at you.

The other thing is that the diff still has to make sense. “Sense” means that it can be applied cleanly. Exactly how you create a sensible diff seems to be a bit of an dark art (at least to me right now), but you should always keep in mind how the original file looked like, and then plan your -s and +s accordingly. If you edit your hunks often enough you’ll eventually get the hang of it.

And if you decide that you don’t want to edit the hunk after all, simply delete everything in the file, save & close it. Git will then abort the editing process and leave the hunk unchanged, returning you to the standard selection.

git add –interactive

--interactive (or -i) is the big brother of --patch. --patch only lets you decide about the individual hunks in files. --interactive enters the interactive mode, and is a bit more powerful. So powerful that it has its own little submenu:

*** Commands ***
  1: [s]tatus     2: [u]pdate     3: [r]evert     4: [a]dd untracked
  5: [p]atch      6: [d]iff       7: [q]uit       8: [h]elp
What now> 

Interactive mode is designed to make it easier to review and manipulate changes in the whole repository. While you can restrict it to a subset of files, I usually use it when there are a lot of different changes strewn across the whole repository.

Using it is pretty straightforward. Press either the number or the character in the square brackets of the command you want to execute.

  • status gives you a status output, showing you how many added and removed lines there are in each file (split up between staged and unstaged changes)
  • update allows you to stage complete files into the index
  • revert allows you to unstage changes from the index, again operating on whole files
  • add untracked allows you to (you guessed it) add untracked files
  • patch does the same thing as --patch
  • diff shows you a diff between the index and HEAD

quit and help work exactly as advertised.

Each of the commands allows you to choose what files you want to work on. You can select multiple files by entering their numbers (or the characters in square brackets) and pressing enter. You can enter multiple numbers/characters at once, separated by spaces, or add them individually. Selected files are marked with a * in front of their line.

Leave the line empty and press enter to tell Git that you’re done, and it’ll continue to the next stage, if there is one

Conclusion

Both those options allow you to create a commit that represents a logical change, even if you’ve got a lot of other changes lying around.

This is really great for me, since it lets me write as much code as I want to without having to remember to commit each change separately beforehand, thus not interrupting my flow. I can easily pick apart all the changes afterwards, and still maintain my “one commit for each logical change” policy.