aboutsummaryrefslogtreecommitdiff
path: root/vim/bundle/vim-gitgutter/autoload/gitgutter/diff.vim
blob: 170193bfe2c08481bb092dfeb32ca09d79f84f27 (plain)
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