14

When I interactively add diff hunks with git add --patch, I sometimes get hunks which are longer than the screen, but no ability to use less to page through the hunks.

This is strange to me as I have already set:

[core]
      pager = less -FRX --tabs=4

[pager]

  diff = diff-highlight | less -FRX --tabs=4

interactive.diffFilter= piped through less doesn't help with paging either.

What do I need to do to get git add--patch to use less such that I can use the keyboard to navigate any output longer than one screen?

Tom Hale
  • 40,825
  • 36
  • 187
  • 242

3 Answers3

5

You can't. It's the nature of the Unix pipeline model.

The reason that Git commands that use pagers can work with any generic stdin pager is because of this model. Such git commands write their output to stdout. The pager reads its input from stdin. The stdout of the former is piped to the stdin of the latter. It is precisely the simplicity of this pipeline model that makes pagers generic and allows git to allow you to choose your own pager, as long as it uses this model.

So why can't git add -p (or git add -i) do the same as git diff or git log?

Because there can only be one interactive app at a time.

git diff and git log are not interactive. git add -p and pagers are interactive.

The nature of the pipeline model means that only one application can be in control at a time, and an interactive app needs to be in control. For the pager to gain control of the terminal (so that it can display prompts and respond to your input), git add -p has to release control. Once it does it cannot get it back.

Look at it this way: There would be two command line prompts trying to interact with you: the one for git add -p and the one for the pager. How would they coordinate? It would have to go something like this:

  1. git add -p writes a hunk to stdout, along with an end-of-hunk (EOH) marker instead of the usual end-of-file (EOF) marker.
  2. git add -p then yields control of the terminal to whatever app is at the other end of the pipe.
  3. The pager would receive the hunk, and with control of the terminal displays pieces of the hunk along with its command prompts.
  4. The pager would behave like it usually does but with a big difference. Usually it sees an EOF marker, so when you say you're done (the quit command), it exits. But the EOH maker tells the pager, "Don't exit. When the user is done, hand control back to the upstream app. Do not quit. Wait."
  5. So when you are done perusing the hunk with the various pager commands, you would use its quit command to tell it you're done like you usually do.
  6. But now instead of exiting, the pager somehow hands terminal control back to git add.
  7. git add's terminal prompt would then replace the pager's...
  8. ...and now we're back to step 1. Keep repeating until EOF.

As you can see, not only is this a bad user experience (using the pager's quit command to get back to git add on each hunk), it would totally undermine destroy the power and beauty of the Unix pipeline model.

It is for this same reason that git add -p cannot use diff-so-fancy

The only way for git -p to have pager like behavior is to have one built-in, or to define a "Git Pager API" and then we wait for people to write pagers that work with this API. This is the plugin model, which is very different from the pipeline model. It also means a tight integration: The git add -p commands and the pager commands would have to be combined and made available at each command prompt.

Use your terminal application's paging

I find it easy enough to scroll up in my terminal window. Mine has keyboard commands that allow me to move line by line or page by page.

Use git add -p's split command

Have you considered using git add -p's split command to break up the hunks? I find smaller hunks much easier to reason with anyway!

Inigo
  • 12,186
  • 5
  • 41
  • 70
  • Your answer seems to assume that two applications would need to be reading from the terminal at the same time. I don't see that this is the case: Why couldn't `git` run ` | less --quit-if-one-screen` to display the diff, wait for `less` to exit, then print the patch menu? – Tom Hale Aug 30 '20 at 15:50
  • You're absolutely right. It could. `git add -p` could invoke a pager as a subprocess on individual hunks rather than just pipe all of its output to it. So this will almost certainly require changes to git's [add-patch.c](https://github.com/git/git/blob/4f0a8be78499454eac3985b6e7e144b8376ab0a5/add-patch.c). I don't have the time the moment to take a look. In the meantime, try `less`'s own ability to invoke a subprocess to see if such UX is acceptable. – Inigo Aug 30 '20 at 18:07
  • For example, `less` something long, then within less use `m` to mark some point in the file, then `|` then your mark letter, then `less -FN` to invoke less within less. See how that works with both short and long bits marked in the outer less process. I'll update my answer when I can after I hear back from you. – Inigo Aug 30 '20 at 18:07
  • I don't follow your intention with invoking `less` within `less`... `less` isn't being invoked even once by `git`. – Tom Hale Aug 31 '20 at 11:14
  • because you're asking it to, either directly or indirectly within a subprocess. "Why couldn't `git` run ` | less --quit-if-one-screen` to display the diff, wait for `less` to exit, then print the patch menu?" – Inigo Aug 31 '20 at 15:30
  • I want to be able to control less from the keyboard. I've clarified this in the question. – Tom Hale Aug 31 '20 at 19:51
  • Doesn't look like you understood what I was asking above. As part of my looking into this I kicked off [a discussion on Git's repo on GitHub](https://github.com/so-fancy/diff-so-fancy/issues/35#issuecomment-683399156), which you discovered probably via my link above, so I hope you at least appreciate my contribution to getting this moving after 4 years of nothing. Even if I wasn't entirely accurate in my first cut at the answer, I was right that it will take a change to how `git add -p` works, and that you can't just naively attach a pager to it for the reason I gave. – Inigo Sep 03 '20 at 06:37
  • 1
    The two answers you accepted you have to admit are hacks and result in a poor UX as I mentioned in my answer. – Inigo Sep 03 '20 at 06:44
  • Your link isn't to git's repo - dsf is orthogonal to git as was printed out there. I do appreciate you linking to the perl source – Tom Hale Sep 03 '20 at 12:02
1

As a workaround you can set EDITOR=less and use e (edit) to run less on large hunks. But as-is that workaround has some disadvantages. Those can be avoided with something like this:

EDITOR="EDITOR='$EDITOR' bash -c 'set -m; less -K \"\$1\"' --" git add -p
  • Resetting EDITOR before invoking less allows using the standard v key in less to invoke the editor.

  • The -K option to less allows quitting less with Control-C, telling Git to not stage the hunk. Exiting less with q will cause the hunk to be staged.

  • set -m creates a separate process group which stops Control-C from bubbling up and killing the Git process.

There are also tools such as Magit that provide a better interface for interactive staging

Etienne Laurin
  • 6,731
  • 2
  • 27
  • 31
1

Based on AtnNn's answer, I came up with the following alias:

ap = !"VISUAL=\"VISUAL='$VISUAL' bash -c 'set -m; less -K -+F \\\"\\$1\\\"' --\" git add -p \"$@\" #"
  • Press e to page the hunk in less
    • Press q to stage what was displayed
    • Press v to edit what is displayed
    • Press ^C to quit and repeat the interactive menu

I'm working on a PR to fix this in git itself.

Tom Hale
  • 40,825
  • 36
  • 187
  • 242