Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easy way to move between diffs (Unimpaired-style) #132

Closed
Peeja opened this issue Dec 9, 2011 · 41 comments
Closed

Easy way to move between diffs (Unimpaired-style) #132

Peeja opened this issue Dec 9, 2011 · 41 comments
Labels
enhancement New feature or request

Comments

@Peeja
Copy link

Peeja commented Dec 9, 2011

When I'm staging a commit, I usually open :Gstatus and run down the diffs (with D) staging things. Moving to the next diff is a complicated procedure, something like ^WP^ND. It feels ripe for an Unimpaired-style binding, such as ]g to go to the next diff, and [g to go to the previous. Looking at fugitive.vim, though, I can't decide how best to implement such a thing. Unlike the Quickfix List, for instance, there's no notion of a "cursor" or "current file" in the :Gstatus window except for the actual cursor, which only exists when the buffer is open, and is ambiguous if it's open in multiple windows.

Any thoughts?

@tpope
Copy link
Owner

tpope commented Dec 9, 2011

I've been thinking of adding a way to load the dirty files into the quickfix list. Then you'd just need some map like only|cnext|Gdiff. My main question is what the interface to it would be. In particular, would there be a way to request only staged/unstaged/untracked files?

@Peeja
Copy link
Author

Peeja commented Dec 10, 2011

It sounds like the more general case is loading a changeset into the quickfix list. That could be the changes not in the index (git diff), the changes in the index (git diff --cached), or the changes in a commit (git show <commit>). The latter would be a nice alternative to looking at the commit directly: you'd have more context than the unified diff.

However, :Gdiff on a file from a commit diffs it against the working copy, so only|cnext|Gdiff wouldn't work for that general case. But maybe the idea shakes a thought loose?

@tpope
Copy link
Owner

tpope commented Mar 9, 2012

Not really. It has occurred to me that we could use the quickfix "message" field to store the filename to diff against. Then some command could retrieve that message and use it to load the diff. Question is, how do we retrieve that message? getqflist() gets us the entire quickfix list, but there's not really a good way to determine which one we're on. :/

@Peeja
Copy link
Author

Peeja commented Mar 10, 2012

Well, if there's only one entry per file, we can find the only entry for the newly-activated buffer.

@tpope
Copy link
Owner

tpope commented Mar 10, 2012

I'd rather decouple and have a method for "load the original for the current quickfix entry" rather than some monster workflow map. Although it might be okay to have :Gdiff check the quickfix list and use it for the default if it's present.

@mattboehm
Copy link

👍 How about 3 commands for loading unstaged, loading untracked, and loading both?

I agree that the best approach is commands to populate the quickfix and leaving the rest to the user.

I'm happy to work on this if you agree with this approach.

@tpope
Copy link
Owner

tpope commented Jan 9, 2014

As discussed on IRC, I'm still super hesitant about the original use case, but I'm on board with a much simpler git ls-files wrapper. @mattboehm will investigate.

@mattboehm
Copy link

I've been thinking about this a lot and am debating the merits of a more generic command that accepts any git command as an argument and treats the output as files for the quickfix list.

The main use case I've thought of for this would be git diff --name-only so that you could list all the files changed between two refs.

Please feel free to chime in with any thoughts on this. I've discussed it with @qstrahl and he and/or I might play around with a few possible implementations.

@mattboehm
Copy link

For what it's worth, here's a gist with what I have in my dotfiles to do this currently: https://gist.github.com/mattboehm/9977950 . It's very similar to what tpope suggested initially: Splits into a new tab and nukes any other windows in the tab any time you go to next/prev diff.

It would take a bit of work to extend this to work for #425 but not too much.

In the meantime, I'll try to make a PR for this story soon. Sorry for the delayed response.

@datalogics-kam
Copy link

Something I'm doing as a workaround for now:

$ git checkout -b compare
$ git reset the-branch-to-compare-to
$ vim 
:Gstatus
Look at all the diffs
Maybe make some changes to touch things up
:qa
$ git checkout the-original-branch
$ git commit the changes

@tpope
Copy link
Owner

tpope commented Sep 14, 2015

Prompted by an email I've started to think about this anew. My unmerged files use case has since been addressed by :Gmerge/:Gpull, so we can set that aside. I am thinking a wrapper around git diff --name-status is probably the way to go. Load the results into the quickfix list, with the second file jammed into the description text, and then change :Gdiff to check the quickfix list for its default. You could build a higher level cycling abstraction on top of that pretty easily, though I'm not sure that's a good candidate for inclusion. For now, let's focus on the options available for populating the quickfix list.

