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

Already on GitHub? Sign in to your account

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

Peeja opened this Issue Dec 9, 2011 · 15 comments


None yet
7 participants

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

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

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.

This was referenced Mar 14, 2014

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.

Something I'm doing as a workaround for now:

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

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?

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.

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?'
    execute 'lcd ' . l:toplevel
  let l:files = system(a:find . " | grep -E " . shellescape(a:regex))
  if (v:shell_error)
    echomsg 'No matching files.'
  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()
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 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 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)

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment