contrib/vim/patchreview.vim
author Augie Fackler <augie@google.com>
Mon, 11 Nov 2019 16:45:22 -0500
changeset 43615 6f5c352f41b6
parent 30332 318a24b52eeb
permissions -rw-r--r--
fuzz: clean out most of fuzzutil It's now a header-only setup that just selects absl:: or std:: versions of things as needed, and a logging helper. There's some room for future cleanups here: we could move to just requiring a C++17 compiler and get rid of the absl stuff. Also, the mpatch parser has a fair amount of parsing the input string into char* blocks that we can and probably should fix that up to use FuzzedDataProvider as well. Differential Revision: https://phab.mercurial-scm.org/D7370

" VIM plugin for doing single, multi-patch or diff code reviews {{{
" Home:  http://www.vim.org/scripts/script.php?script_id=1563

" Version       : 0.2.2                                        "{{{
" Author        : Manpreet Singh < junkblocker@yahoo.com >
" Copyright     : 2006-2010 by Manpreet Singh
" License       : This file is placed in the public domain.
"                 No warranties express or implied. Use at your own risk.
"
" Changelog :
"
"   0.2.2 - Security fixes by removing custom tempfile creation
"         - Removed need for DiffReviewCleanup/PatchReviewCleanup
"         - Better command execution error detection and display
"         - Improved diff view and folding by ignoring modelines
"         - Improved tab labels display
"
"   0.2.1 - Minor temp directory autodetection logic and cleanup
"
"   0.2 - Removed the need for filterdiff by implementing it in pure vim script
"       - Added DiffReview command for reverse (changed repository to
"         pristine state) reviews.
"         (PatchReview does pristine repository to patch review)
"       - DiffReview does automatic detection and generation of diffs for
"         various Source Control systems
"       - Skip load if VIM 7.0 or higher unavailable
"
"   0.1 - First released
"}}}

" Documentation:                                                         "{{{
" ===========================================================================
" This plugin allows single or multiple, patch or diff based code reviews to
" be easily done in VIM. VIM has :diffpatch command to do single file reviews
" but a) can not handle patch files containing multiple patches or b) do
" automated diff generation for various version control systems. This plugin
" attempts to provide those functionalities. It opens each changed / added or
" removed file diff in new tabs.
"
" Installing:
"
"   For a quick start, unzip patchreview.zip into your ~/.vim directory and
"   restart Vim.
"
" Details:
"
"   Requirements:
"
"   1) VIM 7.0 or higher built with +diff option.
"
"   2) A gnu compatible patch command installed. This is the standard patch
"      command on Linux, Mac OS X, *BSD, Cygwin or /usr/bin/gpatch on newer
"      Solaris.
"
"   3) Optional (but recommended for speed)
"
"      Install patchutils ( http://cyberelk.net/tim/patchutils/ ) for your
"      OS. For windows it is available from Cygwin
"
"         http://www.cygwin.com
"
"      or GnuWin32
"
"         http://gnuwin32.sourceforge.net/
"
"   Install:
"
"   1) Extract the zip in your $HOME/.vim or $VIM/vimfiles directory and
"      restart vim. The  directory location relevant to your platform can be
"      seen by running :help add-global-plugin in vim.
"
"   2) Restart vim.
"
"  Configuration:
"
"  Optionally, specify the locations to these filterdiff and patch commands
"  and location of a temporary directory to use in your .vimrc.
"
"      let g:patchreview_patch       = '/path/to/gnu/patch'
"
"      " If you are using filterdiff
"      let g:patchreview_filterdiff  = '/path/to/filterdiff'
"
"
" Usage:
"
"  Please see :help patchreview or :help diffreview for details.
"
""}}}

" Enabled only during development
" unlet! g:loaded_patchreview " DEBUG
" unlet! g:patchreview_patch " DEBUG
" unlet! g:patchreview_filterdiff " DEBUG
" let g:patchreview_patch = 'patch'    " DEBUG

if v:version < 700
  finish
endif
if ! has('diff')
  call confirm('patchreview.vim plugin needs (G)VIM built with +diff support to work.')
  finish
endif

" load only once
if (! exists('g:patchreview_debug') && exists('g:loaded_patchreview')) || &compatible
  finish
endif
let g:loaded_patchreview="0.2.2"

let s:msgbufname = '-PatchReviewMessages-'

function! <SID>Debug(str)                                                 "{{{
  if exists('g:patchreview_debug')
    Pecho 'DEBUG: ' . a:str
  endif
endfunction
command! -nargs=+ -complete=expression Debug call s:Debug(<args>)
"}}}

function! <SID>PR_wipeMsgBuf()                                            "{{{
  let winnum = bufwinnr(s:msgbufname)
  if winnum != -1 " If the window is already open, jump to it
    let cur_winnr = winnr()
    if winnr() != winnum
      exe winnum . 'wincmd w'
      exe 'bw'
      exe cur_winnr . 'wincmd w'
    endif
  endif
endfunction
"}}}

function! <SID>Pecho(...)                                                 "{{{
  " Usage: Pecho(msg, [return_to_original_window_flag])
  "            default return_to_original_window_flag = 0
  "
  let cur_winnr = winnr()
  let winnum = bufwinnr(s:msgbufname)
  if winnum != -1 " If the window is already open, jump to it
    if winnr() != winnum
      exe winnum . 'wincmd w'
    endif
  else
    let bufnum = bufnr(s:msgbufname)
    if bufnum == -1
      let wcmd = s:msgbufname
    else
      let wcmd = '+buffer' . bufnum
    endif
    exe 'silent! botright 5split ' . wcmd
  endif
  setlocal modifiable
  setlocal buftype=nofile
  setlocal bufhidden=delete
  setlocal noswapfile
  setlocal nowrap
  setlocal nobuflisted
  if a:0 != 0
    silent! $put =a:1
  endif
  exe ':$'
  setlocal nomodifiable
  if a:0 > 1 && a:2
    exe cur_winnr . 'wincmd w'
  endif
endfunction

command! -nargs=+ -complete=expression Pecho call s:Pecho(<args>)
"}}}

function! <SID>PR_checkBinary(BinaryName)                                 "{{{
  " Verify that BinaryName is specified or available
  if ! exists('g:patchreview_' . a:BinaryName)
    if executable(a:BinaryName)
      let g:patchreview_{a:BinaryName} = a:BinaryName
      return 1
    else
      Pecho 'g:patchreview_' . a:BinaryName . ' is not defined and ' . a:BinaryName . ' command could not be found on path.'
      Pecho 'Please define it in your .vimrc.'
      return 0
    endif
  elseif ! executable(g:patchreview_{a:BinaryName})
    Pecho 'Specified g:patchreview_' . a:BinaryName . ' [' . g:patchreview_{a:BinaryName} . '] is not executable.'
    return 0
  else
    return 1
  endif
endfunction
"}}}

function! <SID>ExtractDiffsNative(...)                                    "{{{
  " Sets g:patches = {'reason':'', 'patch':[
  " {
  "  'filename': filepath
  "  'type'    : '+' | '-' | '!'
  "  'content' : patch text for this file
  " },
  " ...
  " ]}
  let g:patches = {'reason' : '', 'patch' : []}
  " TODO : User pointers into lines list rather then use collect
  if a:0 == 0
    let g:patches['reason'] = "ExtractDiffsNative expects at least a patchfile argument"
    return
  endif
  let patchfile = expand(a:1, ':p')
  if a:0 > 1
    let patch = a:2
  endif
  if ! filereadable(patchfile)
    let g:patches['reason'] = "File " . patchfile . " is not readable"
    return
  endif
  unlet! filterdiffcmd
  let filterdiffcmd = '' . g:patchreview_filterdiff . ' --list -s ' . patchfile
  let fileslist = split(system(filterdiffcmd), '[\r\n]')
  for filewithchangetype in fileslist
    if filewithchangetype !~ '^[!+-] '
      Pecho '*** Skipping review generation due to unknown change for [' . filewithchangetype . ']'
      continue
    endif

    unlet! this_patch
    let this_patch = {}

    unlet! relpath
    let relpath = substitute(filewithchangetype, '^. ', '', '')

    let this_patch['filename'] = relpath

    if filewithchangetype =~ '^! '
      let this_patch['type'] = '!'
    elseif filewithchangetype =~ '^+ '
      let this_patch['type'] = '+'
    elseif filewithchangetype =~ '^- '
      let this_patch['type'] = '-'
    endif

    unlet! filterdiffcmd
    let filterdiffcmd = '' . g:patchreview_filterdiff . ' -i ' . relpath . ' ' . patchfile
    let this_patch['content'] = split(system(filterdiffcmd), '[\n\r]')
    let g:patches['patch'] += [this_patch]
    Debug "Patch collected for " . relpath
  endfor
endfunction
"}}}

function! <SID>ExtractDiffsPureVim(...)                                   "{{{
  " Sets g:patches = {'reason':'', 'patch':[
  " {
  "  'filename': filepath
  "  'type'    : '+' | '-' | '!'
  "  'content' : patch text for this file
  " },
  " ...
  " ]}
  let g:patches = {'reason' : '', 'patch' : []}
  " TODO : User pointers into lines list rather then use collect
  if a:0 == 0
    let g:patches['reason'] = "ExtractDiffsPureVim expects at least a patchfile argument"
    return
  endif
  let patchfile = expand(a:1, ':p')
  if a:0 > 1
    let patch = a:2
  endif
  if ! filereadable(patchfile)
    let g:patches['reason'] = "File " . patchfile . " is not readable"
    return
  endif
  call s:PR_wipeMsgBuf()
  let collect = []
  let linum = 0
  let lines = readfile(patchfile)
  let linescount = len(lines)
  State 'START'
  while linum < linescount
    let line = lines[linum]
    let linum += 1
    if State() == 'START'
      let mat = matchlist(line, '^--- \([^\t]\+\).*$')
      if ! empty(mat) && mat[1] != ''
        State 'MAYBE_UNIFIED_DIFF'
        let p_first_file = mat[1]
        let collect = [line]
        Debug line . State()
        continue
      endif
      let mat = matchlist(line, '^\*\*\* \([^\t]\+\).*$')
      if ! empty(mat) && mat[1] != ''
        State 'MAYBE_CONTEXT_DIFF'
        let p_first_file = mat[1]
        let collect = [line]
        Debug line . State()
        continue
      endif
      continue
    elseif State() == 'MAYBE_CONTEXT_DIFF'
      let mat = matchlist(line, '^--- \([^\t]\+\).*$')
      if empty(mat) || mat[1] == ''
        State 'START'
        let linum -= 1
        continue
        Debug 'Back to square one ' . line()
      endif
      let p_second_file = mat[1]
      if p_first_file == '/dev/null'
        if p_second_file == '/dev/null'
          let g:patches['reason'] = "Malformed diff found at line " . linum
          return
        endif
        let p_type = '+'
        let filepath = p_second_file
      else
        if p_second_file == '/dev/null'
          let p_type = '-'
          let filepath = p_first_file
        else
          let p_type = '!'
          let filepath = p_first_file
        endif
      endif
      State 'EXPECT_15_STARS'
      let collect += [line]
      Debug line . State()
    elseif State() == 'EXPECT_15_STARS'
      if line !~ '^*\{15}$'
        State 'START'
        let linum -= 1
        Debug line . State()
        continue
      endif
      State 'EXPECT_CONTEXT_CHUNK_HEADER_1'
      let collect += [line]
      Debug line . State()
    elseif State() == 'EXPECT_CONTEXT_CHUNK_HEADER_1'
      let mat = matchlist(line, '^\*\*\* \(\d\+,\)\?\(\d\+\) \*\*\*\*$')
      if empty(mat) || mat[1] == ''
        State 'START'
        let linum -= 1
        Debug line . State()
        continue
      endif
      let collect += [line]
      State 'SKIP_CONTEXT_STUFF_1'
      Debug line . State()
      continue
    elseif State() == 'SKIP_CONTEXT_STUFF_1'
      if line !~ '^[ !+].*$'
        let mat = matchlist(line, '^--- \(\d\+\),\(\d\+\) ----$')
        if ! empty(mat) && mat[1] != '' && mat[2] != ''
          let goal_count = mat[2] - mat[1] + 1
          let c_count = 0
          State 'READ_CONTEXT_CHUNK'
          let collect += [line]
          Debug line . State() . " Goal count set to " . goal_count
          continue
        endif
        State 'START'
        let linum -= 1
        Debug line . State()
        continue
      endif
      let collect += [line]
      continue
    elseif State() == 'READ_CONTEXT_CHUNK'
      let c_count += 1
      if c_count == goal_count
        let collect += [line]
        State 'BACKSLASH_OR_CRANGE_EOF'
        continue
      else " goal not met yet
        let mat = matchlist(line, '^\([\\!+ ]\).*$')
        if empty(mat) || mat[1] == ''
          let linum -= 1
          State 'START'
          Debug line . State()
          continue
        endif
        let collect += [line]
        continue
      endif
    elseif State() == 'BACKSLASH_OR_CRANGE_EOF'
      if line =~ '^\\ No newline.*$'   " XXX: Can we go to another chunk from here??
        let collect += [line]
        let this_patch = {}
        let this_patch['filename'] = filepath
        let this_patch['type'] = p_type
        let this_patch['content'] = collect
        let g:patches['patch'] += [this_patch]
        Debug "Patch collected for " . filepath
        State 'START'
        continue
      endif
      if line =~ '^\*\{15}$'
        let collect += [line]
        State 'EXPECT_CONTEXT_CHUNK_HEADER_1'
        Debug line . State()
        continue
      endif
      let this_patch = {}
      let this_patch['filename'] = filepath
      let this_patch['type'] = p_type
      let this_patch['content'] = collect
      let g:patches['patch'] += [this_patch]
      let linum -= 1
      State 'START'
      Debug "Patch collected for " . filepath
      Debug line . State()
      continue
    elseif State() == 'MAYBE_UNIFIED_DIFF'
      let mat = matchlist(line, '^+++ \([^\t]\+\).*$')
      if empty(mat) || mat[1] == ''
        State 'START'
        let linum -= 1
        Debug line . State()
        continue
      endif
      let p_second_file = mat[1]
      if p_first_file == '/dev/null'
        if p_second_file == '/dev/null'
          let g:patches['reason'] = "Malformed diff found at line " . linum
          return
        endif
        let p_type = '+'
        let filepath = p_second_file
      else
        if p_second_file == '/dev/null'
          let p_type = '-'
          let filepath = p_first_file
        else
          let p_type = '!'
          let filepath = p_first_file
        endif
      endif
      State 'EXPECT_UNIFIED_RANGE_CHUNK'
      let collect += [line]
      Debug line . State()
      continue
    elseif State() == 'EXPECT_UNIFIED_RANGE_CHUNK'
      let mat = matchlist(line, '^@@ -\(\d\+,\)\?\(\d\+\) +\(\d\+,\)\?\(\d\+\) @@$')
      if ! empty(mat)
        let old_goal_count = mat[2]
        let new_goal_count = mat[4]
        let o_count = 0
        let n_count = 0
        Debug "Goal count set to " . old_goal_count . ', ' . new_goal_count
        State 'READ_UNIFIED_CHUNK'
        let collect += [line]
        Debug line . State()
        continue
      endif
      State 'START'
      Debug line . State()
      continue
    elseif State() == 'READ_UNIFIED_CHUNK'
      if o_count == old_goal_count && n_count == new_goal_count
        if line =~ '^\\.*$'   " XXX: Can we go to another chunk from here??
          let collect += [line]
          let this_patch = {}
          let this_patch['filename'] = filepath
          let this_patch['type'] = p_type
          let this_patch['content'] = collect
          let g:patches['patch'] += [this_patch]
          Debug "Patch collected for " . filepath
          State 'START'
          continue
        endif
        let mat = matchlist(line, '^@@ -\(\d\+,\)\?\(\d\+\) +\(\d\+,\)\?\(\d\+\) @@$')
        if ! empty(mat)
          let old_goal_count = mat[2]
          let new_goal_count = mat[4]
          let o_count = 0
          let n_count = 0
          Debug "Goal count set to " . old_goal_count . ', ' . new_goal_count
          let collect += [line]
          Debug line . State()
          continue
        endif
        let this_patch = {}
        let this_patch['filename'] = filepath
        let this_patch['type'] = p_type
        let this_patch['content'] = collect
        let g:patches['patch'] += [this_patch]
        Debug "Patch collected for " . filepath
        let linum -= 1
        State 'START'
        Debug line . State()
        continue
      else " goal not met yet
        let mat = matchlist(line, '^\([\\+ -]\).*$')
        if empty(mat) || mat[1] == ''
          let linum -= 1
          State 'START'
          continue
        endif
        let chr = mat[1]
        if chr == '+'
          let n_count += 1
        endif
        if chr == ' '
          let o_count += 1
          let n_count += 1
        endif
        if chr == '-'
          let o_count += 1
        endif
        let collect += [line]
        Debug line . State()
        continue
      endif
    else
      let g:patches['reason'] = "Internal error: Do not use the plugin anymore and if possible please send the diff or patch file you tried it with to Manpreet Singh <junkblocker@yahoo.com>"
      return
    endif
  endwhile
  "Pecho State()
  if (State() == 'READ_CONTEXT_CHUNK' && c_count == goal_count) || (State() == 'READ_UNIFIED_CHUNK' && n_count == new_goal_count && o_count == old_goal_count)
    let this_patch = {}
    let this_patch['filename'] = filepath
    let this_patch['type'] = p_type
    let this_patch['content'] = collect
    let g:patches['patch'] += [this_patch]
    Debug "Patch collected for " . filepath
  endif
  return
endfunction
"}}}

function! State(...)  " For easy manipulation of diff extraction state      "{{{
  if a:0 != 0
    let s:STATE = a:1
  else
    if ! exists('s:STATE')
      let s:STATE = 'START'
    endif
    return s:STATE
  endif
endfunction
com! -nargs=+ -complete=expression State call State(<args>)
"}}}

function! <SID>PatchReview(...)                                           "{{{
  let s:save_shortmess = &shortmess
  let s:save_aw = &autowrite
  let s:save_awa = &autowriteall
  set shortmess=aW
  call s:PR_wipeMsgBuf()
  let s:reviewmode = 'patch'
  call s:_GenericReview(a:000)
  let &autowriteall = s:save_awa
  let &autowrite = s:save_aw
  let &shortmess = s:save_shortmess
endfunction
"}}}

function! <SID>_GenericReview(argslist)                                   "{{{
  " diff mode:
  "   arg1 = patchfile
  "   arg2 = strip count
  " patch mode:
  "   arg1 = patchfile
  "   arg2 = strip count
  "   arg3 = directory

  " VIM 7+ required
  if version < 700
    Pecho 'This plugin needs VIM 7 or higher'
    return
  endif

  " +diff required
  if ! has('diff')
    Pecho 'This plugin needs VIM built with +diff feature.'
    return
  endif


  if s:reviewmode == 'diff'
    let patch_R_option = ' -t -R '
  elseif s:reviewmode == 'patch'
    let patch_R_option = ''
  else
    Pecho 'Fatal internal error in patchreview.vim plugin'
    return
  endif

  " Check passed arguments
  if len(a:argslist) == 0
    Pecho 'PatchReview command needs at least one argument specifying a patchfile path.'
    return
  endif
  let StripCount = 0
  if len(a:argslist) >= 1 && ((s:reviewmode == 'patch' && len(a:argslist) <= 3) || (s:reviewmode == 'diff' && len(a:argslist) == 2))
    let PatchFilePath = expand(a:argslist[0], ':p')
    if ! filereadable(PatchFilePath)
      Pecho 'File [' . PatchFilePath . '] is not accessible.'
      return
    endif
    if len(a:argslist) >= 2 && s:reviewmode == 'patch'
      let s:SrcDirectory = expand(a:argslist[1], ':p')
      if ! isdirectory(s:SrcDirectory)
        Pecho '[' . s:SrcDirectory . '] is not a directory'
        return
      endif
      try
        " Command line has already escaped the path
        exe 'cd ' . s:SrcDirectory
      catch /^.*E344.*/
        Pecho 'Could not change to directory [' . s:SrcDirectory . ']'
        return
      endtry
    endif
    if s:reviewmode == 'diff'
      " passed in by default
      let StripCount = eval(a:argslist[1])
    elseif s:reviewmode == 'patch'
      let StripCount = 1
      " optional strip count
      if len(a:argslist) == 3
        let StripCount = eval(a:argslist[2])
      endif
    endif
  else
    if s:reviewmode == 'patch'
      Pecho 'PatchReview command needs at most three arguments: patchfile path, optional source directory path and optional strip count.'
    elseif s:reviewmode == 'diff'
      Pecho 'DiffReview command accepts no arguments.'
    endif
    return
  endif

  " Verify that patch command and temporary directory are available or specified
  if ! s:PR_checkBinary('patch')
    return
  endif

  " Requirements met, now execute
  let PatchFilePath = fnamemodify(PatchFilePath, ':p')
  if s:reviewmode == 'patch'
    Pecho 'Patch file      : ' . PatchFilePath
  endif
  Pecho 'Source directory: ' . getcwd()
  Pecho '------------------'
  if s:PR_checkBinary('filterdiff')
    Debug "Using filterdiff"
    call s:ExtractDiffsNative(PatchFilePath)
  else
    Debug "Using own diff extraction (slower)"
    call s:ExtractDiffsPureVim(PatchFilePath)
  endif
  for patch in g:patches['patch']
    if patch.type !~ '^[!+-]$'
      Pecho '*** Skipping review generation due to unknown change [' . patch.type . ']', 1
      continue
    endif
    unlet! relpath
    let relpath = patch.filename
    " XXX: svn diff and hg diff produce different kind of outputs, one requires
    " XXX: stripping but the other doesn't. We need to take care of that
    let stripmore = StripCount
    let StrippedRelativeFilePath = relpath
    while stripmore > 0
      " strip one
      let StrippedRelativeFilePath = substitute(StrippedRelativeFilePath, '^[^\\\/]\+[^\\\/]*[\\\/]' , '' , '')
      let stripmore -= 1
    endwhile
    if patch.type == '!'
      if s:reviewmode == 'patch'
        let msgtype = 'Patch modifies file: '
      elseif s:reviewmode == 'diff'
        let msgtype = 'File has changes: '
      endif
    elseif patch.type == '+'
      if s:reviewmode == 'patch'
        let msgtype = 'Patch adds file    : '
      elseif s:reviewmode == 'diff'
        let msgtype = 'New file        : '
      endif
    elseif patch.type == '-'
      if s:reviewmode == 'patch'
        let msgtype = 'Patch removes file : '
      elseif s:reviewmode == 'diff'
        let msgtype = 'Removed file    : '
      endif
    endif
    let bufnum = bufnr(relpath)
    if buflisted(bufnum) && getbufvar(bufnum, '&mod')
      Pecho 'Old buffer for file [' . relpath . '] exists in modified state. Skipping review.', 1
      continue
    endif
    let tmpname = tempname()

    " write patch for patch.filename into tmpname
    call writefile(patch.content, tmpname)
    if patch.type == '+' && s:reviewmode == 'patch'
      let inputfile = ''
      let patchcmd = '!' . g:patchreview_patch . patch_R_option . ' -o "' . tmpname . '.file" "' . inputfile . '" < "' . tmpname . '"'
    elseif patch.type == '+' && s:reviewmode == 'diff'
      let inputfile = ''
      unlet! patchcmd
    else
      let inputfile = expand(StrippedRelativeFilePath, ':p')
      let patchcmd = '!' . g:patchreview_patch . patch_R_option . ' -o "' . tmpname . '.file" "' . inputfile . '" < "' . tmpname . '"'
    endif
    if exists('patchcmd')
      let v:errmsg = ''
      Debug patchcmd
      silent exe patchcmd
      if v:errmsg != '' || v:shell_error
        Pecho 'ERROR: Could not execute patch command.'
        Pecho 'ERROR:     ' . patchcmd
        Pecho 'ERROR: ' . v:errmsg
        Pecho 'ERROR: Diff skipped.'
        continue
      endif
    endif
    call delete(tmpname)
    let s:origtabpagenr = tabpagenr()
    silent! exe 'tabedit ' . StrippedRelativeFilePath
    if exists('patchcmd')
      " modelines in loaded files mess with diff comparison
      let s:keep_modeline=&modeline
      let &modeline=0
      silent! exe 'vert diffsplit ' . tmpname . '.file'
      setlocal buftype=nofile
      setlocal noswapfile
      setlocal syntax=none
      setlocal bufhidden=delete
      setlocal nobuflisted
      setlocal modifiable
      setlocal nowrap
      " Remove buffer name
      silent! 0f
      " Switch to original to get a nice tab title
      silent! wincmd p
      let &modeline=s:keep_modeline
    else
      silent! exe 'vnew'
    endif
    if filereadable(tmpname . '.file.rej')
      silent! exe 'topleft 5split ' . tmpname . '.file.rej'
      Pecho msgtype . '*** REJECTED *** ' . relpath, 1
    else
      Pecho msgtype . ' ' . relpath, 1
    endif
    silent! exe 'tabn ' . s:origtabpagenr
  endfor
  Pecho '-----'
  Pecho 'Done.'

endfunction
"}}}

function! <SID>DiffReview(...)                                            "{{{
  let s:save_shortmess = &shortmess
  set shortmess=aW
  call s:PR_wipeMsgBuf()

  let vcsdict = {
                  \'Mercurial'  : {'dir' : '.hg',  'binary' : 'hg',  'diffargs' : 'diff' ,          'strip' : 1},
                  \'Bazaar-NG'  : {'dir' : '.bzr', 'binary' : 'bzr', 'diffargs' : 'diff' ,          'strip' : 0},
                  \'monotone'   : {'dir' : '_MTN', 'binary' : 'mtn', 'diffargs' : 'diff --unified', 'strip' : 0},
                  \'Subversion' : {'dir' : '.svn', 'binary' : 'svn', 'diffargs' : 'diff' ,          'strip' : 0},
                  \'cvs'        : {'dir' : 'CVS',  'binary' : 'cvs', 'diffargs' : '-q diff -u' ,    'strip' : 0},
                  \}

  unlet! s:theDiffCmd
  unlet! l:vcs
  if ! exists('g:patchreview_diffcmd')
    for key in keys(vcsdict)
      if isdirectory(vcsdict[key]['dir'])
        if ! s:PR_checkBinary(vcsdict[key]['binary'])
          Pecho 'Current directory looks like a ' . vcsdict[key] . ' repository but ' . vcsdist[key]['binary'] . ' command was not found on path.'
          let &shortmess = s:save_shortmess
          return
        else
          let s:theDiffCmd = vcsdict[key]['binary'] . ' ' . vcsdict[key]['diffargs']
          let strip = vcsdict[key]['strip']

          Pecho 'Using [' . s:theDiffCmd . '] to generate diffs for this ' . key . ' review.'
          let &shortmess = s:save_shortmess
          let l:vcs = vcsdict[key]['binary']
          break
        endif
      else
        continue
      endif
    endfor
  else
    let s:theDiffCmd = g:patchreview_diffcmd
    let strip = 0
  endif
  if ! exists('s:theDiffCmd')
    Pecho 'Please define g:patchreview_diffcmd and make sure you are in a VCS controlled top directory.'
    let &shortmess = s:save_shortmess
    return
  endif

  let outfile = tempname()
  let cmd = s:theDiffCmd . ' > "' . outfile . '"'
  let v:errmsg = ''
  let cout = system(cmd)
  if v:errmsg == '' && exists('l:vcs') && l:vcs == 'cvs' && v:shell_error == 1
    " Ignoring CVS non-error
  elseif v:errmsg != '' || v:shell_error
    Pecho v:errmsg
    Pecho 'Could not execute [' . s:theDiffCmd . ']'
    Pecho 'Error code: ' . v:shell_error
    Pecho cout
    Pecho 'Diff review aborted.'
    let &shortmess = s:save_shortmess
    return
  endif
  let s:reviewmode = 'diff'
  call s:_GenericReview([outfile, strip])
  let &shortmess = s:save_shortmess
endfunction
"}}}

" End user commands                                                         "{{{
"============================================================================
" :PatchReview
command! -nargs=* -complete=file PatchReview call s:PatchReview (<f-args>)

" :DiffReview
command! -nargs=0 DiffReview call s:DiffReview()
"}}}

" Development                                                               "{{{
if exists('g:patchreview_debug')
  " Tests
  function! <SID>PRExtractTestNative(...)
    "let patchfiles = glob(expand(a:1) . '/?*')
    "for fname in split(patchfiles)
    call s:PR_wipeMsgBuf()
    let fname = a:1
    call s:ExtractDiffsNative(fname)
    for patch in g:patches['patch']
      for line in patch.content
        Pecho line
      endfor
    endfor
    "endfor
  endfunction

  function! <SID>PRExtractTestVim(...)
    "let patchfiles = glob(expand(a:1) . '/?*')
    "for fname in split(patchfiles)
    call s:PR_wipeMsgBuf()
    let fname = a:1
    call s:ExtractDiffsPureVim(fname)
    for patch in g:patches['patch']
      for line in patch.content
        Pecho line
      endfor
    endfor
    "endfor
  endfunction

  command! -nargs=+ -complete=file PRTestVim call s:PRExtractTestVim(<f-args>)
  command! -nargs=+ -complete=file PRTestNative call s:PRExtractTestNative(<f-args>)
endif
"}}}

" modeline
" vim: set et fdl=0 fdm=marker fenc=latin ff=unix ft=vim sw=2 sts=0 ts=2 textwidth=78 nowrap :