Between no argument, --cached, HEAD, and commit1..commit2 - plus a subset of additional git diff arguments such as --find-renames - this covers a ton of use cases. It doesn't handle untracked files, which I guess isn't the end of the world. Anything else we're failing to cover?

@mattboehm
Copy link

So as I understand it, the steps for the original use case would be:

  • run :Gcompare (or whatever the alias is called)
  • quickfix is populated with the changed files. The file component is a fugitive url including the commit1, the description includes commit2
    • Is the first file in the list loaded or not?
  • open the first file and run :Gdiff
  • Gdiff checks the quickfix list, realizes it's in a particular format, uses commit1/commit2 from the entry for diffing
  • when done, run :cn then Gdiff to view the next

That seems pretty reasonable to me, although I think that overloading :Gdiff could potentially cause confusion. If a user runs the command, hides the quickfix list and 10 minutes later decides they want to do a :Gdiff, perhaps they'd be surprised when it loads the 2 commits from the quickfix list. Maybe not, though. If loading a quickfix entry creates a buffer with a long fugitive url, and :Gdiff only does this special behavior when the url matches the file component of the quickfix entry, then perhaps that will be clear enough.

For what it's worth, you could try to include both commit ID's into 1 fugitive url so that Gdiff doesn't need to check the quickfix list at all. Or maybe when opening a url with two commits, fugitive would automatically display them side-by-side and potentially diff them. Granted, this is a bit hackish and may be specific enough to make other more general use cases painful.

@cirosantilli
Copy link

Gfiles workaround implementation until something gets merged :-)

Usage described at: http://stackoverflow.com/a/36190738/895245

function! Find(regex, find, git_toplevel)
  if (a:git_toplevel)
    let l:toplevel = system('git rev-parse --show-toplevel')
    if (v:shell_error)
      echomsg 'Not in a Git repo?'
      return
    endif
    execute 'lcd ' . l:toplevel
  endif
  let l:files = system(a:find . " | grep -E " . shellescape(a:regex))
  if (v:shell_error)
    echomsg 'No matching files.'
    return
  endif
  tabedit
  set filetype=filelist
  silent file [filelist]
  set buftype=nofile
  put =l:files
  normal ggdd
  nnoremap <buffer> <Enter> <C-W>gf
  execute 'autocmd BufEnter <buffer> lcd ' . getcwd()
endfunction
command! -nargs=? Find call Find('<args>', 'find . -type f', 0)
command! -nargs=? Gfind call Find('<args>', 'git ls-files', 0)
command! -nargs=? Gtfind call Find('<args>', 'git ls-files', 1)

@frioux
Copy link
Contributor

frioux commented May 30, 2016

I did this with a little perl script and a couple mappings. Mine loads the quickfix with all of the hunks instead of each file, which was more what I wanted. If anyone is interested you can read about it here

@eloytoro
Copy link

eloytoro commented Mar 31, 2017

Wrote this small snippet to be able to diff branches, doesn't use fugitive

" ----------------------------------------------------------------------------
" DiffRev
" ----------------------------------------------------------------------------
let s:git_status_dictionary = {
            \ "A": "Added",
            \ "B": "Broken",
            \ "C": "Copied",
            \ "D": "Deleted",
            \ "M": "Modified",
            \ "R": "Renamed",
            \ "T": "Changed",
            \ "U": "Unmerged",
            \ "X": "Unknown"
            \ }
function! s:get_diff_files(rev)
  let list = map(split(system(
              \ 'git diff --name-status '.a:rev), '\n'),
              \ '{"filename":matchstr(v:val, "\\S\\+$"),"text":s:git_status_dictionary[matchstr(v:val, "^\\w")]}'
              \ )
  call setqflist(list)
  copen
endfunction

command! -nargs=1 DiffRev call s:get_diff_files(<q-args>)

Ideally @tpope would add a gstatus-esque window that would instantly diff revisions

@bam80
Copy link

bam80 commented Mar 16, 2019

I've been thinking of adding a way to load the dirty files into the quickfix list. Then you'd just need some map like only|cnext|Gdiff. My main question is what the interface to it would be. In particular, would there be a way to request only staged/unstaged/untracked files?

Could we traverse through the selection of the staged/unstaged/untracked files in :Gstatus window maybe?

@gauteh
Copy link

gauteh commented Mar 16, 2019

Wrote this small snippet to be able to diff branches, doesn't use fugitive

This is very useful for doing code reviews. Added a Gcd call to the beginning of the function, and add a map to :Gdiff a:rev so that the function works regardless of cwd and it is easy to open a diff against the rev which the diff has been done against. What I am missing now is a way to easily move to the next file in the quickfix window and automatically open the diff there (so that I don´t have to do: <C-w-c>]q\gr to move to the next diff). Gitgutter supported a base_branch type variable so that it would be easy to move between the hunks, but signify which I use now do not. This would also be very useful. My modifications.

