18

I want to git add only a specific range of lines from a file with a single shell command. I'm imaging something like:

git add -c myfile.go@123..204

I don't want to use git add -i, git add -p, or git -e (interactive git tools) because my file is big and has many changes, and I already know precisely which lines I want. For the sake of argument (get it?), let's say I want to stage lines inclusively 123-204 of a 2000 line file myfile.go.

I also don't want to use a GUI. Either my computer is too old for the new programs, too slow to run them, the screen is too small... whatever. All the of the above. Github GUI, Sourcetree, an (Atom|IntelliJ|VSC|.* plugin, KDiff, Kaleidescope... they're all out.

Can it be done?

Bonus points: can it be done for multiple files at once?

For reference:

Two related, but not duplicate, questions. Neither fits my needs:

Git documentation that's close, but no cigar:

irbanana
  • 860
  • 12
  • 19
  • I think you've got to accept that --patch is the closest you're getting lol – L_Church May 31 '18 at 14:53
  • So tools like Sourcetree actually have to step through like at least 4 different git and/or file manipulation steps to just stage one measly hunk? – irbanana May 31 '18 at 14:54
  • if you have this much *commit*ment, you could just buy a new, updated computer... I've been searching this query for a bit and all i'm seeing is --patch – L_Church May 31 '18 at 14:56
  • 2
    But my trusty TI-86 has gotten me this far... I can't just give it up now. – irbanana May 31 '18 at 14:59
  • https://stackoverflow.com/a/6437276/8282168 one more. Good luck, Soldier. I have nothing further to give. May your calculator break free – L_Church May 31 '18 at 15:06
  • Can't you fix this at the editor side? Or through streaming tools like `sed`? – CodeCaster May 31 '18 at 15:18
  • 2
    Most likely what you need to look for is a "plumbing" (low-level data structure manipulation) command rather than a "porcelain" (high-level version control) command. – IMSoP May 31 '18 at 16:33
  • 1
    If you can construct a patch file with your required lines, `git apply --cached` will stage those changes without touching the working tree. The [implementation of `git add --interactive`](https://github.com/git/git/blob/master/git-add--interactive.perl) appears to be written in Perl, and works that way under the hood. As far as I can see, the approach it takes is to take sections from a patch (`git diff` output, essentially), then reconstruct the parts required into a new patch file by manipulating the header/context lines. – IMSoP May 31 '18 at 16:51
  • Thinking about how to do this, I realise there's actually an ambiguity in your proposed syntax: do the line numbers refer to their position in the *previous* version or the *new* version? If I delete line 5 and want to stage that deletion, what lines would I ask to stage? What if there's a new line 5 which I *don't* want? Interactive tools don't have this problem, because they are working with lines (or "hunks") *of a diff output*, so the deletion of line 5 might be indicated on line 2, or line 10, of the diff output, and that is the line that needs to be in the patch passed to `git apply`. – IMSoP May 31 '18 at 17:30

2 Answers2

6

From comments, it's unclear whether you're concerned about the efficiency of executing the add operation, the typing that needs to be done to instruct the tools what to do, or both.

I wouldn't worry about the former; and if you are worried about it, there's not much you can do anyway. There are more steps than you might think sound reasonable, and they involve processing the whole file; but really, it doesn't matter, in that I've never seen a single-file staging operation take enough time to worry about it.

As for the amount of typing involved, the add options you listed are the closest built-in support git offers. So you can do a little scripting to augment them. But it won't be easy to ensure that it always "gets it right".

In particular, it's trickier than you might realize to define "the changes to this range of lines". The seemingly-obvious issue is that line additions and deletions change the line numbers of lines that occur after them; but you can probably resolve that by defining your line number range in terms of the current working version of the file (since that's what one would likely be looking at most recently when determining the line number range)...

But the bigger problem is detecting the case where all lines in a range were edited, and that range overlaps the end of the range of lines for which you're staging changes. For example, suppose you have the file

1
2
3
4
5

in the index, and your working copy says

1
2
3 THREE
4 FOUR
5 FIVE

Now you specify that you want to stage the changes from lines 2-4.

The patch will look like

@@ -1,5 +1,5 @@
 1
 2
-3
-4
-5
+3 THREE
+4 FOUR
+5 FIVE

And it's pretty clear that in this case the intuitive result is

1
2
3 THREE
4 FOUR
5

But writing code that gets this "right" without getting other cases "wrong" (relative to equally intuitive interpretation) is not so easy. Sometimes it really is open to interpretation. "Was this one operation that changed three lines? Or one operation that deleted three lines, followed by three operations that each added a line? Or..."

The automated tooling in git avoids making those interpretive decisions, first by looking at code in hunks of change (rather than arbitrary line ranges) and making you intervene manually if you want something different (i.e. by using patch-edit mode); and then by inserting conflict markers (and again requiring manual intervention) when interpretation still seems to be required.

So you basically have to make simplifying assumptions to build a tool, and make sure those assumptions are valid when using the tool.

The idea, then, would be to create a script that reads the patch from the file named by its first argument and edits the patch in place; and set that script as the editor (i.e. by setting the GIT_EDITOR environment variable) when running git add -e. You would use lines of the form @@ -#,# +#,# @@ to figure out the affected line numbers for a change, use that information (and your assumptions) to decide if you want to keep or discard a given change line, and if you want to discard it

  • if the line starts with -, change the - to a
  • if the line starts with +, delete the line
Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • The patch in your example would be the diff and not the patch to stage line 2-4 unless I'm misunderstanding something. – CervEd Mar 13 '21 at 19:57
2

I have looked into this but not extensively. My understanding is that the way git add -p works is by generating patches, providing an intuitive interface to editing them and then applying them.

You could do this process yourself.

The changes in your working tree is provided by git diff. These are changes that can be applied to your index as a patch using git apply

So instead of git add . you could run

git diff . | git apply - --cached

Ie. get all the difference in your working tree at the current directory and apply all them to the index.

So what you could do is modify the output of the diff yourself and producing a different patch before applying it, using scripting or whatever method.

It may be that the process of modifying this using scripting is trickier than opting for a git add -e or git add -p solution but as far as I gather, this is the way to approach the problem if you want to solve it differently.

Addendum:

This is basically exactly what the interactive git commands do, using perl scripts. It creates chunked diff files, then opens them in $EDITOR. Importantly it automatically fixes the diff format after you've done modification (very annoying to do yourself), because the line summary stuff at the top of a diff ++ XYZ / -- zyx needs to be correct for the patch to apply.

I don't remember what command the perl script runs to chunk the diff, but it's in source somewhere.

If I had to solve this problem today I would probably generate the chunked diffs. Delete the ones outside of the line. Then git appy foo.diff --cached && git restore --staged foo.diff && git add -p.

CervEd
  • 3,306
  • 28
  • 25