diff.vim 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. scriptencoding utf8
  2. let s:nomodeline = (v:version > 703 || (v:version == 703 && has('patch442'))) ? '<nomodeline>' : ''
  3. let s:hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@'
  4. " True for git v1.7.2+.
  5. function! s:git_supports_command_line_config_override() abort
  6. call gitgutter#utility#system(g:gitgutter_git_executable.' '.g:gitgutter_git_args.' -c foo.bar=baz --version')
  7. return !v:shell_error
  8. endfunction
  9. let s:c_flag = s:git_supports_command_line_config_override()
  10. let s:temp_from = tempname()
  11. let s:temp_buffer = tempname()
  12. let s:counter = 0
  13. " Returns a diff of the buffer against the index or the working tree.
  14. "
  15. " After running the diff we pass it through grep where available to reduce
  16. " subsequent processing by the plugin. If grep is not available the plugin
  17. " does the filtering instead.
  18. "
  19. " When diffing against the index:
  20. "
  21. " The buffer contents is not the same as the file on disk so we need to pass
  22. " two instances of the file to git-diff:
  23. "
  24. " git diff myfileA myfileB
  25. "
  26. " where myfileA comes from
  27. "
  28. " git show :myfile > myfileA
  29. "
  30. " and myfileB is the buffer contents.
  31. "
  32. " Regarding line endings:
  33. "
  34. " git-show does not convert line endings.
  35. " git-diff FILE FILE does convert line endings for the given files.
  36. "
  37. " If a file has CRLF line endings and git's core.autocrlf is true,
  38. " the file in git's object store will have LF line endings. Writing
  39. " it out via git-show will produce a file with LF line endings.
  40. "
  41. " If this last file is one of the files passed to git-diff, git-diff will
  42. " convert its line endings to CRLF before diffing -- which is what we want --
  43. " but also by default output a warning on stderr.
  44. "
  45. " warning: LF will be replace by CRLF in <temp file>.
  46. " The file will have its original line endings in your working directory.
  47. "
  48. " When running the diff asynchronously, the warning message triggers the stderr
  49. " callbacks which assume the overall command has failed and reset all the
  50. " signs. As this is not what we want, and we can safely ignore the warning,
  51. " we turn it off by passing the '-c "core.safecrlf=false"' argument to
  52. " git-diff.
  53. "
  54. " When writing the temporary files we preserve the original file's extension
  55. " so that repos using .gitattributes to control EOL conversion continue to
  56. " convert correctly.
  57. "
  58. " Arguments:
  59. "
  60. " bufnr - the number of the buffer to be diffed
  61. " from - 'index' or 'working_tree'; what the buffer is diffed against
  62. " preserve_full_diff - truthy to return the full diff or falsey to return only
  63. " the hunk headers (@@ -x,y +m,n @@); only possible if
  64. " grep is available.
  65. function! gitgutter#diff#run_diff(bufnr, from, preserve_full_diff) abort
  66. if gitgutter#utility#repo_path(a:bufnr, 0) == -1
  67. throw 'gitgutter path not set'
  68. endif
  69. if gitgutter#utility#repo_path(a:bufnr, 0) == -2
  70. throw 'gitgutter not tracked'
  71. endif
  72. if gitgutter#utility#repo_path(a:bufnr, 0) == -3
  73. throw 'gitgutter assume unchanged'
  74. endif
  75. " Wrap compound commands in parentheses to make Windows happy.
  76. " bash doesn't mind the parentheses.
  77. let cmd = '('
  78. " Append buffer number to temp filenames to avoid race conditions between
  79. " writing and reading the files when asynchronously processing multiple
  80. " buffers.
  81. " Without the buffer number, buff_file would have a race between the
  82. " second gitgutter#process_buffer() writing the file (synchronously, below)
  83. " and the first gitgutter#process_buffer()'s async job reading it (with
  84. " git-diff).
  85. let buff_file = s:temp_buffer.'.'.a:bufnr
  86. " Add a counter to avoid a similar race with two quick writes of the same buffer.
  87. " Use a modulus greater than a maximum reasonable number of visible buffers.
  88. let s:counter = (s:counter + 1) % 20
  89. let buff_file .= '.'.s:counter
  90. let extension = gitgutter#utility#extension(a:bufnr)
  91. if !empty(extension)
  92. let buff_file .= '.'.extension
  93. endif
  94. " Write buffer to temporary file.
  95. " Note: this is synchronous.
  96. call s:write_buffer(a:bufnr, buff_file)
  97. if a:from ==# 'index'
  98. " Without the buffer number, from_file would have a race in the shell
  99. " between the second process writing it (with git-show) and the first
  100. " reading it (with git-diff).
  101. let from_file = s:temp_from.'.'.a:bufnr
  102. " Add a counter to avoid a similar race with two quick writes of the same buffer.
  103. let from_file .= '.'.s:counter
  104. if !empty(extension)
  105. let from_file .= '.'.extension
  106. endif
  107. " Write file from index to temporary file.
  108. let index_name = gitgutter#utility#get_diff_base(a:bufnr).':'.gitgutter#utility#repo_path(a:bufnr, 1)
  109. let cmd .= g:gitgutter_git_executable.' '.g:gitgutter_git_args.' --no-pager show '.index_name.' > '.from_file.' && '
  110. elseif a:from ==# 'working_tree'
  111. let from_file = gitgutter#utility#repo_path(a:bufnr, 1)
  112. endif
  113. " Call git-diff.
  114. let cmd .= g:gitgutter_git_executable.' '.g:gitgutter_git_args.' --no-pager'
  115. if s:c_flag
  116. let cmd .= ' -c "diff.autorefreshindex=0"'
  117. let cmd .= ' -c "diff.noprefix=false"'
  118. let cmd .= ' -c "core.safecrlf=false"'
  119. endif
  120. let cmd .= ' diff --no-ext-diff --no-color -U0 '.g:gitgutter_diff_args.' -- '.from_file.' '.buff_file
  121. " Pipe git-diff output into grep.
  122. if !a:preserve_full_diff && !empty(g:gitgutter_grep)
  123. let cmd .= ' | '.g:gitgutter_grep.' '.gitgutter#utility#shellescape('^@@ ')
  124. endif
  125. " grep exits with 1 when no matches are found; git-diff exits with 1 when
  126. " differences are found. However we want to treat non-matches and
  127. " differences as non-erroneous behaviour; so we OR the command with one
  128. " which always exits with success (0).
  129. let cmd .= ' || exit 0'
  130. let cmd .= ')'
  131. let cmd = gitgutter#utility#cd_cmd(a:bufnr, cmd)
  132. if g:gitgutter_async && gitgutter#async#available()
  133. call gitgutter#async#execute(cmd, a:bufnr, {
  134. \ 'out': function('gitgutter#diff#handler'),
  135. \ 'err': function('gitgutter#hunk#reset'),
  136. \ })
  137. return 'async'
  138. else
  139. let diff = gitgutter#utility#system(cmd)
  140. if v:shell_error
  141. call gitgutter#debug#log(diff)
  142. throw 'gitgutter diff failed'
  143. endif
  144. return diff
  145. endif
  146. endfunction
  147. function! gitgutter#diff#handler(bufnr, diff) abort
  148. call gitgutter#debug#log(a:diff)
  149. if !bufexists(a:bufnr)
  150. return
  151. endif
  152. call gitgutter#hunk#set_hunks(a:bufnr, gitgutter#diff#parse_diff(a:diff))
  153. let modified_lines = gitgutter#diff#process_hunks(a:bufnr, gitgutter#hunk#hunks(a:bufnr))
  154. let signs_count = len(modified_lines)
  155. if g:gitgutter_max_signs != -1 && signs_count > g:gitgutter_max_signs
  156. call gitgutter#utility#warn_once(a:bufnr, printf(
  157. \ 'exceeded maximum number of signs (%d > %d, configured by g:gitgutter_max_signs).',
  158. \ signs_count, g:gitgutter_max_signs), 'max_signs')
  159. call gitgutter#sign#clear_signs(a:bufnr)
  160. else
  161. if g:gitgutter_signs || g:gitgutter_highlight_lines || g:gitgutter_highlight_linenrs
  162. call gitgutter#sign#update_signs(a:bufnr, modified_lines)
  163. endif
  164. endif
  165. call s:save_last_seen_change(a:bufnr)
  166. if exists('#User#GitGutter')
  167. let g:gitgutter_hook_context = {'bufnr': a:bufnr}
  168. execute 'doautocmd' s:nomodeline 'User GitGutter'
  169. unlet g:gitgutter_hook_context
  170. endif
  171. endfunction
  172. function! gitgutter#diff#parse_diff(diff) abort
  173. let hunks = []
  174. for line in split(a:diff, '\n')
  175. let hunk_info = gitgutter#diff#parse_hunk(line)
  176. if len(hunk_info) == 4
  177. call add(hunks, hunk_info)
  178. endif
  179. endfor
  180. return hunks
  181. endfunction
  182. function! gitgutter#diff#parse_hunk(line) abort
  183. let matches = matchlist(a:line, s:hunk_re)
  184. if len(matches) > 0
  185. let from_line = str2nr(matches[1])
  186. let from_count = (matches[2] == '') ? 1 : str2nr(matches[2])
  187. let to_line = str2nr(matches[3])
  188. let to_count = (matches[4] == '') ? 1 : str2nr(matches[4])
  189. return [from_line, from_count, to_line, to_count]
  190. else
  191. return []
  192. end
  193. endfunction
  194. " This function is public so it may be used by other plugins
  195. " e.g. vim-signature.
  196. function! gitgutter#diff#process_hunks(bufnr, hunks) abort
  197. let modified_lines = []
  198. for hunk in a:hunks
  199. call extend(modified_lines, s:process_hunk(a:bufnr, hunk))
  200. endfor
  201. return modified_lines
  202. endfunction
  203. " Returns [ [<line_number (number)>, <name (string)>], ...]
  204. function! s:process_hunk(bufnr, hunk) abort
  205. let modifications = []
  206. let from_line = a:hunk[0]
  207. let from_count = a:hunk[1]
  208. let to_line = a:hunk[2]
  209. let to_count = a:hunk[3]
  210. if s:is_added(from_count, to_count)
  211. call s:process_added(modifications, from_count, to_count, to_line)
  212. call gitgutter#hunk#increment_lines_added(a:bufnr, to_count)
  213. elseif s:is_removed(from_count, to_count)
  214. call s:process_removed(modifications, from_count, to_count, to_line)
  215. call gitgutter#hunk#increment_lines_removed(a:bufnr, from_count)
  216. elseif s:is_modified(from_count, to_count)
  217. call s:process_modified(modifications, from_count, to_count, to_line)
  218. call gitgutter#hunk#increment_lines_modified(a:bufnr, to_count)
  219. elseif s:is_modified_and_added(from_count, to_count)
  220. call s:process_modified_and_added(modifications, from_count, to_count, to_line)
  221. call gitgutter#hunk#increment_lines_added(a:bufnr, to_count - from_count)
  222. call gitgutter#hunk#increment_lines_modified(a:bufnr, from_count)
  223. elseif s:is_modified_and_removed(from_count, to_count)
  224. call s:process_modified_and_removed(modifications, from_count, to_count, to_line)
  225. call gitgutter#hunk#increment_lines_modified(a:bufnr, to_count)
  226. call gitgutter#hunk#increment_lines_removed(a:bufnr, from_count - to_count)
  227. endif
  228. return modifications
  229. endfunction
  230. function! s:is_added(from_count, to_count) abort
  231. return a:from_count == 0 && a:to_count > 0
  232. endfunction
  233. function! s:is_removed(from_count, to_count) abort
  234. return a:from_count > 0 && a:to_count == 0
  235. endfunction
  236. function! s:is_modified(from_count, to_count) abort
  237. return a:from_count > 0 && a:to_count > 0 && a:from_count == a:to_count
  238. endfunction
  239. function! s:is_modified_and_added(from_count, to_count) abort
  240. return a:from_count > 0 && a:to_count > 0 && a:from_count < a:to_count
  241. endfunction
  242. function! s:is_modified_and_removed(from_count, to_count) abort
  243. return a:from_count > 0 && a:to_count > 0 && a:from_count > a:to_count
  244. endfunction
  245. function! s:process_added(modifications, from_count, to_count, to_line) abort
  246. let offset = 0
  247. while offset < a:to_count
  248. let line_number = a:to_line + offset
  249. call add(a:modifications, [line_number, 'added'])
  250. let offset += 1
  251. endwhile
  252. endfunction
  253. function! s:process_removed(modifications, from_count, to_count, to_line) abort
  254. if a:to_line == 0
  255. call add(a:modifications, [1, 'removed_first_line'])
  256. else
  257. call add(a:modifications, [a:to_line, 'removed'])
  258. endif
  259. endfunction
  260. function! s:process_modified(modifications, from_count, to_count, to_line) abort
  261. let offset = 0
  262. while offset < a:to_count
  263. let line_number = a:to_line + offset
  264. call add(a:modifications, [line_number, 'modified'])
  265. let offset += 1
  266. endwhile
  267. endfunction
  268. function! s:process_modified_and_added(modifications, from_count, to_count, to_line) abort
  269. let offset = 0
  270. while offset < a:from_count
  271. let line_number = a:to_line + offset
  272. call add(a:modifications, [line_number, 'modified'])
  273. let offset += 1
  274. endwhile
  275. while offset < a:to_count
  276. let line_number = a:to_line + offset
  277. call add(a:modifications, [line_number, 'added'])
  278. let offset += 1
  279. endwhile
  280. endfunction
  281. function! s:process_modified_and_removed(modifications, from_count, to_count, to_line) abort
  282. let offset = 0
  283. while offset < a:to_count
  284. let line_number = a:to_line + offset
  285. call add(a:modifications, [line_number, 'modified'])
  286. let offset += 1
  287. endwhile
  288. let a:modifications[-1] = [a:to_line + offset - 1, 'modified_removed']
  289. endfunction
  290. " Returns a diff for the current hunk.
  291. " Assumes there is only 1 current hunk unless the optional argument is given,
  292. " in which case the cursor is in two hunks and the argument specifies the one
  293. " to choose.
  294. "
  295. " Optional argument: 0 (to use the first hunk) or 1 (to use the second).
  296. function! gitgutter#diff#hunk_diff(bufnr, full_diff, ...)
  297. let modified_diff = []
  298. let hunk_index = 0
  299. let keep_line = 1
  300. " Don't keepempty when splitting because the diff we want may not be the
  301. " final one. Instead add trailing NL at end of function.
  302. for line in split(a:full_diff, '\n')
  303. let hunk_info = gitgutter#diff#parse_hunk(line)
  304. if len(hunk_info) == 4 " start of new hunk
  305. let keep_line = gitgutter#hunk#cursor_in_hunk(hunk_info)
  306. if a:0 && hunk_index != a:1
  307. let keep_line = 0
  308. endif
  309. let hunk_index += 1
  310. endif
  311. if keep_line
  312. call add(modified_diff, line)
  313. endif
  314. endfor
  315. return join(modified_diff, "\n")."\n"
  316. endfunction
  317. function! s:write_buffer(bufnr, file)
  318. let bufcontents = getbufline(a:bufnr, 1, '$')
  319. if bufcontents == [''] && line2byte(1) == -1
  320. " Special case: completely empty buffer.
  321. " A nearly empty buffer of only a newline has line2byte(1) == 1.
  322. call writefile([], a:file)
  323. return
  324. endif
  325. if getbufvar(a:bufnr, '&fileformat') ==# 'dos'
  326. call map(bufcontents, 'v:val."\r"')
  327. endif
  328. if getbufvar(a:bufnr, '&endofline')
  329. call add(bufcontents, '')
  330. endif
  331. let fenc = getbufvar(a:bufnr, '&fileencoding')
  332. if fenc !=# &encoding
  333. call map(bufcontents, 'iconv(v:val, &encoding, "'.fenc.'")')
  334. endif
  335. if getbufvar(a:bufnr, '&bomb')
  336. let bufcontents[0]=''.bufcontents[0]
  337. endif
  338. " The file we are writing to is a temporary file. Sometimes the parent
  339. " directory is deleted outside Vim but, because Vim caches the directory
  340. " name at startup and does not check for its existence subsequently, Vim
  341. " does not realise. This causes E482 errors.
  342. try
  343. call writefile(bufcontents, a:file, 'b')
  344. catch /E482/
  345. call mkdir(fnamemodify(a:file, ':h'), '', '0700')
  346. call writefile(bufcontents, a:file, 'b')
  347. endtry
  348. endfunction
  349. function! s:save_last_seen_change(bufnr) abort
  350. call gitgutter#utility#setbufvar(a:bufnr, 'tick', getbufvar(a:bufnr, 'changedtick'))
  351. endfunction