@bzinberg
Copy link

bzinberg commented Jan 3, 2020

@bam80, do you know if there is currently an easier way to move from Gdiff(file1) to Gdiff(file2) than just doing :diffoff, closing one of the split windows, opening file2 in the remaining window, and running :Gdiff?

@gauteh
Copy link

gauteh commented Jan 3, 2020 via email

@tpope
Copy link
Owner

tpope commented Jan 3, 2020

I'll need that information as well to finish difftool -y. Once I have it, it could potentially be stored in :help quickfix-context or something like that. Implementation would still require custom commands like :Cnext since the built-ins don't trigger an event. I don't want to take responsibility for that but if you want to, knock yourself out.

tpope added a commit that referenced this issue Jan 5, 2020
tpope added a commit that referenced this issue Jan 5, 2020
tpope added a commit that referenced this issue Jan 5, 2020
@tpope
Copy link
Owner

tpope commented Jan 5, 2020

What to diff against is now available in the quickfix context. Here's a simple usage example.

function DiffCurrentQuickfixEntry() abort
  cc
  let qf = getqflist({'context': 0, 'idx': 0})
  if get(qf, 'idx') && type(get(qf, 'context')) == type({}) && type(get(qf.context, 'items')) == type([])
    let diff = get(qf.context.items[qf.idx - 1], 'diff', [])
    for i in reverse(range(len(diff)))
      exe (i ? 'rightbelow' : 'leftabove') 'vert diffsplit' fnameescape(diff[i].filename)
      wincmd p
    endfor
  endif
endfunction

tpope added a commit that referenced this issue Jan 5, 2020
tpope added a commit that referenced this issue Jan 5, 2020
tpope added a commit that referenced this issue Jan 5, 2020
tpope added a commit that referenced this issue Jan 5, 2020
tpope added a commit that referenced this issue Jan 5, 2020
@tpope tpope closed this as completed in ddd64fc Jan 6, 2020
@kristijanhusak
Copy link

What to diff against is now available in the quickfix context. Here's a simple usage example.

function DiffCurrentQuickfixEntry() abort
  cc
  let qf = getqflist({'context': 0, 'idx': 0})
  if get(qf, 'idx') && type(get(qf, 'context')) == type({}) && type(get(qf.context, 'items')) == type([])
    let diff = get(qf.context.items[qf.idx - 1], 'diff', [])
    for i in reverse(range(len(diff)))
      exe (i ? 'rightbelow' : 'leftabove') 'vert diffsplit' fnameescape(diff[i].filename)
      wincmd p
    endfor
  endif
endfunction

Will this ever be part of the plugin as a mapping or command?

It works, but it's a bit problematic if you do :cn and try to do diff again. Some way to clean itself up before doing another diff would be great.

@bzinberg
Copy link

bzinberg commented Jan 6, 2020

Thanks @tpope, this is awesome!

@tpope
Copy link
Owner

tpope commented Jan 6, 2020

Will this ever be part of the plugin as a mapping or command?

Like I said:

I don't want to take responsibility for that but if you want to, knock yourself out.

I think it should start its life as an external addon. When the bugs are worked out, we can talk pull request.

@gauteh
Copy link

gauteh commented Apr 27, 2020

Just wanted to say: this is great!! thanks!

@bam80
Copy link

bam80 commented Apr 27, 2020

Will this ever be part of the plugin as a mapping or command?

I think it should start its life as an external addon. When the bugs are worked out, we can talk pull request.

Anyone brave? :)

@kristijanhusak
Copy link

Here's code that I use to achieve this:

command! DiffHistory call s:view_git_history()

function! s:view_git_history() abort
  Git difftool --name-only ! !^@
  call s:diff_current_quickfix_entry()
  " Bind <CR> for current quickfix window to properly set up diff split layout after selecting an item
  " There's probably a better way to map this without changing the window
  copen
  nnoremap <buffer> <CR> <CR><BAR>:call <sid>diff_current_quickfix_entry()<CR>
  wincmd p
endfunction

