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
|
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
|