Can I split an already split hunk with git?
Asked Answered
S

4

258

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!

Spokeshave answered 8/6, 2011 at 9:34 Comment(0)
F
321

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

Forefend answered 9/6, 2011 at 9:22 Comment(11)
Cool solution! I saw that but misunderstood... I though the changes would also be removed from the working tree.Spokeshave
Indeed, it's not very obvious from the help text. I find myself using this a lot, actually, since I think git really encourages you to make each commit as precise and beautiful as possible :)Forefend
Your solution does not require any additional software, so I think it should be THE answer to my question. Accepted!Spokeshave
Note that you really do have to replace it with a space. I tried it figuring I could just delete the - characters, and Git complained that my patch didn't apply.Hush
Oh and of course as recommended you'll want to carefully read the straightforward instructions at the bottom of the patch edit file (the lines preceded by #) before attempting this :)Neu
I'm guessing the reason you delete the lines with the '-' and replace '+'s with a space is that then you are forming a patch where those lines with the '-' have already been removed and the lines with the '+'s have already been added (in the eye's of the patch). Or another way of looking at it, is you actually do the action that those characters (-,+) represent (adding a line or removing it). Only the remaining lines with '-'s and '+'s are recorded as changes and the rest is "just how the file is".Hanahanae
I am a bit confused by the fact that when I deleted the lines with + in the hunk as described, it actually deleted them in the file. I just don't want to commit, but still want it in the file.Newland
@Filype: I don't know why that would have happened, I'm afraid - if you were running git add -p and edited a hunk with e that should only affect what's staged, not your working tree.Forefend
Thank you very very much for explaining this. My main fear of using e was that I would clobber everything.Exhibitive
@Jeff Puckett II - Aargh, how embarrassing that that's been wrong for so long; thank-you for pointing it out. I use this feature all the time, but somehow got mixed up when typing out the answer. (I've also upvoted your very nicely explained and more detailed answer.)Forefend
@MarkLongair awesome! +2 = ( +1 - (-1) ) I have deleted my comment.Gastight
G
82

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.

Gastight answered 11/6, 2016 at 22:45 Comment(5)
Bonus #2: If you happen to be using VIM as your editor, you have to press "d" twice on your keyboard to delete a line :DMenispermaceous
Also, instead of removing the added lines you do not want to add, you can replace + with #. The result is the same, but maybe you're uncomfortable with deleting (and being unable to revert) or you want to experiment before you save.Cognac
And that, for vim is r # over the plus xDCaras
The goal is "How can I add the CSS comment removal only to the next commit ?", but the steps are really confusing as to what it's accomplishing. (we want to "add" only the "removal" of the few lines to the next commit.) So simply saying remove or add is very confusing. Stating what was accomplished at each step would help clarify.Orv
I believe this answer should've been the top answer. It does a much better job at explaining how to go about this process!Iambus
L
10

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.

Lieabed answered 8/6, 2011 at 9:49 Comment(2)
Your second proposition is quite evident, but the first one is interesting, could you detail a bit more? I installed git-gui but I have no clue how to achieve what you're describing.Spokeshave
tanks a lot! This works! I was even able to select the lines I wanted to stage and index them with one click.Spokeshave
W
0

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.

Wieche answered 8/6, 2011 at 9:38 Comment(4)
This is what I tried, and the hunk in my question is the only one when I run git add -p again, but I cannot split it. I get this : Stage this hunk [y,n,q,a,d,/,e,?]? and then hitting 's' prints the help. BTW, you meant add patch, not patch add? Or is there a git patch plugin I should install?Spokeshave
Did you commit the staged hunks before you ran this again? And no, Mercurial has plugins, Git does not.Wieche
No I didn't, I want them to be in the same commit (but I guess if your solution works, I can use --amend to achieve this). I'll give it a try.Spokeshave
As my answer said → git rebase -i. Which is more flexible than commit --amendWieche

© 2022 - 2024 — McMap. All rights reserved.