function s:diff_current_quickfix_entry() abort
  " Cleanup windows
  for window in getwininfo()
    if window.winnr !=? winnr() && bufname(window.bufnr) =~? '^fugitive:'
      exe 'bdelete' window.bufnr
    endif
  endfor
  cc
  call s:add_mappings()
  let qf = getqflist({'context': 0, 'idx': 0})
  if get(qf, 'idx') && type(get(qf, 'context')) == type({}) && type(get(qf.context, 'items')) == type([])
    let diff = get(qf.context.items[qf.idx - 1], 'diff', [])
    echom string(reverse(range(len(diff))))
    for i in reverse(range(len(diff)))
      exe (i ? 'leftabove' : 'rightbelow') 'vert diffsplit' fnameescape(diff[i].filename)
      call s:add_mappings()
    endfor
  endif
endfunction

function! s:add_mappings() abort
  nnoremap <buffer>]q :cnext <BAR> :call <sid>diff_current_quickfix_entry()<CR>
  nnoremap <buffer>[q :cprevious <BAR> :call <sid>diff_current_quickfix_entry()<CR>
  " Reset quickfix height. Sometimes it messes up after selecting another item
  11copen
  wincmd p
endfunction

Basically, after you select a commit you want to preview, call :DiffHistory, and it will show all changed files for that commit in a quickfix window, start a diff split, and add mappings for properly refreshing the windows after quickfix item is changed.
Note that this is tested only on latest Neovim.

@mosheavni
Copy link

Here's code that I use to achieve this:

command! DiffHistory call s:view_git_history()

function! s:view_git_history() abort
  Git difftool --name-only ! !^@
  call s:diff_current_quickfix_entry()
  " Bind <CR> for current quickfix window to properly set up diff split layout after selecting an item
  " There's probably a better way to map this without changing the window
  copen
  nnoremap <buffer> <CR> <CR><BAR>:call <sid>diff_current_quickfix_entry()<CR>
  wincmd p
endfunction

function s:diff_current_quickfix_entry() abort
  " Cleanup windows
  for window in getwininfo()
    if window.winnr !=? winnr() && bufname(window.bufnr) =~? '^fugitive:'
      exe 'bdelete' window.bufnr
    endif
  endfor
  cc
  call s:add_mappings()
  let qf = getqflist({'context': 0, 'idx': 0})
  if get(qf, 'idx') && type(get(qf, 'context')) == type({}) && type(get(qf.context, 'items')) == type([])
    let diff = get(qf.context.items[qf.idx - 1], 'diff', [])
    echom string(reverse(range(len(diff))))
    for i in reverse(range(len(diff)))
      exe (i ? 'leftabove' : 'rightbelow') 'vert diffsplit' fnameescape(diff[i].filename)
      call s:add_mappings()
    endfor
  endif
endfunction

function! s:add_mappings() abort
  nnoremap <buffer>]q :cnext <BAR> :call <sid>diff_current_quickfix_entry()<CR>
  nnoremap <buffer>[q :cprevious <BAR> :call <sid>diff_current_quickfix_entry()<CR>
  " Reset quickfix height. Sometimes it messes up after selecting another item
  11copen
  wincmd p
endfunction

Basically, after you select a commit you want to preview, call :DiffHistory, and it will show all changed files for that commit in a quickfix window, start a diff split, and add mappings for properly refreshing the windows after quickfix item is changed.
Note that this is tested only on latest Neovim.

This is amazing! @kristijanhusak is there a way to diff with a certain commit rather than just the last one?

@kristijanhusak
Copy link

@MosheM123 this is for any commit. Just select a commit from Glog, and after you selected it, run the command.

@vizcay
Copy link

vizcay commented Jul 13, 2021

It occurred to me a couple of months ago that the appropriate interface for this is :Git difftool, because git difftool is Git's interface to loading changes into an editor. I'm pushing up my work-in-progress for this to the difftool branch. Please do try it out and see if you can break it. My goal is 100% compatibility with git difftool: any valid argument list should work, and any invalid argument list should give the same error message that Git would.

I was trying to get away from :G status to a way to load modified files in my quickfix list where I find :cn and :cp great for navigation when I've found this issue. The only problem I have is that it doesn't lists untracked files there (where in :G status I have)..

I wasn't able to find a workaround for this, as the typical solution in your shell is to use git ls-files --exclude-estandard -o with some xargs magic..

Anybody found a workaround for this? I wil really apreciate it.

@tpope
Copy link
Owner

tpope commented Jul 13, 2021

Git's own solution to this is git add --intent-to-add aka git add -N.

benknoble added a commit to benknoble/Dotfiles that referenced this issue May 1, 2024
This cribs code from
tpope/vim-fugitive#132 (comment)
to make difftool and mergetool a bit more useful.

Note that git difftool launching "Git difftool" is not that helpful,
since the arguments aren't forwarded.
@jecaro
Copy link

jecaro commented Nov 15, 2024

Based on this discussion, I came up with a small plugin for neovim. Check it out, it's called fugitive-difftool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests