229
votes

I've recently discovered git's patch option to the add command, and I must say it really is a fantastic feature. I also discovered that a large hunk could be split into smaller hunks by hitting the s key, which adds to the precision of the commit. But what if I want even more precision, if the split hunk is not small enough?

For example, consider this already split hunk:

@@ -34,12 +34,7 @@
   width: 440px;
 }

-/*#field_teacher_id {
-  display: block;
-} */
-
-form.table-form #field_teacher + label,
-form.table-form #field_producer_distributor + label {
+#user-register form.table-form .field-type-checkbox label {
   width: 300px;
 }

How can I add the CSS comment removal only to the next commit ? The s option is not available anymore!

4

4 Answers

288
votes

If you're using git add -p and even after splitting with s, you don't have a small enough change, you can use e to edit the patch directly.

This can be a little confusing, but if you carefully follow the instructions in the editor window that will be opened up after pressing e then you'll be fine. In the case you've quoted, you would want to replace the - with a space at the beginning of these lines:

-
-form.table-form #field_teacher + label,
-form.table-form #field_producer_distributor + label {

... and delete the the following line, i.e. the one that begins with +. If you then save and exit your editor, just the removal of the CSS comment will be staged.

70
votes

Let's say your example.css looks like this:

.classname {
  width: 440px;
}

/*#field_teacher_id {
  display: block;
} */

form.table-form #field_teacher + label,
form.table-form #field_producer_distributor + label {
  width: 300px;
}

.another {
  width: 420px;
}

Now let's change the style selectors in the middle block, and while we're at it, delete some old commented-out style we don't need anymore.

.classname {
  width: 440px;
}

#user-register form.table-form .field-type-checkbox label {
  width: 300px;
}

.another {
  width: 420px;
}

That was easy, now let's commit. But wait, I want to maintain logical separation of changes in version control for simple step-wise code review, and so that my team and I can easily search commit history for specifics.

Deleting old code is logically separate from the other style selector change. We're going to need two distinct commits, so let's add hunks for a patch.

git add --patch
diff --git a/example.css b/example.css
index 426449d..50ecff9 100644
--- a/example.css
+++ b/example.css
@@ -2,12 +2,7 @@
   width: 440px;
 }

-/*#field_teacher_id {
-  display: block;
-} */
-
-form.table-form #field_teacher + label,
-form.table-form #field_producer_distributor + label {
+#user-register form.table-form .field-type-checkbox label {
   width: 300px;
 }

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

Whoops, looks like the changes are too close, so git has hunked them together.

Even trying to split it by pressing s has the same result because the split isn't granular enough for our precision changes. Unchanged lines are required between changed lines for git to be able to automatically split the patch.

So, let's manually edit it by pressing e

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

git will open the patch in our editor of choice.

# Manual hunk edit mode -- see bottom for a quick guide
@@ -2,12 +2,7 @@
   width: 440px;
 }

-/*#field_teacher_id {
-  display: block;
-} */
-
-form.table-form #field_teacher + label,
-form.table-form #field_producer_distributor + label {
+#user-register form.table-form .field-type-checkbox label {
   width: 300px;
 }

# ---
# 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.

Let's review the goal:

How can I add the CSS comment removal only to the next commit ?

We want to split this into two commits:

  1. The first commit involves deleting some lines (comment removal).

    To remove the commented lines, just leave them alone, they are already marked to track the deletions in version control just like we want.

    -/*#field_teacher_id {
    - display: block;
    -} */

  2. The second commit is a change, which is tracked by recording both deletions and additions:

    • Deletions (old selector lines removed)

      To keep the old selector lines (do not delete them during this commit), we want...

      To remove '-' lines, make them ' '

      ...which literally means replacing the minus - signs with a space character.

      So these three lines...

      -
      -form.table-form #field_teacher + label,
      -form.table-form #field_producer_distributor + label {

      ...will become (notice the single space at the first of all 3 lines):


      form.table-form #field_teacher + label,
      form.table-form #field_producer_distributor + label {

    • Additions (new selector line added)

      To not pay attention to the new selector line added during this commit, we want...

      To remove '+' lines, delete them.

      ...which literally means to delete the whole line:

      +#user-register form.table-form .field-type-checkbox label {

      (Bonus: If you happen to be using vim as your editor, press dd to delete a line. Nano users press Ctrl+K)

Your editor should look like this when you save:

# Manual hunk edit mode -- see bottom for a quick guide
@@ -2,12 +2,7 @@
   width: 440px;
 }

-/*#field_teacher_id {
-  display: block;
-} */

 form.table-form #field_teacher + label,
 form.table-form #field_producer_distributor + label {
   width: 300px;
 }

# ---
# 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.

Now let's commit.

git commit -m "remove old code"

And just to make sure, let's see the changes from the last commit.

git show
commit 572ecbc7beecca495c8965ce54fbccabdd085112
Author: Jeff Puckett <[email protected]>
Date:   Sat Jun 11 17:06:48 2016 -0500

    remove old code

diff --git a/example.css b/example.css
index 426449d..d04c832 100644
--- a/example.css
+++ b/example.css
@@ -2,9 +2,6 @@
   width: 440px;
 }

-/*#field_teacher_id {
-  display: block;
-} */

 form.table-form #field_teacher + label,
 form.table-form #field_producer_distributor + label {

Perfect - you can see that only the deletions were included in that atomic commit. Now let's finish the job and commit the rest.

git add .
git commit -m "change selectors"
git show
commit 83ec3c16b73bca799e4ed525148cf303e0bd39f9
Author: Jeff Puckett <[email protected]>
Date:   Sat Jun 11 17:09:12 2016 -0500

    change selectors

diff --git a/example.css b/example.css
index d04c832..50ecff9 100644
--- a/example.css
+++ b/example.css
@@ -2,9 +2,7 @@
   width: 440px;
 }

-
-form.table-form #field_teacher + label,
-form.table-form #field_producer_distributor + label {
+#user-register form.table-form .field-type-checkbox label {
   width: 300px;
 }

Finally you can see the last commit only includes the selector changes.

9
votes

If you can use git gui, it allows you to stage changes line by line. Unfortunately, I don't know how to do it from the command line - or even if it is possible.

One other option I've used in the past is rolling back part of the change (keep the editor open), commit the bits I want, undo and re-save from the editor. Not very elegant, but gets the job done. :)


EDIT (git-gui usage):

I am not sure if the git-gui is the same in msysgit and linux versions, I've only used the msysgit one. But assuming it is the same, when you run it, there are four panes: top-left pane is your working directory changes, bottom-left is your stages changes, top-right is the diff for the selected file (be it working dir or staged), and bottom right is for description of the commit (I suspect you won't need it). When you click a file in the top-right one, you will see the diff. If you right-click on a diff line, you'll see a context menu. The two options to note are "stage hunk for commit" and "stage line for commit". You keep selecting "stage line for commit" on the lines you want to commit, and you are done. You can even select several lines and stage them if you want. You can always click the file in the staging box to see what you are bout to commit.

As for committing, you can use either the gui tool or the command line.

0
votes

One way to do it is to skip the chunk, git add whatever else you need, and then run git add again. If this is the only chunk, you'll be able to split it.

If you're worried about the order of commits, just use git rebase -i.