commit 26cf153e76f79063ae290db08fa741d7b1803b44 (tree)
parent 15fb5f68adc6a635cc9bca2f62c2843b1cdc8b3c
Author: Tim Pope <code@tpope.net>
Date: Thu, 14 Feb 2019 16:23:13 -0500
Refactor status spaghetti into dispatch mechanism
Diffstat:
| M | autoload/fugitive.vim | | | 368 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------ |
1 file changed, 257 insertions(+), 111 deletions(-)
diff --git a/autoload/fugitive.vim b/autoload/fugitive.vim
@@ -1532,12 +1532,12 @@ function! fugitive#BufReadStatus() abort
nunmap <buffer> ~
nnoremap <buffer> <silent> <C-N> :<C-U>execute <SID>StageNext(v:count1)<CR>
nnoremap <buffer> <silent> <C-P> :<C-U>execute <SID>StagePrevious(v:count1)<CR>
- exe "nnoremap <buffer> <silent>" nowait "- :<C-U>execute <SID>StageToggle(line('.'),v:count)<CR>"
- exe "xnoremap <buffer> <silent>" nowait "- :<C-U>execute <SID>StageToggle(line(\"'<\"),line(\"'>\")-line(\"'<\")+1)<CR>"
- exe "nnoremap <buffer> <silent>" nowait "s :<C-U>execute <SID>StageToggleOnly('Unstaged',line('.'),v:count)<CR>"
- exe "xnoremap <buffer> <silent>" nowait "s :<C-U>execute <SID>StageToggle(line(\"'<\"),line(\"'>\")-line(\"'<\")+1)<CR>"
- exe "nnoremap <buffer> <silent>" nowait "u :<C-U>execute <SID>StageToggleOnly('Staged',line('.'),v:count)<CR>"
- exe "xnoremap <buffer> <silent>" nowait "u :<C-U>execute <SID>StageToggle(line(\"'<\"),line(\"'>\")-line(\"'<\")+1)<CR>"
+ exe "nnoremap <buffer> <silent>" nowait "- :<C-U>execute <SID>Do('Toggle',0)<CR>"
+ exe "xnoremap <buffer> <silent>" nowait "- :<C-U>execute <SID>Do('Toggle',1)<CR>"
+ exe "nnoremap <buffer> <silent>" nowait "s :<C-U>execute <SID>Do('Stage',0)<CR>"
+ exe "xnoremap <buffer> <silent>" nowait "s :<C-U>execute <SID>Do('Stage',1)<CR>"
+ exe "nnoremap <buffer> <silent>" nowait "u :<C-U>execute <SID>Do('Unstage',0)<CR>"
+ exe "xnoremap <buffer> <silent>" nowait "u :<C-U>execute <SID>Do('Unstage',1)<CR>"
nnoremap <buffer> <silent> C :<C-U>Gcommit<CR>:echohl WarningMsg<Bar>echo ':Gstatus C is deprecated in favor of cc'<Bar>echohl NONE<CR>
nnoremap <buffer> <silent> a :<C-U>execute <SID>StageInline('toggle',line('.'),v:count)<CR>
nnoremap <buffer> <silent> i :<C-U>execute <SID>StageInline('toggle',line('.'),v:count)<CR>
@@ -2067,6 +2067,151 @@ function! s:StageInfo(...) abort
\ 'index': index}
endfunction
+function! s:Selection(arg1, ...) abort
+ if a:arg1 ==# 'n'
+ let arg1 = line('.')
+ let arg2 = v:count
+ elseif a:arg1 ==# 'v'
+ let arg1 = line("'<")
+ let arg2 = line("'>")
+ else
+ let arg1 = a:arg1
+ let arg2 = a:0 ? a:1 : 0
+ endif
+ let first = arg1
+ if arg2 < 0
+ let last = first - arg2 + 1
+ elseif arg2 > 0
+ let last = arg2
+ else
+ let last = first
+ endif
+ while getline(first) =~# '^$\|^[A-Z][a-z]'
+ let first += 1
+ endwhile
+ if first > last || &filetype !=# 'fugitive'
+ return []
+ endif
+ let flnum = first
+ while getline(flnum) =~# '^[ @\+-]'
+ let flnum -= 1
+ endwhile
+ let slnum = flnum + 1
+ let section = ''
+ let index = 0
+ while len(getline(slnum - 1)) && empty(section)
+ let slnum -= 1
+ let heading = matchstr(getline(slnum), '^\u\l\+.* (\d\+)$')
+ if empty(heading) && getline(slnum) !~# '^[ @\+-]'
+ let index += 1
+ endif
+ endwhile
+ let results = []
+ let template = {
+ \ 'heading': heading,
+ \ 'section': matchstr(heading, '^\u\l\+\ze.* (\d\+)$'),
+ \ 'filename': '',
+ \ 'paths': [],
+ \ 'commit': '',
+ \ 'status': '',
+ \ 'patch': 0,
+ \ 'index': index}
+ let line = getline(flnum)
+ let lnum = first - (arg1 == flnum ? 0 : 1)
+ let root = s:Tree() . '/'
+ while lnum <= last
+ if line =~# '^\u\l\+\ze.* (\d\+)$'
+ let template.heading = getline(lnum)
+ let template.section = matchstr(template.heading, '^\u\l\+\ze.* (\d\+)$')
+ let template.index = 0
+ elseif line =~# '^[ @\+-]'
+ let template.index -= 1
+ if !results[-1].patch
+ let results[-1].patch = lnum
+ endif
+ let results[-1].lnum = lnum
+ elseif line =~# '^[A-Z?] '
+ let filename = matchstr(line, '^[A-Z?] \zs.*')
+ call add(results, extend(deepcopy(template), {
+ \ 'lnum': lnum,
+ \ 'filename': filename,
+ \ 'paths': map(reverse(split(filename, ' -> ')), 'root . v:val'),
+ \ 'status': matchstr(line, '^[A-Z?]'),
+ \ }))
+ elseif line =~# '^\x\x\x\+ '
+ call add(results, extend({
+ \ 'lnum': lnum,
+ \ 'commit': matchstr(line, '^\x\x\x\+'),
+ \ }, template, 'keep'))
+ elseif line =~# '^\l\+ \x\x\x\+ '
+ call add(results, extend({
+ \ 'lnum': lnum,
+ \ 'commit': matchstr(line, '^\l\+ \zs\x\x\x\+'),
+ \ 'status': matchstr(line, '^\l\+'),
+ \ }, template, 'keep'))
+ endif
+ let lnum += 1
+ let template.index += 1
+ let line = getline(lnum)
+ endwhile
+ if len(results) && results[0].patch && arg2 == 0
+ while getline(results[0].patch) =~# '^[ \+-]'
+ let results[0].patch -= 1
+ endwhile
+ while getline(results[0].lnum + 1) =~# '^[ \+-]'
+ let results[0].lnum += 1
+ endwhile
+ endif
+ return results
+endfunction
+
+function! s:Do(action, visual) abort
+ let line = getline('.')
+ if !a:0 && !v:count && line =~# '^[A-Z][a-z]'
+ let header = matchstr(line, '^\S\+\ze:')
+ if len(header) && exists('*s:Do' . a:action . header . 'Header')
+ call s:Do{a:action}{header}Header(matchstr(line, ': \zs.*'))
+ endif
+ let section = matchstr(line, '^\S\+')
+ if exists('*s:Do' . a:action . section . 'Heading')
+ call s:Do{a:action}{section}Heading(line)
+ return s:ReloadStatus()
+ endif
+ endif
+ let selection = s:Selection(a:visual ? 'v' : 'n')
+ if empty(selection)
+ return ''
+ endif
+ call filter(selection, 'v:val.section ==# selection[0].section')
+ let reload = 0
+ let status = 0
+ let err = ''
+ try
+ for record in selection
+ if exists('*s:Do' . a:action . record.section)
+ let status = s:Do{a:action}{record.section}(record)
+ else
+ continue
+ endif
+ if !status
+ return ''
+ endif
+ let reload = reload || (status > 0)
+ endfor
+ if status < 0
+ execute record.lnum + 1
+ endif
+ call s:StageReveal()
+ catch /^fugitive:/
+ return 'echoerr v:errmsg'
+ finally
+ if reload
+ execute s:ReloadStatus()
+ endif
+ endtry
+ return ''
+endfunction
+
function! s:StageReveal(...) abort
let begin = a:0 ? a:1 : line('.')
if getline(begin) =~# '^@'
@@ -2213,28 +2358,14 @@ function! s:StageDiffEdit() abort
endif
endfunction
-function! s:StageApply(info, lnum1, count, reverse, extra) abort
- let cmd = ['apply', '-p0'] + a:extra
+function! s:StageApply(info, reverse, extra) abort
+ let cmd = ['apply', '-p0', '--recount'] + a:extra
let info = a:info
- let start = a:lnum1
- let end = start
- if !a:count
- while start > 0 && getline(start) !~# '^@@'
- let start -= 1
- endwhile
- while getline(end + 1) =~# '^[-+ ]'
- let end += 1
- endwhile
- else
- let end = a:lnum1 + a:count - 1
- call add(cmd, '--recount')
- endif
+ let start = info.patch
+ let end = info.lnum
let lines = getline(start, end)
if empty(filter(copy(lines), 'v:val =~# "^[+-]"'))
- return ''
- endif
- if len(filter(copy(lines), 'v:val !~# "^[ @\+-]"'))
- return 'fugitive: cannot apply hunks across multiple files'
+ return -1
endif
while getline(end) =~# '^[-+ ]'
let end += 1
@@ -2251,7 +2382,7 @@ function! s:StageApply(info, lnum1, count, reverse, extra) abort
endif
endwhile
if start == 0 || getline(start) !~# '^@@ '
- return "fugitive: could not find hunk"
+ call s:throw("could not find hunk")
endif
let i = b:fugitive_expanded[info.section][info.filename][0]
let head = []
@@ -2267,30 +2398,34 @@ function! s:StageApply(info, lnum1, count, reverse, extra) abort
endif
call extend(cmd, ['--', temp])
let output = call('s:TreeChomp', cmd)
- return v:shell_error ? output : ''
+ if !v:shell_error
+ return 1
+ endif
+ call s:throw(output)
endfunction
function! s:StageDelete(lnum, count) abort
- let info = s:StageInfo(a:lnum)
+ let info = get(s:Selection(a:lnum, -a:count), 0, {'filename': ''})
if empty(info.filename)
return ''
endif
- let hash = s:TreeChomp('hash-object', '-w', './' . info.filename)
+ let hash = s:TreeChomp('hash-object', '-w', '--', info.paths[0])
if empty(hash)
return ''
- elseif info.offset >= 0
- let output = s:StageApply(info, a:lnum, a:count, 1, info.section ==# 'Staged' ? ['--index'] : [])
- if len(output)
- return 'echoerr ' . string(output)
- endif
+ elseif info.patch
+ try
+ call s:StageApply(info, 1, info.section ==# 'Staged' ? ['--index'] : [])
+ catch /^fugitive:/
+ return 'echoerr v:errmsg'
+ endtry
elseif info.status ==# 'U'
- call s:TreeChomp('rm', './' . info.filename)
+ call s:TreeChomp('rm', '--', info.paths[0])
elseif info.status ==# '?'
- call s:TreeChomp('clean', '-f', './' . info.filename)
+ call s:TreeChomp('clean', '-f', '--', info.paths[0])
elseif info.section ==# 'Unstaged'
- call s:TreeChomp('checkout', './' . info.filename)
+ call s:TreeChomp('checkout', '--', info.paths[0])
else
- call s:TreeChomp('checkout', 'HEAD^{}', './' . info.filename)
+ call s:TreeChomp('checkout', 'HEAD^{}', '--', info.paths[0])
endif
exe s:ReloadStatus()
let @@ = hash
@@ -2298,80 +2433,91 @@ function! s:StageDelete(lnum, count) abort
\ string('To restore, :Git cat-file blob '.hash[0:6].' > '.info.filename)
endfunction
-function! s:StageToggle(lnum1, count) abort
- if a:lnum1 == 1 && a:count == 0
- return 'Gedit .git/|call search("^index$", "wc")'
+function! s:DoToggleHeadHeader(value) abort
+ exe 'edit' s:fnameescape(b:git_dir)
+ call search('\C^index$', 'wc')
+endfunction
+
+function! s:DoToggleUnpushedHeading(heading) abort
+ let remote = matchstr(a:heading, 'to \zs[^/]\+\ze/')
+ if empty(remote)
+ let remote = '.'
endif
- try
- let info = s:StageInfo(a:lnum1)
- if empty(info.filename)
- if info.section ==# 'Staged'
- call s:TreeChomp('reset','-q')
- silent! edit!
- 1
- call search('^Unstaged','W')
- return ''
- elseif info.section ==# 'Unstaged'
- call s:TreeChomp('add','.')
- silent! edit!
- 1
- call search('^Staged','W')
- return ''
- elseif info.section ==# 'Unpushed' && len(info.commit)
- let remote = matchstr(info.heading, 'to \zs[^/]\+\ze/')
- if empty(remote)
- let remote = '.'
- endif
- let branch = matchstr(info.heading, 'to \%([^/]\+/\)\=\zs\S\+')
- call feedkeys(':Gpush ' . remote . ' ' . info.commit . ':' . branch)
- return ''
- elseif info.section ==# 'Unpulled'
- call feedkeys(':Grebase ' . info.commit)
- return ''
- endif
- elseif info.offset >= 0
- let output = s:StageApply(info, a:lnum1, a:count, info.section ==# 'Staged', ['--cached'])
- return len(output) ? 'redraw|echoerr ' . string(output) : s:ReloadStatus()
- endif
- let lnum2 = a:count ? a:lnum1 + a:count - 1 : a:lnum1
- for lnum in range(a:lnum1, lnum2)
- let info = s:StageInfo(lnum)
- let filename = info.filename
- if empty(filename) || info.offset >= 0
- continue
- endif
- execute lnum
- if info.section ==# 'Staged'
- let files_to_unstage = split(filename, ' -> ')
- let filename = files_to_unstage[-1]
- let cmd = ['reset', '-q', '--'] + map(copy(files_to_unstage), 's:Tree() . "/" . v:val')
- elseif getline(lnum) =~# '^D'
- let cmd = ['rm', './' . filename]
- elseif getline(lnum) =~# '^M'
- let cmd = ['add', './' . filename]
- else
- let cmd = ['add','-A', './' . filename]
- endif
- if !exists('target')
- let target = [filename, info.section ==# 'Staged' ? '' : 'Staged']
- endif
- call call('s:TreeChomp', cmd)
- endfor
- if exists('target')
- exe s:ReloadStatus()
- endif
- catch /^fugitive:/
- return 'echoerr v:errmsg'
- endtry
- return ''
+ let branch = matchstr(a:heading, 'to \%([^/]\+/\)\=\zs\S\+')
+ call feedkeys(':Gpush ' . remote . ' ' . 'HEAD:' . branch)
+endfunction
+
+function! s:DoToggleUnpushed(record) abort
+ let remote = matchstr(a:record.heading, 'to \zs[^/]\+\ze/')
+ if empty(remote)
+ let remote = '.'
+ endif
+ let branch = matchstr(a:record.heading, 'to \%([^/]\+/\)\=\zs\S\+')
+ call feedkeys(':Gpush ' . remote . ' ' . a:record.commit . ':' . branch)
+endfunction
+
+function! s:DoToggleUnpulledHeading(heading) abort
+ call feedkeys(':Grebase')
endfunction
-function! s:StageToggleOnly(section, lnum1, count) abort
- let info = s:StageInfo(a:lnum1)
- if info.section ==# a:section
- return s:StageToggle(a:lnum1, a:count)
+function! s:DoToggleUnpulled(record) abort
+ call feedkeys(':Grebase ' . a:record.commit)
+endfunction
+
+function! s:DoToggleStagedHeading(...) abort
+ call s:TreeChomp('reset', '-q')
+ return 1
+endfunction
+
+function! s:DoUnstageStagedHeading(heading) abort
+ return s:DoToggleStagedHeading(a:heading)
+endfunction
+
+function! s:DoToggleUnstagedHeading(...) abort
+ call s:TreeChomp('add', '-u')
+ return 1
+endfunction
+
+function! s:DoStageUnstagedHeading(heading) abort
+ return s:DoToggleUnstagedHeading(a:heading)
+endfunction
+
+function! s:DoToggleStaged(record) abort
+ if a:record.patch
+ return s:StageApply(a:record, 1, ['--cached'])
+ else
+ call s:TreeChomp(['reset', '-q', '--'] + a:record.paths)
+ return 1
+ endif
+endfunction
+
+function! s:DoStageStaged(record) abort
+ return -1
+endfunction
+
+function! s:DoUnstageStaged(record) abort
+ return s:DoToggleStaged(a:record)
+endfunction
+
+function! s:DoToggleUnstaged(record) abort
+ if a:record.patch
+ return s:StageApply(a:record, 0, ['--cached'])
+ else
+ call s:TreeChomp(['add', '-A', '--'] + a:record.paths)
+ return 1
+ endif
+endfunction
+
+function! s:DoStageUnstaged(record) abort
+ return s:DoToggleUnstaged(a:record)
+endfunction
+
+function! s:DoUnstageUnstaged(record) abort
+ if a:record.status ==# 'A'
+ call s:TreeChomp(['reset', '-q', '--'] + a:record.paths)
+ return 1
else
- return s:StageNext(a:count ? a:count : 1)
+ return -1
endif
endfunction
@@ -4084,7 +4230,7 @@ function! fugitive#MapJumps(...) abort
endif
endfunction
-function! s:StatusCfile(...) abort
+function! s:DoCfile(...) abort
let tree = FugitiveTreeForGitDir(b:git_dir)
let lead = s:cpath(tree, getcwd()) ? './' : tree . '/'
let info = s:StageInfo()
@@ -4109,7 +4255,7 @@ function! s:StatusCfile(...) abort
endfunction
function! fugitive#StatusCfile() abort
- let file = s:Generate(s:StatusCfile()[0])
+ let file = s:Generate(s:DoCfile()[0])
return empty(file) ? fugitive#Cfile() : s:fnameescape(file)
endfunction
@@ -4296,7 +4442,7 @@ endfunction
function! s:GF(mode) abort
try
- let results = &filetype ==# 'fugitive' ? s:StatusCfile() : &filetype ==# 'gitcommit' ? [s:MessageCfile()] : s:cfile()
+ let results = &filetype ==# 'fugitive' ? s:DoCfile() : &filetype ==# 'gitcommit' ? [s:MessageCfile()] : s:cfile()
catch /^fugitive:/
return 'echoerr v:errmsg'
endtry