mirror of
https://github.com/airblade/vim-gitgutter.git
synced 2025-11-08 11:33:48 -05:00
This tells git-show to output the smudged version of a file if a clean/smudge filter is defined for it. In turns this allows us to diff the buffer contents (which is smudged) against the reference version. However patches cannot be applied. See the man pages for git-show and gitattributes for more information. See #796, #435.
442 lines
15 KiB
VimL
442 lines
15 KiB
VimL
scriptencoding utf8
|
||
|
||
let s:nomodeline = (v:version > 703 || (v:version == 703 && has('patch442'))) ? '<nomodeline>' : ''
|
||
|
||
let s:hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@'
|
||
|
||
" True for git v1.7.2+.
|
||
function! s:git_supports_command_line_config_override() abort
|
||
call gitgutter#utility#system(g:gitgutter_git_executable.' '.g:gitgutter_git_args.' -c foo.bar=baz --version')
|
||
return !v:shell_error
|
||
endfunction
|
||
|
||
let s:c_flag = s:git_supports_command_line_config_override()
|
||
|
||
let s:temp_from = tempname()
|
||
let s:temp_buffer = tempname()
|
||
let s:counter = 0
|
||
|
||
" Returns a diff of the buffer against the index or the working tree.
|
||
"
|
||
" After running the diff we pass it through grep where available to reduce
|
||
" subsequent processing by the plugin. If grep is not available the plugin
|
||
" does the filtering instead.
|
||
"
|
||
" When diffing against the index:
|
||
"
|
||
" The buffer contents is not the same as the file on disk so we need to pass
|
||
" two instances of the file to git-diff:
|
||
"
|
||
" git diff myfileA myfileB
|
||
"
|
||
" where myfileA comes from
|
||
"
|
||
" git show :myfile > myfileA
|
||
"
|
||
" and myfileB is the buffer contents.
|
||
"
|
||
" Regarding line endings:
|
||
"
|
||
" git-show does not convert line endings.
|
||
" git-diff FILE FILE does convert line endings for the given files.
|
||
"
|
||
" If a file has CRLF line endings and git's core.autocrlf is true,
|
||
" the file in git's object store will have LF line endings. Writing
|
||
" it out via git-show will produce a file with LF line endings.
|
||
"
|
||
" If this last file is one of the files passed to git-diff, git-diff will
|
||
" convert its line endings to CRLF before diffing -- which is what we want --
|
||
" but also by default output a warning on stderr.
|
||
"
|
||
" warning: LF will be replace by CRLF in <temp file>.
|
||
" The file will have its original line endings in your working directory.
|
||
"
|
||
" When running the diff asynchronously, the warning message triggers the stderr
|
||
" callbacks which assume the overall command has failed and reset all the
|
||
" signs. As this is not what we want, and we can safely ignore the warning,
|
||
" we turn it off by passing the '-c "core.safecrlf=false"' argument to
|
||
" git-diff.
|
||
"
|
||
" When writing the temporary files we preserve the original file's extension
|
||
" so that repos using .gitattributes to control EOL conversion continue to
|
||
" convert correctly.
|
||
"
|
||
" Arguments:
|
||
"
|
||
" bufnr - the number of the buffer to be diffed
|
||
" from - 'index' or 'working_tree'; what the buffer is diffed against
|
||
" preserve_full_diff - truthy to return the full diff or falsey to return only
|
||
" the hunk headers (@@ -x,y +m,n @@); only possible if
|
||
" grep is available.
|
||
function! gitgutter#diff#run_diff(bufnr, from, preserve_full_diff) abort
|
||
if gitgutter#utility#repo_path(a:bufnr, 0) == -1
|
||
throw 'gitgutter path not set'
|
||
endif
|
||
|
||
if gitgutter#utility#repo_path(a:bufnr, 0) == -2
|
||
throw 'gitgutter not tracked'
|
||
endif
|
||
|
||
if gitgutter#utility#repo_path(a:bufnr, 0) == -3
|
||
throw 'gitgutter assume unchanged'
|
||
endif
|
||
|
||
" If we are diffing against a specific branch/commit, handle the case
|
||
" where a file exists on the current branch but not in/at the diff base.
|
||
" We have to handle it here because the approach below (using git-show)
|
||
" doesn't work for this case.
|
||
if !empty(g:gitgutter_diff_base)
|
||
let index_name = gitgutter#utility#get_diff_base(a:bufnr).':'.gitgutter#utility#repo_path(a:bufnr, 1)
|
||
let cmd = g:gitgutter_git_executable.' '.g:gitgutter_git_args.' --no-pager show '.index_name
|
||
let cmd = gitgutter#utility#cd_cmd(a:bufnr, cmd)
|
||
call gitgutter#utility#system(cmd)
|
||
if v:shell_error
|
||
throw 'gitgutter file unknown in base'
|
||
endif
|
||
endif
|
||
|
||
" Wrap compound commands in parentheses to make Windows happy.
|
||
" bash doesn't mind the parentheses.
|
||
let cmd = '('
|
||
|
||
" Append buffer number to temp filenames to avoid race conditions between
|
||
" writing and reading the files when asynchronously processing multiple
|
||
" buffers.
|
||
|
||
" Without the buffer number, buff_file would have a race between the
|
||
" second gitgutter#process_buffer() writing the file (synchronously, below)
|
||
" and the first gitgutter#process_buffer()'s async job reading it (with
|
||
" git-diff).
|
||
let buff_file = s:temp_buffer.'.'.a:bufnr
|
||
|
||
" Add a counter to avoid a similar race with two quick writes of the same buffer.
|
||
" Use a modulus greater than a maximum reasonable number of visible buffers.
|
||
let s:counter = (s:counter + 1) % 20
|
||
let buff_file .= '.'.s:counter
|
||
|
||
let extension = gitgutter#utility#extension(a:bufnr)
|
||
if !empty(extension)
|
||
let buff_file .= '.'.extension
|
||
endif
|
||
|
||
" Write buffer to temporary file.
|
||
" Note: this is synchronous.
|
||
call s:write_buffer(a:bufnr, buff_file)
|
||
|
||
if a:from ==# 'index'
|
||
" Without the buffer number, from_file would have a race in the shell
|
||
" between the second process writing it (with git-show) and the first
|
||
" reading it (with git-diff).
|
||
let from_file = s:temp_from.'.'.a:bufnr
|
||
|
||
" Add a counter to avoid a similar race with two quick writes of the same buffer.
|
||
let from_file .= '.'.s:counter
|
||
|
||
if !empty(extension)
|
||
let from_file .= '.'.extension
|
||
endif
|
||
|
||
" Write file from index to temporary file.
|
||
let index_name = gitgutter#utility#get_diff_base(a:bufnr).':'.gitgutter#utility#repo_path(a:bufnr, 1)
|
||
let cmd .= g:gitgutter_git_executable.' '.g:gitgutter_git_args.' --no-pager show --textconv '.index_name.' > '.from_file.' && '
|
||
|
||
elseif a:from ==# 'working_tree'
|
||
let from_file = gitgutter#utility#repo_path(a:bufnr, 1)
|
||
endif
|
||
|
||
" Call git-diff.
|
||
let cmd .= g:gitgutter_git_executable.' '.g:gitgutter_git_args.' --no-pager'
|
||
if s:c_flag
|
||
let cmd .= ' -c "diff.autorefreshindex=0"'
|
||
let cmd .= ' -c "diff.noprefix=false"'
|
||
let cmd .= ' -c "core.safecrlf=false"'
|
||
endif
|
||
let cmd .= ' diff --no-ext-diff --no-color -U0 '.g:gitgutter_diff_args.' -- '.from_file.' '.buff_file
|
||
|
||
" Pipe git-diff output into grep.
|
||
if !a:preserve_full_diff && !empty(g:gitgutter_grep)
|
||
let cmd .= ' | '.g:gitgutter_grep.' '.gitgutter#utility#shellescape('^@@ ')
|
||
endif
|
||
|
||
" grep exits with 1 when no matches are found; git-diff exits with 1 when
|
||
" differences are found. However we want to treat non-matches and
|
||
" differences as non-erroneous behaviour; so we OR the command with one
|
||
" which always exits with success (0).
|
||
let cmd .= ' || exit 0'
|
||
|
||
let cmd .= ')'
|
||
|
||
let cmd = gitgutter#utility#cd_cmd(a:bufnr, cmd)
|
||
|
||
if g:gitgutter_async && gitgutter#async#available()
|
||
call gitgutter#async#execute(cmd, a:bufnr, {
|
||
\ 'out': function('gitgutter#diff#handler'),
|
||
\ 'err': function('gitgutter#hunk#reset'),
|
||
\ })
|
||
return 'async'
|
||
|
||
else
|
||
let diff = gitgutter#utility#system(cmd)
|
||
|
||
if v:shell_error
|
||
call gitgutter#debug#log(diff)
|
||
throw 'gitgutter diff failed'
|
||
endif
|
||
|
||
return diff
|
||
endif
|
||
endfunction
|
||
|
||
|
||
function! gitgutter#diff#handler(bufnr, diff) abort
|
||
call gitgutter#debug#log(a:diff)
|
||
|
||
if !bufexists(a:bufnr)
|
||
return
|
||
endif
|
||
|
||
call gitgutter#hunk#set_hunks(a:bufnr, gitgutter#diff#parse_diff(a:diff))
|
||
let modified_lines = gitgutter#diff#process_hunks(a:bufnr, gitgutter#hunk#hunks(a:bufnr))
|
||
|
||
let signs_count = len(modified_lines)
|
||
if g:gitgutter_max_signs != -1 && signs_count > g:gitgutter_max_signs
|
||
call gitgutter#utility#warn_once(a:bufnr, printf(
|
||
\ 'exceeded maximum number of signs (%d > %d, configured by g:gitgutter_max_signs).',
|
||
\ signs_count, g:gitgutter_max_signs), 'max_signs')
|
||
call gitgutter#sign#clear_signs(a:bufnr)
|
||
|
||
else
|
||
if g:gitgutter_signs || g:gitgutter_highlight_lines || g:gitgutter_highlight_linenrs
|
||
call gitgutter#sign#update_signs(a:bufnr, modified_lines)
|
||
endif
|
||
endif
|
||
|
||
call s:save_last_seen_change(a:bufnr)
|
||
if exists('#User#GitGutter')
|
||
let g:gitgutter_hook_context = {'bufnr': a:bufnr}
|
||
execute 'doautocmd' s:nomodeline 'User GitGutter'
|
||
unlet g:gitgutter_hook_context
|
||
endif
|
||
endfunction
|
||
|
||
|
||
function! gitgutter#diff#parse_diff(diff) abort
|
||
let hunks = []
|
||
for line in split(a:diff, '\n')
|
||
let hunk_info = gitgutter#diff#parse_hunk(line)
|
||
if len(hunk_info) == 4
|
||
call add(hunks, hunk_info)
|
||
endif
|
||
endfor
|
||
return hunks
|
||
endfunction
|
||
|
||
function! gitgutter#diff#parse_hunk(line) abort
|
||
let matches = matchlist(a:line, s:hunk_re)
|
||
if len(matches) > 0
|
||
let from_line = str2nr(matches[1])
|
||
let from_count = (matches[2] == '') ? 1 : str2nr(matches[2])
|
||
let to_line = str2nr(matches[3])
|
||
let to_count = (matches[4] == '') ? 1 : str2nr(matches[4])
|
||
return [from_line, from_count, to_line, to_count]
|
||
else
|
||
return []
|
||
end
|
||
endfunction
|
||
|
||
" This function is public so it may be used by other plugins
|
||
" e.g. vim-signature.
|
||
function! gitgutter#diff#process_hunks(bufnr, hunks) abort
|
||
let modified_lines = []
|
||
for hunk in a:hunks
|
||
call extend(modified_lines, s:process_hunk(a:bufnr, hunk))
|
||
endfor
|
||
return modified_lines
|
||
endfunction
|
||
|
||
" Returns [ [<line_number (number)>, <name (string)>], ...]
|
||
function! s:process_hunk(bufnr, hunk) abort
|
||
let modifications = []
|
||
let from_line = a:hunk[0]
|
||
let from_count = a:hunk[1]
|
||
let to_line = a:hunk[2]
|
||
let to_count = a:hunk[3]
|
||
|
||
if s:is_added(from_count, to_count)
|
||
call s:process_added(modifications, from_count, to_count, to_line)
|
||
call gitgutter#hunk#increment_lines_added(a:bufnr, to_count)
|
||
|
||
elseif s:is_removed(from_count, to_count)
|
||
call s:process_removed(modifications, from_count, to_count, to_line)
|
||
call gitgutter#hunk#increment_lines_removed(a:bufnr, from_count)
|
||
|
||
elseif s:is_modified(from_count, to_count)
|
||
call s:process_modified(modifications, from_count, to_count, to_line)
|
||
call gitgutter#hunk#increment_lines_modified(a:bufnr, to_count)
|
||
|
||
elseif s:is_modified_and_added(from_count, to_count)
|
||
call s:process_modified_and_added(modifications, from_count, to_count, to_line)
|
||
call gitgutter#hunk#increment_lines_added(a:bufnr, to_count - from_count)
|
||
call gitgutter#hunk#increment_lines_modified(a:bufnr, from_count)
|
||
|
||
elseif s:is_modified_and_removed(from_count, to_count)
|
||
call s:process_modified_and_removed(modifications, from_count, to_count, to_line)
|
||
call gitgutter#hunk#increment_lines_modified(a:bufnr, to_count)
|
||
call gitgutter#hunk#increment_lines_removed(a:bufnr, from_count - to_count)
|
||
|
||
endif
|
||
return modifications
|
||
endfunction
|
||
|
||
function! s:is_added(from_count, to_count) abort
|
||
return a:from_count == 0 && a:to_count > 0
|
||
endfunction
|
||
|
||
function! s:is_removed(from_count, to_count) abort
|
||
return a:from_count > 0 && a:to_count == 0
|
||
endfunction
|
||
|
||
function! s:is_modified(from_count, to_count) abort
|
||
return a:from_count > 0 && a:to_count > 0 && a:from_count == a:to_count
|
||
endfunction
|
||
|
||
function! s:is_modified_and_added(from_count, to_count) abort
|
||
return a:from_count > 0 && a:to_count > 0 && a:from_count < a:to_count
|
||
endfunction
|
||
|
||
function! s:is_modified_and_removed(from_count, to_count) abort
|
||
return a:from_count > 0 && a:to_count > 0 && a:from_count > a:to_count
|
||
endfunction
|
||
|
||
function! s:process_added(modifications, from_count, to_count, to_line) abort
|
||
let offset = 0
|
||
while offset < a:to_count
|
||
let line_number = a:to_line + offset
|
||
call add(a:modifications, [line_number, 'added'])
|
||
let offset += 1
|
||
endwhile
|
||
endfunction
|
||
|
||
function! s:process_removed(modifications, from_count, to_count, to_line) abort
|
||
if a:to_line == 0
|
||
call add(a:modifications, [1, 'removed_first_line'])
|
||
else
|
||
call add(a:modifications, [a:to_line, 'removed'])
|
||
endif
|
||
endfunction
|
||
|
||
function! s:process_modified(modifications, from_count, to_count, to_line) abort
|
||
let offset = 0
|
||
while offset < a:to_count
|
||
let line_number = a:to_line + offset
|
||
call add(a:modifications, [line_number, 'modified'])
|
||
let offset += 1
|
||
endwhile
|
||
endfunction
|
||
|
||
function! s:process_modified_and_added(modifications, from_count, to_count, to_line) abort
|
||
let offset = 0
|
||
while offset < a:from_count
|
||
let line_number = a:to_line + offset
|
||
call add(a:modifications, [line_number, 'modified'])
|
||
let offset += 1
|
||
endwhile
|
||
while offset < a:to_count
|
||
let line_number = a:to_line + offset
|
||
call add(a:modifications, [line_number, 'added'])
|
||
let offset += 1
|
||
endwhile
|
||
endfunction
|
||
|
||
function! s:process_modified_and_removed(modifications, from_count, to_count, to_line) abort
|
||
let offset = 0
|
||
while offset < a:to_count
|
||
let line_number = a:to_line + offset
|
||
call add(a:modifications, [line_number, 'modified'])
|
||
let offset += 1
|
||
endwhile
|
||
let a:modifications[-1] = [a:to_line + offset - 1, 'modified_removed']
|
||
endfunction
|
||
|
||
|
||
" Returns a diff for the current hunk.
|
||
" Assumes there is only 1 current hunk unless the optional argument is given,
|
||
" in which case the cursor is in two hunks and the argument specifies the one
|
||
" to choose.
|
||
"
|
||
" Optional argument: 0 (to use the first hunk) or 1 (to use the second).
|
||
function! gitgutter#diff#hunk_diff(bufnr, full_diff, ...)
|
||
let modified_diff = []
|
||
let hunk_index = 0
|
||
let keep_line = 1
|
||
" Don't keepempty when splitting because the diff we want may not be the
|
||
" final one. Instead add trailing NL at end of function.
|
||
for line in split(a:full_diff, '\n')
|
||
let hunk_info = gitgutter#diff#parse_hunk(line)
|
||
if len(hunk_info) == 4 " start of new hunk
|
||
let keep_line = gitgutter#hunk#cursor_in_hunk(hunk_info)
|
||
|
||
if a:0 && hunk_index != a:1
|
||
let keep_line = 0
|
||
endif
|
||
|
||
let hunk_index += 1
|
||
endif
|
||
if keep_line
|
||
call add(modified_diff, line)
|
||
endif
|
||
endfor
|
||
return join(modified_diff, "\n")."\n"
|
||
endfunction
|
||
|
||
|
||
function! gitgutter#diff#hunk_header_showing_every_line_added(bufnr)
|
||
let buf_line_count = getbufinfo(a:bufnr)[0].linecount
|
||
return '@@ -0,0 +1,'.buf_line_count.' @@'
|
||
endfunction
|
||
|
||
|
||
function! s:write_buffer(bufnr, file)
|
||
let bufcontents = getbufline(a:bufnr, 1, '$')
|
||
|
||
if bufcontents == [''] && line2byte(1) == -1
|
||
" Special case: completely empty buffer.
|
||
" A nearly empty buffer of only a newline has line2byte(1) == 1.
|
||
call writefile([], a:file)
|
||
return
|
||
endif
|
||
|
||
if getbufvar(a:bufnr, '&fileformat') ==# 'dos'
|
||
call map(bufcontents, 'v:val."\r"')
|
||
endif
|
||
|
||
if getbufvar(a:bufnr, '&endofline')
|
||
call add(bufcontents, '')
|
||
endif
|
||
|
||
let fenc = getbufvar(a:bufnr, '&fileencoding')
|
||
if fenc !=# &encoding
|
||
call map(bufcontents, 'iconv(v:val, &encoding, "'.fenc.'")')
|
||
endif
|
||
|
||
if getbufvar(a:bufnr, '&bomb')
|
||
let bufcontents[0]=''.bufcontents[0]
|
||
endif
|
||
|
||
" The file we are writing to is a temporary file. Sometimes the parent
|
||
" directory is deleted outside Vim but, because Vim caches the directory
|
||
" name at startup and does not check for its existence subsequently, Vim
|
||
" does not realise. This causes E482 errors.
|
||
try
|
||
call writefile(bufcontents, a:file, 'b')
|
||
catch /E482/
|
||
call mkdir(fnamemodify(a:file, ':h'), '', '0700')
|
||
call writefile(bufcontents, a:file, 'b')
|
||
endtry
|
||
endfunction
|
||
|
||
|
||
function! s:save_last_seen_change(bufnr) abort
|
||
call gitgutter#utility#setbufvar(a:bufnr, 'tick', getbufvar(a:bufnr, 'changedtick'))
|
||
endfunction
|