diff --git a/autoload/fugitive.vim b/autoload/fugitive.vim index c1e32af..f9d7ed5 100644 --- a/autoload/fugitive.vim +++ b/autoload/fugitive.vim @@ -1368,43 +1368,158 @@ function! s:ReplaceCmd(cmd, ...) abort endtry endfunction +function! s:QueryLog(refspec) abort + let lines = split(system(FugitivePrepare('log', '-n', '256', '--format=%h%x09%s', a:refspec, '--')), "\n") + call map(lines, 'split(v:val, "\t")') + call map(lines, '{"type": "Log", "commit": v:val[0], "subject": v:val[-1]}') + return lines +endfunction + +function! s:FormatLog(dict) abort + return a:dict.commit . ' ' . a:dict.subject +endfunction + +function! s:FormatFile(dict) abort + return a:dict.status . ' ' . a:dict.filename +endfunction + +function! s:Format(val) abort + if type(a:val) == type({}) + return s:Format{a:val.type}(a:val) + elseif type(a:val) == type([]) + return map(copy(a:val), 's:Format(v:val)') + else + return '' . a:val + endif +endfunction + +function! s:AddHeader(key, value) abort + if empty(a:value) + return + endif + let before = 1 + while !empty(getline(before)) + let before += 1 + endwhile + call append(before - 1, [a:key . ':' . (len(a:value) ? ' ' . a:value : '')]) + if before == 1 && line('$') == 2 + silent 2delete _ + endif +endfunction + +function! s:AddSection(label, lines, ...) abort + let note = a:0 ? a:1 : '' + if empty(a:lines) && empty(note) + return + endif + call append(line('$'), ['', a:label . (len(note) ? ': ' . note : ' (' . len(a:lines) . ')')] + s:Format(a:lines)) +endfunction + function! fugitive#BufReadStatus() abort let amatch = s:Slash(expand('%:p')) - if !exists('b:fugitive_display_format') - let b:fugitive_display_format = filereadable(expand('%').'.lock') - endif - let b:fugitive_display_format = b:fugitive_display_format % 2 let b:fugitive_type = 'index' try silent doautocmd BufReadPre let cmd = [fnamemodify(amatch, ':h')] - setlocal noro ma nomodeline + setlocal noro ma nomodeline buftype=nowrite if s:cpath(fnamemodify($GIT_INDEX_FILE !=# '' ? $GIT_INDEX_FILE : b:git_dir . '/index', ':p')) !=# s:cpath(amatch) let cmd += ['-c', 'GIT_INDEX_FILE=' . amatch] endif - if b:fugitive_display_format - let cmd += ['ls-files', '--stage'] - else - let cmd += [ - \ '-c', 'status.displayCommentPrefix=true', - \ '-c', 'color.status=false', - \ '-c', 'status.short=false', - \ 'status'] + let cmd += ['status', '--porcelain', '-bz'] + let output = split(system(fugitive#Prepare(cmd)), "\1") + if v:shell_error + throw 'fugitive: ' . join(output, ' ') endif - call s:ReplaceCmd(call('fugitive#Prepare', cmd), 1) + + let head = matchstr(output[0], '^## \zs\S\+\ze\%($\| \[\)') + let pull = '' + if head =~# '\.\.\.' + let [head, pull] = split(head, '\.\.\.') + let branch = head + elseif head ==# 'HEAD' || empty(head) + let head = FugitiveHead(11) + let branch = '' + else + let branch = head + endif + + let [staged, unstaged, untracked] = [[], [], []] + let i = 0 + while i < len(output) + let line = output[i] + let file = line[3:-1] + let files = file + let i += 1 + if line[0:1] =~# '[RC]' + let files = output[i] . ' -> ' . file + let i += 1 + endif + if line[0] ==# '?' + call add(untracked, {'type': 'File', 'status': '?', 'filename': file}) + continue + endif + if line[0] !~# '[ ?!#]' + call add(staged, {'type': 'File', 'status': line[0], 'filename': (line[0] =~# '[RC]' ? files : file)}) + endif + if line[1] !~# '[ ?!#]' + call add(unstaged, {'type': 'File', 'status': line[1], 'filename': (line[1] =~# '[RC]' ? files : file)}) + endif + endwhile + + let config = fugitive#Config() + + if len(pull) + let rebase = fugitive#Config('branch.' . branch . '.rebase', config) + if empty(rebase) + let rebase = fugitive#Config('pull.rebase', config) + endif + if rebase =~# '^\%(true\|yes\|on\|1\|interactive\)$' + let pull_type = 'Rebase' + elseif rebase =~# '^\%(false\|no|off\|0\|\)$' + let pull_type = 'Merge' + else + let pull_type = 'Upstream' + endif + endif + + let push_remote = fugitive#Config('branch.' . branch . '.pushRemote', config) + if empty(push_remote) + let push_remote = fugitive#Config('remote.pushDefault', config) + endif + let push = len(push_remote) && len(branch) ? push_remote . '/' . branch : '' + if empty(push) + let push = pull + endif + + if len(pull) + let unpulled = s:QueryLog(head . '..' . pull) + else + let unpulled = [] + endif + if len(push) + let unpushed = s:QueryLog(push . '..' . head) + else + let unpushed = [] + endif + + silent keepjumps %delete_ + + call s:AddHeader('Head', head) + call s:AddHeader(pull_type, pull) + if push !=# pull + call s:AddHeader('Push', push) + endif + call s:AddSection('Untracked', untracked) + call s:AddSection('Unstaged', unstaged) + call s:AddSection('Staged', staged) + call s:AddSection('Unpushed to ' . push, unpushed) + call s:AddSection('Unpulled from ' . pull, unpulled) + + set nomodified readonly noswapfile silent doautocmd BufReadPost - if b:fugitive_display_format - if &filetype !=# 'git' - set filetype=git - endif - set nospell - else - if &filetype !=# 'gitcommit' - set filetype=gitcommit - endif - set foldtext=fugitive#Foldtext() - endif - setlocal readonly nomodifiable nomodified noswapfile + set foldtext=fugitive#Foldtext() + set filetype=fugitive + setlocal nomodifiable if &bufhidden ==# '' setlocal bufhidden=delete endif @@ -1420,8 +1535,6 @@ function! fugitive#BufReadStatus() abort exe "xnoremap " nowait "s :silent execute StageToggle(line(\"'<\"),line(\"'>\"))" exe "nnoremap " nowait "u :silent execute StageToggle(line('.'),line('.')+v:count1-1)" exe "xnoremap " nowait "u :silent execute StageToggle(line(\"'<\"),line(\"'>\"))" - nnoremap a :let b:fugitive_display_format += 1exe fugitive#BufReadStatus() - nnoremap i :let b:fugitive_display_format -= 1exe fugitive#BufReadStatus() nnoremap C :Gcommit:echohl WarningMsgecho ':Gstatus C is deprecated in favor of cc'echohl NONE nnoremap ca :Gcommit --amend nnoremap cc :Gcommit @@ -1772,7 +1885,7 @@ function! s:Status(bang, count, mods) abort return '' endfunction -function! fugitive#ReloadStatus() abort +function! fugitive#ReloadStatus(...) abort if exists('s:reloading_status') return endif @@ -1789,7 +1902,9 @@ function! fugitive#ReloadStatus() abort endif try if !&modified + let pos = getpos('.') call fugitive#BufReadStatus() + call setpos('.', pos) endif finally if exists('restorewinnr') @@ -1805,39 +1920,30 @@ function! fugitive#ReloadStatus() abort endtry endfunction -function! fugitive#reload_status() abort - return fugitive#ReloadStatus() +function! s:StageInfo(...) abort + let lnum = a:0 ? a:1 : line('.') + let slnum = lnum + let section = '' + while len(getline(slnum - 1)) && empty(section) + let slnum -= 1 + let section = matchstr(getline(slnum), '^\u\l\+\ze.* (\d\+)$') + endwhile + return {'section': section, + \ 'heading': getline(slnum), + \ 'filename': matchstr(getline(lnum), '^[A-Z?] \zs.*'), + \ 'commit': matchstr(getline(lnum), '^[0-9a-f]\{4,\}\ze '), + \ 'status': matchstr(getline(lnum), '^[A-Z?]\ze '), + \ 'index': lnum - slnum} endfunction function! s:StageFileSection(lnum) abort - let filename = matchstr(getline(a:lnum),'^.\=\t\zs.\{-\}\ze\%( ([^()[:digit:]]\+)\)\=$') - let lnum = a:lnum - if has('multi_byte_encoding') - let colon = '\%(:\|\%uff1a\)' - else - let colon = ':' - endif - while lnum && getline(lnum) !~# colon.'$' - let lnum -= 1 - endwhile - if !lnum - return ['', ''] - elseif (getline(lnum+1) =~# '^.\= .*\ '.filename) + \ string('To restore, :Git cat-file blob '.hash[0:6].' > '.info.filename) endif endfunction function! s:StageDiff(diff) abort let [filename, section] = s:StageFileSection(line('.')) - if filename ==# '' && section ==# 'staged' + if filename ==# '' && section ==# 'Staged' return 'Git! diff --no-ext-diff --cached' elseif filename ==# '' return 'Git! diff --no-ext-diff' @@ -1901,7 +2017,7 @@ function! s:StageDiff(diff) abort let [old, new] = split(filename,' -> ') execute 'Gedit '.s:fnameescape(':0:'.new) return a:diff.' HEAD:'.s:fnameescape(old) - elseif section ==# 'staged' + elseif section ==# 'Staged' execute 'Gedit '.s:fnameescape(':0:'.filename) return a:diff.' -' else @@ -1913,18 +2029,18 @@ endfunction function! s:StageDiffEdit() abort let [filename, section] = s:StageFileSection(line('.')) let arg = (filename ==# '' ? '.' : filename) - if section ==# 'staged' + if section ==# 'Staged' return 'Git! diff --no-ext-diff --cached '.s:shellesc(arg) - elseif section ==# 'untracked' + elseif section ==# 'Untracked' call s:TreeChomp('add', '--intent-to-add', './' . arg) if arg ==# '.' silent! edit! 1 - if !search('^.*:\n.*\n.\= .*"git checkout \|^\%(# \)=Changes not staged for commit:$','W') + if !search('^Unstaged','W') call search(':$','W') endif else - call s:StageReloadSeek([filename, 'staged'], line('.'), line('.')) + call s:StageReloadSeek([filename, 'Staged'], line('.'), line('.')) endif return '' else @@ -1939,49 +2055,61 @@ function! s:StageToggle(lnum1,lnum2) abort try let output = '' for lnum in range(a:lnum1,a:lnum2) - let [filename, section] = s:StageFileSection(lnum) - if getline('.') =~# ':$' - if section ==# 'staged' + let info = s:StageInfo(lnum) + if empty(info.filename) + if info.section ==# 'Staged' call s:TreeChomp('reset','-q') silent! edit! 1 - if !search('^.*:\n.\= .*"git add .*\n#\n\|^\%(. \)\=Untracked files:$','W') - call search(':$','W') + if !search('^Untracked','W') + call search('^Unstaged','W') endif return '' - elseif section ==# 'unstaged' + elseif info.section ==# 'Unstaged' call s:TreeChomp('add','-u') silent! edit! 1 - if !search('^.*:\n\.\= .*"git add .*\n#\n\|^\%( \)=Untracked files:$','W') - call search(':$','W') + if !search('^Untracked','W') + call search('^Staged','W') endif return '' - else + 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 '' + elseif info.section ==# 'Untracked' call s:TreeChomp('add', '.') silent! edit! 1 - call search(':$','W') + call search('^Unstaged\|^Staged','W') return '' endif endif - if filename ==# '' + let filename = info.filename + if empty(filename) continue endif execute lnum - if section ==# 'staged' + 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), '"./" . v:val') - elseif getline(lnum) =~# '^.\=\tdeleted:' + elseif getline(lnum) =~# '^D' let cmd = ['rm', './' . filename] - elseif getline(lnum) =~# '^.\=\tmodified:' + elseif getline(lnum) =~# '^M' let cmd = ['add', './' . filename] else let cmd = ['add','-A', './' . filename] endif if !exists('target') - let target = [filename, section ==# 'staged' ? '' : 'staged'] + let target = [filename, info.section ==# 'Staged' ? '' : 'Staged'] endif let output .= call('s:TreeChomp', cmd)."\n" endfor @@ -2001,11 +2129,11 @@ function! s:StagePatch(lnum1,lnum2) abort for lnum in range(a:lnum1,a:lnum2) let [filename, section] = s:StageFileSection(lnum) - if getline('.') =~# ':$' && section ==# 'staged' + if empty(filename) && section ==# 'Staged' return 'Git reset --patch' - elseif getline('.') =~# ':$' && section ==# 'unstaged' + elseif empty(filename) && section ==# 'Unstaged' return 'Git add --patch' - elseif getline('.') =~# ':$' && section ==# 'untracked' + elseif empty(filename) && section ==# 'Untracked' return 'Git add -N .' elseif filename ==# '' continue @@ -2016,7 +2144,7 @@ function! s:StagePatch(lnum1,lnum2) abort execute lnum if filename =~ ' -> ' let reset += [split(filename,' -> ')[1]] - elseif section ==# 'staged' + elseif section ==# 'Staged' let reset += [filename] elseif getline(lnum) !~# '^.\=\tdeleted:' let add += [filename] @@ -2033,7 +2161,7 @@ function! s:StagePatch(lnum1,lnum2) abort silent! edit! 1 redraw - call search('^.\=\t\%([[:alpha:] ]\+: *\)\=\V'.first_filename.'\%( ([^()[:digit:]]\+)\)\=\$','W') + call search('^[A-Z?] \V'.first_filename.'\$','W') endif catch /^fugitive:/ return 'echoerr v:errmsg' @@ -3531,7 +3659,11 @@ function! s:ContainingCommit() abort endfunction function! s:SquashArgument() abort - return s:Owner(@%) + if &filetype == 'fugitive' + return matchstr(getline('.'), '^[0-9a-f]\{4,\}\ze ') + else + return s:Owner(@%) + endif endfunction function! s:NavigateUp(count) abort @@ -3590,7 +3722,26 @@ function! fugitive#MapJumps(...) abort endfunction function! s:StatusCfile(...) abort - let pre = '' + let tree = FugitiveTreeForGitDir(b:git_dir) + let lead = s:cpath(tree, getcwd()) ? './' : tree . '/' + let line = getline('.') + if line =~# '^\S ' + return lead . line[2:-1] + elseif line =~# '^[0-9a-f]\{4,\}\s' + return matchstr(line, '^\S\+') + elseif line =~# '^\%(Head\|Merge\|Rebase\|Upstream\|Pull\|Push\): ' + return matchstr(line, ' \zs.*') + else + return '' + endif +endfunction + +function! fugitive#StatusCfile() abort + let file = s:Generate(s:StatusCfile()) + return empty(file) ? fugitive#Cfile() : s:fnameescape(file) +endfunction + +function! s:MessageCfile(...) abort let tree = FugitiveTreeForGitDir(b:git_dir) let lead = s:cpath(tree, getcwd()) ? './' : tree . '/' if getline('.') =~# '^.\=\trenamed:.* -> ' @@ -3612,8 +3763,8 @@ function! s:StatusCfile(...) abort endif endfunction -function! fugitive#StatusCfile() abort - let file = s:Generate(s:StatusCfile()) +function! fugitive#MessageCfile() abort + let file = s:Generate(s:MessageCfile()) return empty(file) ? fugitive#Cfile() : s:fnameescape(file) endfunction @@ -3773,7 +3924,7 @@ endfunction function! s:GF(mode) abort try - let results = &filetype ==# 'gitcommit' ? [s:StatusCfile()] : s:cfile() + let results = &filetype ==# 'fugitive' ? [s:StatusCfile()] : &filetype ==# 'gitcommit' ? s:MessageCfile() : s:cfile() catch /^fugitive:/ return 'echoerr v:errmsg' endtry diff --git a/plugin/fugitive.vim b/plugin/fugitive.vim index bff7702..ca67b57 100644 --- a/plugin/fugitive.vim +++ b/plugin/fugitive.vim @@ -256,6 +256,10 @@ augroup fugitive \ call fugitive#MapCfile() | \ endif autocmd FileType gitcommit + \ if exists('b:git_dir') | + \ call fugitive#MapCfile('fugitive#MessageCfile()') | + \ endif + autocmd FileType fugitive \ if exists('b:git_dir') | \ call fugitive#MapCfile('fugitive#StatusCfile()') | \ endif diff --git a/syntax/fugitive.vim b/syntax/fugitive.vim new file mode 100644 index 0000000..fe58e9b --- /dev/null +++ b/syntax/fugitive.vim @@ -0,0 +1,30 @@ +if exists("b:current_syntax") + finish +endif + +syn sync fromstart +syn spell notoplevel + +syn include @fugitiveDiff syntax/diff.vim + +syn match fugitiveHeader /^[A-Z][a-z][^:]*:/ nextgroup=fugitiveHash,fugitiveSymbolicRef skipwhite + +syn region fugitiveSection start=/^\%(.*(\d\+)$\)\@=/ contains=fugitiveHeading end=/^$\@=/ +syn match fugitiveHeading /^[A-Z][a-z][^:]*\ze (\d\+)$/ contains=fugitivePreposition contained nextgroup=fugitiveCount skipwhite +syn match fugitiveCount /(\d\+)/hs=s+1,he=e-1 contained +syn match fugitivePreposition /\<\%([io]nto\|from\|to\)\>/ transparent contained nextgroup=fugitiveHash,fugitiveSymbolicRef skipwhite + +syn match fugitiveModifier /^[MADRCU?]\{1,2} / contained containedin=fugitiveSection +syn match FugitiveSymbolicRef /\.\@!\%(\.\.\@!\|[^[:space:][:cntrl:]\:.]\)\+\.\@/ contained containedin=fugitiveSection + +syn region fugitiveHunk start=/^\%(@@ -\)\@=/ end=/^\%(diff --\%(git\|cc\|combined\) \|@@\|$\)\@=/ contains=@fugitiveDiff containedin=fugitiveSection fold + +hi link fugitiveModifier Type +hi link fugitiveHeader Label +hi link fugitiveHeading PreProc +hi link fugitiveHash Identifier +hi link fugitiveSymbolicRef Function +hi link fugitiveCount Number + +let b:current_syntax = "fugitive"