aboutsummaryrefslogtreecommitdiff
path: root/vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim
diff options
context:
space:
mode:
Diffstat (limited to 'vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim')
-rw-r--r--vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim342
1 files changed, 342 insertions, 0 deletions
diff --git a/vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim b/vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim
new file mode 100644
index 0000000..170193b
--- /dev/null
+++ b/vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim
@@ -0,0 +1,342 @@
+if exists('g:gitgutter_grep_command')
+ let s:grep_available = 1
+ let s:grep_command = g:gitgutter_grep_command
+else
+ let s:grep_available = executable('grep')
+ if s:grep_available
+ let s:grep_command = 'grep --color=never -e'
+ endif
+endif
+let s:hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@'
+
+let s:c_flag = gitgutter#utility#git_supports_command_line_config_override()
+
+let s:temp_index = tempname()
+let s:temp_buffer = tempname()
+
+" Returns a diff of the buffer.
+"
+" The way to get the diff depends on whether the buffer is saved or unsaved.
+"
+" * Saved: the buffer contents is the same as the file on disk in the working
+" tree so we simply do:
+"
+" git diff myfile
+"
+" * Unsaved: 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
+"
+" The first instance is the file in the index which we obtain with:
+"
+" git show :myfile > myfileA
+"
+" The second instance is the buffer contents. Ideally we would pass this to
+" git-diff on stdin via the second argument to vim's system() function.
+" Unfortunately git-diff does not do CRLF conversion for input received on
+" stdin, and git-show never performs CRLF conversion, so repos with CRLF
+" conversion report that every line is modified due to mismatching EOLs.
+"
+" Instead, we write the buffer contents to a temporary file - myfileB in this
+" example. Note the file extension must be preserved for the CRLF
+" conversion to work.
+"
+" Before diffing a buffer for the first time, we check whether git knows about
+" the file:
+"
+" git ls-files --error-unmatch myfile
+"
+" 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.
+function! gitgutter#diff#run_diff(realtime, preserve_full_diff) abort
+ " Wrap compound commands in parentheses to make Windows happy.
+ " bash doesn't mind the parentheses.
+ let cmd = '('
+
+ let bufnr = gitgutter#utility#bufnr()
+ let tracked = getbufvar(bufnr, 'gitgutter_tracked') " i.e. tracked by git
+ if !tracked
+ " Don't bother trying to realtime-diff an untracked file.
+ " NOTE: perhaps we should pull this guard up to the caller?
+ if a:realtime
+ throw 'diff failed'
+ else
+ let cmd .= g:gitgutter_git_executable.' ls-files --error-unmatch '.gitgutter#utility#shellescape(gitgutter#utility#filename()).' && ('
+ endif
+ endif
+
+ if a:realtime
+ let blob_name = g:gitgutter_diff_base.':'.gitgutter#utility#shellescape(gitgutter#utility#file_relative_to_repo_root())
+ let blob_file = s:temp_index
+ let buff_file = s:temp_buffer
+ let extension = gitgutter#utility#extension()
+ if !empty(extension)
+ let blob_file .= '.'.extension
+ let buff_file .= '.'.extension
+ endif
+ let cmd .= g:gitgutter_git_executable.' show '.blob_name.' > '.blob_file.' && '
+
+ " Writing the whole buffer resets the '[ and '] marks and also the
+ " 'modified' flag (if &cpoptions includes '+'). These are unwanted
+ " side-effects so we save and restore the values ourselves.
+ let modified = getbufvar(bufnr, "&mod")
+ let op_mark_start = getpos("'[")
+ let op_mark_end = getpos("']")
+
+ execute 'keepalt noautocmd silent write!' buff_file
+
+ call setbufvar(bufnr, "&mod", modified)
+ call setpos("'[", op_mark_start)
+ call setpos("']", op_mark_end)
+ endif
+
+ let cmd .= g:gitgutter_git_executable
+ if s:c_flag
+ let cmd .= ' -c "diff.autorefreshindex=0"'
+ endif
+ let cmd .= ' diff --no-ext-diff --no-color -U0 '.g:gitgutter_diff_args.' '
+
+ if a:realtime
+ let cmd .= ' -- '.blob_file.' '.buff_file
+ else
+ let cmd .= g:gitgutter_diff_base.' -- '.gitgutter#utility#shellescape(gitgutter#utility#filename())
+ endif
+
+ if !a:preserve_full_diff && s:grep_available
+ let cmd .= ' | '.s:grep_command.' '.gitgutter#utility#shellescape('^@@ ')
+ endif
+
+ if (!a:preserve_full_diff && s:grep_available) || a:realtime
+ " grep exits with 1 when no matches are found; 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'
+ endif
+
+ let cmd .= ')'
+
+ if !tracked
+ let cmd .= ')'
+ endif
+
+ let cmd = gitgutter#utility#command_in_directory_of_file(cmd)
+
+ if g:gitgutter_async && gitgutter#async#available() && !a:preserve_full_diff
+ call gitgutter#async#execute(cmd)
+ return 'async'
+
+ else
+ let diff = gitgutter#utility#system(cmd)
+
+ if gitgutter#utility#shell_error()
+ " A shell error indicates the file is not tracked by git (unless something bizarre is going on).
+ throw 'diff failed'
+ endif
+
+ return diff
+ 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
+
+function! gitgutter#diff#process_hunks(hunks) abort
+ call gitgutter#hunk#reset()
+ let modified_lines = []
+ for hunk in a:hunks
+ call extend(modified_lines, gitgutter#diff#process_hunk(hunk))
+ endfor
+ return modified_lines
+endfunction
+
+" Returns [ [<line_number (number)>, <name (string)>], ...]
+function! gitgutter#diff#process_hunk(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 gitgutter#diff#is_added(from_count, to_count)
+ call gitgutter#diff#process_added(modifications, from_count, to_count, to_line)
+ call gitgutter#hunk#increment_lines_added(to_count)
+
+ elseif gitgutter#diff#is_removed(from_count, to_count)
+ call gitgutter#diff#process_removed(modifications, from_count, to_count, to_line)
+ call gitgutter#hunk#increment_lines_removed(from_count)
+
+ elseif gitgutter#diff#is_modified(from_count, to_count)
+ call gitgutter#diff#process_modified(modifications, from_count, to_count, to_line)
+ call gitgutter#hunk#increment_lines_modified(to_count)
+
+ elseif gitgutter#diff#is_modified_and_added(from_count, to_count)
+ call gitgutter#diff#process_modified_and_added(modifications, from_count, to_count, to_line)
+ call gitgutter#hunk#increment_lines_added(to_count - from_count)
+ call gitgutter#hunk#increment_lines_modified(from_count)
+
+ elseif gitgutter#diff#is_modified_and_removed(from_count, to_count)
+ call gitgutter#diff#process_modified_and_removed(modifications, from_count, to_count, to_line)
+ call gitgutter#hunk#increment_lines_modified(to_count)
+ call gitgutter#hunk#increment_lines_removed(from_count - to_count)
+
+ endif
+ return modifications
+endfunction
+
+function! gitgutter#diff#is_added(from_count, to_count) abort
+ return a:from_count == 0 && a:to_count > 0
+endfunction
+
+function! gitgutter#diff#is_removed(from_count, to_count) abort
+ return a:from_count > 0 && a:to_count == 0
+endfunction
+
+function! gitgutter#diff#is_modified(from_count, to_count) abort
+ return a:from_count > 0 && a:to_count > 0 && a:from_count == a:to_count
+endfunction
+
+function! gitgutter#diff#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! gitgutter#diff#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! gitgutter#diff#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! gitgutter#diff#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! gitgutter#diff#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! gitgutter#diff#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! gitgutter#diff#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
+
+" Generates a zero-context diff for the current hunk.
+"
+" diff - the full diff for the buffer
+" type - stage | undo | preview
+function! gitgutter#diff#generate_diff_for_hunk(diff, type) abort
+ let diff_for_hunk = gitgutter#diff#discard_hunks(a:diff, a:type == 'stage' || a:type == 'undo')
+
+ if a:type == 'stage' || a:type == 'undo'
+ let diff_for_hunk = gitgutter#diff#adjust_hunk_summary(diff_for_hunk, a:type == 'stage')
+ endif
+
+ return diff_for_hunk
+endfunction
+
+" Returns the diff with all hunks discarded except the current.
+"
+" diff - the diff to process
+" keep_header - truthy to keep the diff header and hunk summary, falsy to discard it
+function! gitgutter#diff#discard_hunks(diff, keep_header) abort
+ let modified_diff = []
+ let keep_line = a:keep_header
+ for line in split(a: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)
+ endif
+ if keep_line
+ call add(modified_diff, line)
+ endif
+ endfor
+
+ if a:keep_header
+ return gitgutter#utility#stringify(modified_diff)
+ else
+ " Discard hunk summary too.
+ return gitgutter#utility#stringify(modified_diff[1:])
+ endif
+endfunction
+
+" Adjust hunk summary (from's / to's line number) to ignore changes above/before this one.
+"
+" diff_for_hunk - a diff containing only the hunk of interest
+" staging - truthy if the hunk is to be staged, falsy if it is to be undone
+"
+" TODO: push this down to #discard_hunks?
+function! gitgutter#diff#adjust_hunk_summary(diff_for_hunk, staging) abort
+ let line_adjustment = gitgutter#hunk#line_adjustment_for_current_hunk()
+ let adj_diff = []
+ for line in split(a:diff_for_hunk, '\n')
+ if match(line, s:hunk_re) != -1
+ if a:staging
+ " increment 'to' line number
+ let line = substitute(line, '+\@<=\(\d\+\)', '\=submatch(1)+line_adjustment', '')
+ else
+ " decrement 'from' line number
+ let line = substitute(line, '-\@<=\(\d\+\)', '\=submatch(1)-line_adjustment', '')
+ endif
+ endif
+ call add(adj_diff, line)
+ endfor
+ return gitgutter#utility#stringify(adj_diff)
+endfunction
+