-
Notifications
You must be signed in to change notification settings - Fork 297
/
diff.vim
417 lines (348 loc) · 13.8 KB
/
diff.vim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
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
" 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 '.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! 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