"AUTHOR: Greg Sexton "WEBSITE: http://www.gregsexton.org/portfolio/gitv/ "LICENSE: Same terms as Vim itself (see :help license). "NOTES: Much of the credit for gitv goes to Tim Pope and the fugitive plugin " where this plugin either uses functionality directly or was inspired heavily. if exists("g:loaded_gitv") || v:version < 700 finish endif let g:loaded_gitv = 1 let s:savecpo = &cpo set cpo&vim "configurable options: "g:Gitv_CommitStep - int "g:Gitv_OpenHorizontal - {0,1,'AUTO'} "g:Gitv_GitExecutable - string "g:Gitv_WipeAllOnClose - int "g:Gitv_WrapLines - {0,1} "g:Gitv_TruncateCommitSubjects - {0,1} if !exists("g:Gitv_CommitStep") let g:Gitv_CommitStep = &lines endif if !exists('g:Gitv_GitExecutable') let g:Gitv_GitExecutable = 'git' endif if !exists('g:Gitv_WipeAllOnClose') let g:Gitv_WipeAllOnClose = 0 "default for safety endif if !exists('g:Gitv_WrapLines') let g:Gitv_WrapLines = 0 endif if !exists('g:Gitv_TruncateCommitSubjects') let g:Gitv_TruncateCommitSubjects = 0 endif "this counts up each time gitv is opened to ensure a unique file name let g:Gitv_InstanceCounter = 0 let s:localUncommitedMsg = '* Local uncommitted changes, not checked in to index.' let s:localCommitedMsg = '* Local changes checked in to index but not committed.' command! -nargs=* -bang Gitv call s:OpenGitv(shellescape(), 0) cabbrev gitv Gitv "Public API:"{{{ fu! Gitv_OpenGitCommand(command, windowCmd, ...) "{{{ "returns 1 if command succeeded with output "optional arg is a flag, if present runs command verbatim "this function is not limited to script scope as is useful for running other commands. "e.g call Gitv_OpenGitCommand("diff --no-color", 'vnew') is useful for getting an overall git diff. let [result, finalCmd] = s:RunGitCommand(a:command, a:0) if type(result) == type(0) return 0 endif if type(result) == type("") && result == "" echom "No output." return 0 else if a:windowCmd == '' silent setlocal modifiable silent setlocal noreadonly 1,$ d else let goBackTo = winnr() let dir = s:GetRepoDir() let workingDir = fnamemodify(dir,':h') let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' let bufferDir = getcwd() let tempSplitBelow = &splitbelow let tempSplitRight = &splitright try set nosplitbelow set nosplitright execute cd.'`=workingDir`' exec a:windowCmd let newWindow = winnr() finally exec goBackTo . 'wincmd w' execute cd.'`=bufferDir`' if exists('newWindow') exec newWindow . 'wincmd w' endif exec 'set '. (tempSplitBelow ? '' : 'no') . 'splitbelow' exec 'set '. (tempSplitRight ? '' : 'no') . 'splitright' endtry endif if !(&modifiable) return 0 endif let b:Git_Command = finalCmd silent setlocal ft=git silent setlocal buftype=nofile silent setlocal nobuflisted silent setlocal noswapfile silent setlocal bufhidden=wipe silent setlocal nonumber if g:Gitv_WrapLines silent setlocal wrap else silent setlocal nowrap endif silent setlocal fdm=syntax silent setlocal foldlevel=0 nmap q :q! nmap u :if exists('b:Git_Command')call Gitv_OpenGitCommand(b:Git_Command, '', 1)endif call append(0, split(result, '\n')) "system converts eols to \n regardless of os. silent setlocal nomodifiable silent setlocal readonly 1 return 1 endif endf "}}} }}} "General Git Functions: "{{{ fu! s:RunGitCommand(command, verbatim) "{{{ "if verbatim returns result of system command, else "switches to the buffer repository before running the command and switches back after. if !a:verbatim "switches to the buffer repository before running the command and switches back after. let dir = s:GetRepoDir() let workingDir = fnamemodify(dir,':h') if workingDir == '' return 0 endif let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' let bufferDir = getcwd() try execute cd.'`=workingDir`' let finalCmd = g:Gitv_GitExecutable.' --git-dir="' .dir. '" ' . a:command let result = system(finalCmd) finally execute cd.'`=bufferDir`' endtry else let result = system(a:command) let finalCmd = a:command endif return [result, finalCmd] endfu "}}} fu! s:GetRepoDir() "{{{ let dir = fugitive#buffer().repo().dir() if dir == '' echom "No git repository could be found." endif return dir endfu "}}} }}} "Open And Update Gitv:"{{{ fu! s:OpenGitv(extraArgs, fileMode) "{{{ let sanatizedArgs = a:extraArgs == "''" ? '' : a:extraArgs let sanatizedArgs = sanatizedArgs == '""' ? '' : sanatizedArgs let g:Gitv_InstanceCounter += 1 if !s:IsCompatible() "this outputs specific errors return endif try if a:fileMode call s:OpenFileMode(sanatizedArgs) else call s:OpenBrowserMode(sanatizedArgs) endif catch /not a git repository/ echom 'Not a git repository.' return endtry endf "}}} fu! s:IsCompatible() "{{{ if !exists('g:loaded_fugitive') echoerr "gitv requires the fugitive plugin to be installed." endif return exists('g:loaded_fugitive') endfu "}}} fu! s:OpenBrowserMode(extraArgs) "{{{ "this throws an exception if not a git repo which is caught immediately let fubuffer = fugitive#buffer() silent Gtabedit HEAD if s:IsHorizontal() let direction = 'new gitv'.'-'.g:Gitv_InstanceCounter else let direction = 'vnew gitv'.'-'.g:Gitv_InstanceCounter endif if !s:LoadGitv(direction, 0, g:Gitv_CommitStep, a:extraArgs, '') return 0 endif call s:SetupBufferCommands(0) "open the first commit silent call s:OpenGitvCommit("Gedit", 0) endf "}}} fu! s:OpenFileMode(extraArgs) "{{{ let relPath = fugitive#buffer().path() pclose! if !s:LoadGitv(&previewheight . "new gitv".'-'.g:Gitv_InstanceCounter, 0, g:Gitv_CommitStep, a:extraArgs, relPath) return 0 endif set previewwindow set winfixheight let b:Gitv_FileMode = 1 let b:Gitv_FileModeRelPath = relPath call s:SetupBufferCommands(1) endf "}}} fu! s:LoadGitv(direction, reload, commitCount, extraArgs, filePath) "{{{ if a:reload let jumpTo = line('.') "this is for repositioning the cursor after reload endif if !s:ConstructAndExecuteCmd(a:direction, a:reload, a:commitCount, a:extraArgs, a:filePath) return 0 endif call s:SetupBuffer(a:commitCount, a:extraArgs, a:filePath) exec exists('jumpTo') ? jumpTo : '1' call s:SetupMappings() "redefines some of the mappings made by Gitv_OpenGitCommand call s:ResizeWindow(a:filePath!='') echom "Loaded up to " . a:commitCount . " commits." return 1 endf "}}} fu! s:ConstructAndExecuteCmd(direction, reload, commitCount, extraArgs, filePath) "{{{ if a:reload "run the same command again with any extra args if exists('b:Git_Command') "substitute in the potentially new commit count taking account of a potential filePath let newcmd = b:Git_Command if a:filePath != '' let newcmd = substitute(newcmd, " -- " . a:filePath . "$", "", "") endif let newcmd = substitute(newcmd, " -\\d\\+$", " -" . a:commitCount, "") if a:filePath != '' let newcmd .= ' -- ' . a:filePath endif silent let res = Gitv_OpenGitCommand(newcmd, a:direction, 1) return res endif else let cmd = "log " . a:extraArgs let cmd .= " --no-color --decorate --pretty=format:\"%d %s__SEP__%ar__SEP__%an__SEP__[%h]\" --graph -" let cmd .= a:commitCount if a:filePath != '' let cmd .= ' -- ' . a:filePath endif silent let res = Gitv_OpenGitCommand(cmd, a:direction) return res endif return 0 endf "}}} fu! s:SetupBuffer(commitCount, extraArgs, filePath) "{{{ silent set filetype=gitv let b:Gitv_CommitCount = a:commitCount let b:Gitv_ExtraArgs = a:extraArgs silent setlocal modifiable silent setlocal noreadonly silent %s/refs\/tags\//t:/ge silent %s/refs\/remotes\//r:/ge silent %s/refs\/heads\///ge silent %call s:Align("__SEP__", a:filePath) silent %s/\s\+$//e call s:AddLoadMore() call s:AddLocalNodes(a:filePath) if a:filePath != '' call append(0, '-- ['.a:filePath.'] --') endif silent setlocal nomodifiable silent setlocal readonly silent setlocal cursorline endf "}}} fu! s:AddLocalNodes(filePath) "{{{ let suffix = a:filePath == '' ? '' : ' -- '.a:filePath let gitCmd = "diff --no-color --cached" . suffix let [result, cmd] = s:RunGitCommand(gitCmd, 0) if result != "" call append(0, s:localCommitedMsg) endif let gitCmd = "diff --no-color" . suffix let [result, cmd] = s:RunGitCommand(gitCmd, 0) if result != "" call append(0, s:localUncommitedMsg) endif endfu "}}} fu! s:AddLoadMore() "{{{ call append(line('$'), '-- Load More --') endfu "}}} fu! s:SetupMappings() "{{{ "operations nmap :call OpenGitvCommit("Gedit", 0) nmap o :call OpenGitvCommit("Gsplit", 0) nmap O :call OpenGitvCommit("Gtabedit", 0) nmap s :call OpenGitvCommit("Gvsplit", 0) "force opening the fugitive buffer for the commit nmap :call OpenGitvCommit("Gedit", 1) nmap q :call CloseGitv() nmap u :call LoadGitv('', 1, b:Gitv_CommitCount, b:Gitv_ExtraArgs, GetRelativeFilePath()) nmap co :call CheckOutGitvCommit() nmap D :call DiffGitvCommit() vmap D :call DiffGitvCommit() nmap S :call StatGitvCommit() vmap S :call StatGitvCommit() "movement nmap x :call JumpToBranch(0) nmap X :call JumpToBranch(1) nmap r :call JumpToRef(0) nmap R :call JumpToRef(1) nmap P :call JumpToHead() endf "}}} fu! s:SetupBufferCommands(fileMode) "{{{ silent command! -buffer -nargs=* -complete=customlist,s:fugitive_GitComplete Git call MoveIntoPreviewAndExecute("Git ",1)|normal u endfu "}}} fu! s:ResizeWindow(fileMode) "{{{ if a:fileMode "window height determined by &previewheight return endif if !s:IsHorizontal() "size window based on longest line let longest = max(map(range(1, line('$')), "virtcol([v:val, '$'])")) if longest > &columns/2 "potentially auto change to horizontal if s:AutoHorizontal() "switching to horizontal let b:Gitv_AutoHorizontal=1 wincmd K call s:ResizeWindow(a:fileMode) return else let longest = &columns/2 endif endif exec "vertical resize " . longest else "size window based on num lines call s:ResizeHorizontal() endif endf "}}} }}} "Utilities:"{{{ fu! s:GetGitvSha(lineNumber) "{{{ let l = getline(a:lineNumber) let sha = matchstr(l, "\\[\\zs[0-9a-f]\\{7}\\ze\\]$") return sha endf "}}} fu! s:GetGitvRefs() "{{{ let l = getline('.') let refstr = matchstr(l, "^\\(\\(|\\|\\/\\|\\\\\\|\\*\\)\\s\\?\\)*\\s\\+(\\zs.\\{-}\\ze)") let refs = split(refstr, ', ') return refs endf "}}} fu! s:RecordBufferExecAndWipe(cmd, wipe) "{{{ "this should be used to replace the buffer in a window let buf = bufnr('%') exec a:cmd if a:wipe "safe guard against wiping out buffer you're in if bufnr('%') != buf && bufexists(buf) exec 'bwipeout ' . buf endif endif endfu "}}} fu! s:MoveIntoPreviewAndExecute(cmd, tryToOpenNewWin) "{{{ if winnr("$") == 1 "is the only window call s:AttemptToCreateAPreviewWindow(a:tryToOpenNewWin, a:cmd) return endif let horiz = s:IsHorizontal() let filem = s:IsFileMode() let currentWin = winnr() if horiz || filem wincmd j else wincmd l endif if currentWin == winnr() "haven't moved anywhere call s:AttemptToCreateAPreviewWindow(a:tryToOpenNewWin, a:cmd) return endif silent exec a:cmd if horiz || filem wincmd k else wincmd h endif endfu "}}} fu! s:AttemptToCreateAPreviewWindow(shouldAttempt, cmd) "{{{ if a:shouldAttempt call s:CreateNewPreviewWindow() call s:MoveIntoPreviewAndExecute(a:cmd, 0) else echoerr "No preview window detected." endif endfu "}}} fu! s:CreateNewPreviewWindow() "{{{ "this should not be called by anything other than AttemptToCreateAPreviewWindow let horiz = s:IsHorizontal() let filem = s:IsFileMode() if horiz || filem Gsplit HEAD else Gvsplit HEAD endif wincmd x endfu "}}} fu! s:IsHorizontal() "{{{ "NOTE: this can only tell you if horizontal while cursor in browser window let horizGlobal = exists('g:Gitv_OpenHorizontal') && g:Gitv_OpenHorizontal == 1 let horizBuffer = exists('b:Gitv_AutoHorizontal') && b:Gitv_AutoHorizontal == 1 return horizGlobal || horizBuffer endf "}}} fu! s:AutoHorizontal() "{{{ return exists('g:Gitv_OpenHorizontal') && \ type(g:Gitv_OpenHorizontal) == type("") && \ g:Gitv_OpenHorizontal ==? 'auto' endf "}}} fu! s:IsFileMode() "{{{ return exists('b:Gitv_FileMode') && b:Gitv_FileMode == 1 endf "}}} fu! s:ResizeHorizontal() "{{{ let lines = line('$') if lines > (&lines/2)-2 let lines = (&lines/2)-2 endif exec "resize " . lines endf "}}} fu! s:GetRelativeFilePath() "{{{ return exists('b:Gitv_FileModeRelPath') ? b:Gitv_FileModeRelPath : '' endf "}}} fu! s:OpenRelativeFilePath(sha, geditForm) "{{{ let relPath = s:GetRelativeFilePath() if relPath == '' return endif let cmd = a:geditForm . " " . a:sha . ":" . relPath let cmd = 'call s:RecordBufferExecAndWipe("'.cmd.'", '.(a:geditForm=='Gedit').')' call s:MoveIntoPreviewAndExecute(cmd, 1) endf "}}} }}} "Mapped Functions:"{{{ "Operations: "{{{ fu! s:OpenGitvCommit(geditForm, forceOpenFugitive) "{{{ if getline('.') == "-- Load More --" call s:LoadGitv('', 1, b:Gitv_CommitCount+g:Gitv_CommitStep, b:Gitv_ExtraArgs, s:GetRelativeFilePath()) return endif if s:IsFileMode() && getline('.') =~ "^-- \\[.*\\] --$" call s:OpenWorkingCopy(a:geditForm) return endif if getline('.') == s:localUncommitedMsg call s:OpenWorkingDiff(a:geditForm, 0) return endif if getline('.') == s:localCommitedMsg call s:OpenWorkingDiff(a:geditForm, 1) return endif let sha = s:GetGitvSha(line('.')) if sha == "" return endif if s:IsFileMode() && !a:forceOpenFugitive call s:OpenRelativeFilePath(sha, a:geditForm) else let cmd = a:geditForm . " " . sha let cmd = 'call s:RecordBufferExecAndWipe("'.cmd.'", '.(a:geditForm=='Gedit').')' call s:MoveIntoPreviewAndExecute(cmd, 1) endif endf fu! s:OpenWorkingCopy(geditForm) let fp = s:GetRelativeFilePath() let form = a:geditForm[1:] "strip off the leading 'G' let cmd = form . " " . fugitive#buffer().repo().tree() . "/" . fp let cmd = 'call s:RecordBufferExecAndWipe("'.cmd.'", '.(form=='edit').')' call s:MoveIntoPreviewAndExecute(cmd, 1) endfu fu! s:OpenWorkingDiff(geditForm, staged) let winCmd = a:geditForm[1:] == 'edit' ? '' : a:geditForm[1:] if s:IsFileMode() let fp = s:GetRelativeFilePath() let suffix = ' -- '.fp let g:Gitv_InstanceCounter += 1 let winCmd = 'new gitv'.'-'.g:Gitv_InstanceCounter else let suffix = '' endif if a:staged let cmd = 'call Gitv_OpenGitCommand(\"diff --no-color --cached'.suffix.'\", \"'.winCmd.'\")' else let cmd = 'call Gitv_OpenGitCommand(\"diff --no-color'.suffix.'\", \"'.winCmd.'\")' endif let cmd = 'call s:RecordBufferExecAndWipe("'.cmd.'", '.(winCmd=='').')' call s:MoveIntoPreviewAndExecute(cmd, 1) endfu "}}} fu! s:CheckOutGitvCommit() "{{{ let allrefs = s:GetGitvRefs() let sha = s:GetGitvSha(line('.')) if sha == "" return endif let refs = allrefs + [sha] let refstr = join(refs, "\n") let choice = confirm("Checkout commit:", refstr . "\nCancel") if choice == 0 return endif let choice = get(refs, choice-1, "") if choice == "" return endif let choice = substitute(choice, "^t:", "", "") let choice = substitute(choice, "^r:", "", "") if s:IsFileMode() let relPath = s:GetRelativeFilePath() let choice .= " -- " . relPath endif exec "Git checkout " . choice endf "}}} fu! s:CloseGitv() "{{{ if s:IsFileMode() q else if g:Gitv_WipeAllOnClose silent windo setlocal bufhidden=wipe endif let moveLeft = tabpagenr() == tabpagenr('$') ? 0 : 1 tabc if moveLeft && tabpagenr() != 1 tabp endif endif endf "}}} fu! s:DiffGitvCommit() range "{{{ if !s:IsFileMode() echom "Diffing is not possible in browser mode." return endif let shafirst = s:GetGitvSha(a:firstline) let shalast = s:GetGitvSha(a:lastline) if shafirst == "" || shalast == "" return endif if a:firstline != a:lastline call s:OpenRelativeFilePath(shafirst, "Gedit") endif call s:MoveIntoPreviewAndExecute("Gdiff " . shalast, a:firstline != a:lastline) endf "}}} fu! s:StatGitvCommit() range "{{{ let shafirst = s:GetGitvSha(a:firstline) let shalast = s:GetGitvSha(a:lastline) if shafirst == "" || shalast == "" return endif let cmd = 'diff '.shafirst if shafirst != shalast let cmd .= ' '.shalast endif let cmd .= ' --stat' let cmd = "call s:SetupStatBuffer('".cmd."')" if s:IsFileMode() exec cmd else call s:MoveIntoPreviewAndExecute(cmd, 1) endif endf fu! s:SetupStatBuffer(cmd) silent let res = Gitv_OpenGitCommand(a:cmd, s:IsFileMode()?'vnew':'') if res silent set filetype=gitv endif endfu "}}} }}} "Movement: "{{{ fu! s:JumpToBranch(backward) "{{{ if a:backward silent! ?|/\||\\?-1 else silent! /|\\\||\//+1 endif endf "}}} fu! s:JumpToRef(backward) "{{{ if a:backward silent! ?^\(\(|\|\/\|\\\|\*\)\s\=\)\+\s\+\zs( else silent! /^\(\(|\|\/\|\\\|\*\)\s\?\)\+\s\+\zs(/ endif endf "}}} fu! s:JumpToHead() "{{{ silent! /^\(\(|\|\/\|\\\|\*\)\s\?\)\+\s\+\zs(HEAD/ endf "}}} "}}} }}} "Align And Truncate Functions: "{{{ fu! s:Align(seperator, filePath) range "{{{ let lines = getline(a:firstline, a:lastline) call map(lines, 'split(v:val, a:seperator)') let newlines = copy(lines) call filter(newlines, 'len(v:val)>1') let maxLens = s:MaxLengths(newlines) let newlines = [] for tokens in lines if len(tokens)>1 let newline = [] for i in range(len(tokens)) let token = tokens[i] call add(newline, token . repeat(' ', maxLens[i]-strlen(token)+1)) endfor call add(newlines, newline) else call add(newlines, tokens) endif endfor if g:Gitv_TruncateCommitSubjects call s:TruncateLines(newlines, a:filePath) endif call map(newlines, "join(v:val)") call setline(a:firstline, newlines) endfu "}}} fu! s:TruncateLines(lines, filePath) "{{{ "truncates the commit subject for any line > &columns call map(a:lines, "s:TruncateHelp(v:val, a:filePath)") endfu "}}} fu! s:TruncateHelp(line, filePath) "{{{ let length = strlen(join(a:line)) let maxWidth = s:IsHorizontal() ? &columns : &columns/2 let maxWidth = a:filePath != '' ? winwidth(0) : maxWidth if length > maxWidth let delta = length - maxWidth "offset = 3 for the elipsis and 1 for truncation let offset = 3 + 1 if a:line[0][-(delta + offset + 1):] =~ "^\\s\\+$" let extension = " " else let extension = "..." endif let a:line[0] = a:line[0][:-(delta + offset)] . extension endif return a:line endfu "}}} fu! s:MaxLengths(colls) "{{{ "precondition: coll is a list of lists of strings -- should be rectangular "returns a list of maximum string lengths let lengths = [] for x in a:colls for y in range(len(x)) let length = strlen(x[y]) if length > get(lengths, y, 0) if len(lengths)-1 < y call add(lengths, length) else let lengths[y] = length endif endif endfor endfor return lengths endfu "}}} }}} "Fugitive Functions: "{{{ "These functions are lifted directly from fugitive and modified only to work with gitv. function! s:fugitive_sub(str,pat,rep) abort "{{{ return substitute(a:str,'\v\C'.a:pat,a:rep,'') endfunction "}}} function! s:fugitive_GitComplete(A,L,P) abort "{{{ if !exists('s:exec_path') let s:exec_path = s:fugitive_sub(system(g:fugitive_git_executable.' --exec-path'),'\n$','') endif let cmds = map(split(glob(s:exec_path.'/git-*'),"\n"),'s:fugitive_sub(v:val[strlen(s:exec_path)+5 : -1],"\\.exe$","")') if a:L =~ ' [[:alnum:]-]\+ ' return fugitive#buffer().repo().superglob(a:A) elseif a:A == '' return cmds else return filter(cmds,'v:val[0 : strlen(a:A)-1] ==# a:A') endif endfunction "}}} }}} let &cpo = s:savecpo unlet s:savecpo " vim:fdm=marker