diff --git a/autoload/fugitive.vim b/autoload/fugitive.vim index 97b96ff..5e70b58 100644 --- a/autoload/fugitive.vim +++ b/autoload/fugitive.vim @@ -1532,12 +1532,12 @@ function! fugitive#BufReadStatus() abort nunmap ~ nnoremap :execute StageNext(v:count1) nnoremap :execute StagePrevious(v:count1) - exe "nnoremap " nowait "- :execute StageToggle(line('.'),v:count)" - exe "xnoremap " nowait "- :execute StageToggle(line(\"'<\"),line(\"'>\")-line(\"'<\")+1)" - exe "nnoremap " nowait "s :execute StageToggleOnly('Unstaged',line('.'),v:count)" - exe "xnoremap " nowait "s :execute StageToggle(line(\"'<\"),line(\"'>\")-line(\"'<\")+1)" - exe "nnoremap " nowait "u :execute StageToggleOnly('Staged',line('.'),v:count)" - exe "xnoremap " nowait "u :execute StageToggle(line(\"'<\"),line(\"'>\")-line(\"'<\")+1)" + exe "nnoremap " nowait "- :execute Do('Toggle',0)" + exe "xnoremap " nowait "- :execute Do('Toggle',1)" + exe "nnoremap " nowait "s :execute Do('Stage',0)" + exe "xnoremap " nowait "s :execute Do('Stage',1)" + exe "nnoremap " nowait "u :execute Do('Unstage',0)" + exe "xnoremap " nowait "u :execute Do('Unstage',1)" nnoremap C :Gcommit:echohl WarningMsgecho ':Gstatus C is deprecated in favor of cc'echohl NONE nnoremap a :execute StageInline('toggle',line('.'),v:count) nnoremap i :execute StageInline('toggle',line('.'),v:count) @@ -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")' - 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 '' +function! s:DoToggleHeadHeader(value) abort + exe 'edit' s:fnameescape(b:git_dir) + call search('\C^index$', 'wc') 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:DoToggleUnpushedHeading(heading) abort + let remote = matchstr(a:heading, 'to \zs[^/]\+\ze/') + if empty(remote) + let remote = '.' + endif + 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: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 - return s:StageNext(a:count ? a:count : 1) + 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 -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