1 "AUTHOR: Greg Sexton <gregsexton@gmail.com>
2 "WEBSITE: http://www.gregsexton.org/portfolio/gitv/
3 "LICENSE: Same terms as Vim itself (see :help license).
4 "NOTES: Much of the credit for gitv goes to Tim Pope and the fugitive plugin
5 " where this plugin either uses functionality directly or was inspired heavily.
7 if exists("g:loaded_gitv") || v:version < 700
15 "configurable options:
16 "g:Gitv_CommitStep - int
17 "g:Gitv_OpenHorizontal - {0,1,'AUTO'}
18 "g:Gitv_GitExecutable - string
19 "g:Gitv_WipeAllOnClose - int
20 "g:Gitv_WrapLines - {0,1}
21 "g:Gitv_TruncateCommitSubjects - {0,1}
23 if !exists("g:Gitv_CommitStep")
24 let g:Gitv_CommitStep = &lines
27 if !exists('g:Gitv_GitExecutable')
28 let g:Gitv_GitExecutable = 'git'
31 if !exists('g:Gitv_WipeAllOnClose')
32 let g:Gitv_WipeAllOnClose = 0 "default for safety
35 if !exists('g:Gitv_WrapLines')
36 let g:Gitv_WrapLines = 0
39 if !exists('g:Gitv_TruncateCommitSubjects')
40 let g:Gitv_TruncateCommitSubjects = 0
43 "this counts up each time gitv is opened to ensure a unique file name
44 let g:Gitv_InstanceCounter = 0
46 let s:localUncommitedMsg = '* Local uncommitted changes, not checked in to index.'
47 let s:localCommitedMsg = '* Local changes checked in to index but not committed.'
49 command! -nargs=* -bang Gitv call s:OpenGitv(shellescape(<q-args>), <bang>0)
53 fu! Gitv_OpenGitCommand(command, windowCmd, ...) "{{{
54 "returns 1 if command succeeded with output
55 "optional arg is a flag, if present runs command verbatim
57 "this function is not limited to script scope as is useful for running other commands.
58 "e.g call Gitv_OpenGitCommand("diff --no-color", 'vnew') is useful for getting an overall git diff.
60 let [result, finalCmd] = s:RunGitCommand(a:command, a:0)
62 if type(result) == type(0)
65 if type(result) == type("") && result == ""
70 silent setlocal modifiable
71 silent setlocal noreadonly
74 let goBackTo = winnr()
75 let dir = s:GetRepoDir()
76 let workingDir = fnamemodify(dir,':h')
77 let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd '
78 let bufferDir = getcwd()
79 let tempSplitBelow = &splitbelow
80 let tempSplitRight = &splitright
84 execute cd.'`=workingDir`'
86 let newWindow = winnr()
88 exec goBackTo . 'wincmd w'
89 execute cd.'`=bufferDir`'
90 if exists('newWindow')
91 exec newWindow . 'wincmd w'
93 exec 'set '. (tempSplitBelow ? '' : 'no') . 'splitbelow'
94 exec 'set '. (tempSplitRight ? '' : 'no') . 'splitright'
100 let b:Git_Command = finalCmd
101 silent setlocal ft=git
102 silent setlocal buftype=nofile
103 silent setlocal nobuflisted
104 silent setlocal noswapfile
105 silent setlocal bufhidden=wipe
106 silent setlocal nonumber
110 silent setlocal nowrap
112 silent setlocal fdm=syntax
113 silent setlocal foldlevel=0
114 nmap <buffer> <silent> q :q!<CR>
115 nmap <buffer> <silent> u :if exists('b:Git_Command')<bar>call Gitv_OpenGitCommand(b:Git_Command, '', 1)<bar>endif<cr>
116 call append(0, split(result, '\n')) "system converts eols to \n regardless of os.
117 silent setlocal nomodifiable
118 silent setlocal readonly
123 "General Git Functions: "{{{
124 fu! s:RunGitCommand(command, verbatim) "{{{
125 "if verbatim returns result of system command, else
126 "switches to the buffer repository before running the command and switches back after.
128 "switches to the buffer repository before running the command and switches back after.
129 let dir = s:GetRepoDir()
130 let workingDir = fnamemodify(dir,':h')
135 let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd '
136 let bufferDir = getcwd()
138 execute cd.'`=workingDir`'
139 let finalCmd = g:Gitv_GitExecutable.' --git-dir="' .dir. '" ' . a:command
140 let result = system(finalCmd)
142 execute cd.'`=bufferDir`'
145 let result = system(a:command)
146 let finalCmd = a:command
148 return [result, finalCmd]
150 fu! s:GetRepoDir() "{{{
151 let dir = fugitive#buffer().repo().dir()
153 echom "No git repository could be found."
157 "Open And Update Gitv:"{{{
158 fu! s:OpenGitv(extraArgs, fileMode) "{{{
159 let sanatizedArgs = a:extraArgs == "''" ? '' : a:extraArgs
160 let sanatizedArgs = sanatizedArgs == '""' ? '' : sanatizedArgs
161 let g:Gitv_InstanceCounter += 1
162 if !s:IsCompatible() "this outputs specific errors
167 call s:OpenFileMode(sanatizedArgs)
169 call s:OpenBrowserMode(sanatizedArgs)
171 catch /not a git repository/
172 echom 'Not a git repository.'
176 fu! s:IsCompatible() "{{{
177 if !exists('g:loaded_fugitive')
178 echoerr "gitv requires the fugitive plugin to be installed."
180 return exists('g:loaded_fugitive')
182 fu! s:OpenBrowserMode(extraArgs) "{{{
183 "this throws an exception if not a git repo which is caught immediately
184 let fubuffer = fugitive#buffer()
188 let direction = 'new gitv'.'-'.g:Gitv_InstanceCounter
190 let direction = 'vnew gitv'.'-'.g:Gitv_InstanceCounter
192 if !s:LoadGitv(direction, 0, g:Gitv_CommitStep, a:extraArgs, '')
195 call s:SetupBufferCommands(0)
196 "open the first commit
197 silent call s:OpenGitvCommit("Gedit", 0)
199 fu! s:OpenFileMode(extraArgs) "{{{
200 let relPath = fugitive#buffer().path()
202 if !s:LoadGitv(&previewheight . "new gitv".'-'.g:Gitv_InstanceCounter, 0, g:Gitv_CommitStep, a:extraArgs, relPath)
207 let b:Gitv_FileMode = 1
208 let b:Gitv_FileModeRelPath = relPath
209 call s:SetupBufferCommands(1)
211 fu! s:LoadGitv(direction, reload, commitCount, extraArgs, filePath) "{{{
213 let jumpTo = line('.') "this is for repositioning the cursor after reload
216 if !s:ConstructAndExecuteCmd(a:direction, a:reload, a:commitCount, a:extraArgs, a:filePath)
219 call s:SetupBuffer(a:commitCount, a:extraArgs, a:filePath)
220 exec exists('jumpTo') ? jumpTo : '1'
221 call s:SetupMappings() "redefines some of the mappings made by Gitv_OpenGitCommand
222 call s:ResizeWindow(a:filePath!='')
224 echom "Loaded up to " . a:commitCount . " commits."
227 fu! s:ConstructAndExecuteCmd(direction, reload, commitCount, extraArgs, filePath) "{{{
228 if a:reload "run the same command again with any extra args
229 if exists('b:Git_Command')
230 "substitute in the potentially new commit count taking account of a potential filePath
231 let newcmd = b:Git_Command
233 let newcmd = substitute(newcmd, " -- " . a:filePath . "$", "", "")
235 let newcmd = substitute(newcmd, " -\\d\\+$", " -" . a:commitCount, "")
237 let newcmd .= ' -- ' . a:filePath
239 silent let res = Gitv_OpenGitCommand(newcmd, a:direction, 1)
243 let cmd = "log " . a:extraArgs
244 let cmd .= " --no-color --decorate --pretty=format:\"%d %s__SEP__%ar__SEP__%an__SEP__[%h]\" --graph -"
245 let cmd .= a:commitCount
247 let cmd .= ' -- ' . a:filePath
249 silent let res = Gitv_OpenGitCommand(cmd, a:direction)
254 fu! s:SetupBuffer(commitCount, extraArgs, filePath) "{{{
255 silent set filetype=gitv
256 let b:Gitv_CommitCount = a:commitCount
257 let b:Gitv_ExtraArgs = a:extraArgs
258 silent setlocal modifiable
259 silent setlocal noreadonly
260 silent %s/refs\/tags\//t:/ge
261 silent %s/refs\/remotes\//r:/ge
262 silent %s/refs\/heads\///ge
263 silent %call s:Align("__SEP__", a:filePath)
266 call s:AddLocalNodes(a:filePath)
268 call append(0, '-- ['.a:filePath.'] --')
270 silent setlocal nomodifiable
271 silent setlocal readonly
272 silent setlocal cursorline
274 fu! s:AddLocalNodes(filePath) "{{{
275 let suffix = a:filePath == '' ? '' : ' -- '.a:filePath
276 let gitCmd = "diff --no-color --cached" . suffix
277 let [result, cmd] = s:RunGitCommand(gitCmd, 0)
279 call append(0, s:localCommitedMsg)
281 let gitCmd = "diff --no-color" . suffix
282 let [result, cmd] = s:RunGitCommand(gitCmd, 0)
284 call append(0, s:localUncommitedMsg)
287 fu! s:AddLoadMore() "{{{
288 call append(line('$'), '-- Load More --')
290 fu! s:SetupMappings() "{{{
292 nmap <buffer> <silent> <cr> :call <SID>OpenGitvCommit("Gedit", 0)<cr>
293 nmap <buffer> <silent> o :call <SID>OpenGitvCommit("Gsplit", 0)<cr>
294 nmap <buffer> <silent> O :call <SID>OpenGitvCommit("Gtabedit", 0)<cr>
295 nmap <buffer> <silent> s :call <SID>OpenGitvCommit("Gvsplit", 0)<cr>
296 "force opening the fugitive buffer for the commit
297 nmap <buffer> <silent> <c-cr> :call <SID>OpenGitvCommit("Gedit", 1)<cr>
299 nmap <buffer> <silent> q :call <SID>CloseGitv()<cr>
300 nmap <buffer> <silent> u :call <SID>LoadGitv('', 1, b:Gitv_CommitCount, b:Gitv_ExtraArgs, <SID>GetRelativeFilePath())<cr>
301 nmap <buffer> <silent> co :call <SID>CheckOutGitvCommit()<cr>
303 nmap <buffer> <silent> D :call <SID>DiffGitvCommit()<cr>
304 vmap <buffer> <silent> D :call <SID>DiffGitvCommit()<cr>
306 nmap <buffer> <silent> S :call <SID>StatGitvCommit()<cr>
307 vmap <buffer> <silent> S :call <SID>StatGitvCommit()<cr>
310 nmap <buffer> <silent> x :call <SID>JumpToBranch(0)<cr>
311 nmap <buffer> <silent> X :call <SID>JumpToBranch(1)<cr>
312 nmap <buffer> <silent> r :call <SID>JumpToRef(0)<cr>
313 nmap <buffer> <silent> R :call <SID>JumpToRef(1)<cr>
314 nmap <buffer> <silent> P :call <SID>JumpToHead()<cr>
316 fu! s:SetupBufferCommands(fileMode) "{{{
317 silent command! -buffer -nargs=* -complete=customlist,s:fugitive_GitComplete Git call <sid>MoveIntoPreviewAndExecute("Git <args>",1)|normal u
319 fu! s:ResizeWindow(fileMode) "{{{
320 if a:fileMode "window height determined by &previewheight
324 "size window based on longest line
325 let longest = max(map(range(1, line('$')), "virtcol([v:val, '$'])"))
326 if longest > &columns/2
327 "potentially auto change to horizontal
328 if s:AutoHorizontal()
329 "switching to horizontal
330 let b:Gitv_AutoHorizontal=1
332 call s:ResizeWindow(a:fileMode)
335 let longest = &columns/2
338 exec "vertical resize " . longest
340 "size window based on num lines
341 call s:ResizeHorizontal()
345 fu! s:GetGitvSha(lineNumber) "{{{
346 let l = getline(a:lineNumber)
347 let sha = matchstr(l, "\\[\\zs[0-9a-f]\\{7}\\ze\\]$")
350 fu! s:GetGitvRefs() "{{{
352 let refstr = matchstr(l, "^\\(\\(|\\|\\/\\|\\\\\\|\\*\\)\\s\\?\\)*\\s\\+(\\zs.\\{-}\\ze)")
353 let refs = split(refstr, ', ')
356 fu! s:RecordBufferExecAndWipe(cmd, wipe) "{{{
357 "this should be used to replace the buffer in a window
361 "safe guard against wiping out buffer you're in
362 if bufnr('%') != buf && bufexists(buf)
363 exec 'bwipeout ' . buf
367 fu! s:MoveIntoPreviewAndExecute(cmd, tryToOpenNewWin) "{{{
368 if winnr("$") == 1 "is the only window
369 call s:AttemptToCreateAPreviewWindow(a:tryToOpenNewWin, a:cmd)
372 let horiz = s:IsHorizontal()
373 let filem = s:IsFileMode()
374 let currentWin = winnr()
382 if currentWin == winnr() "haven't moved anywhere
383 call s:AttemptToCreateAPreviewWindow(a:tryToOpenNewWin, a:cmd)
394 fu! s:AttemptToCreateAPreviewWindow(shouldAttempt, cmd) "{{{
396 call s:CreateNewPreviewWindow()
397 call s:MoveIntoPreviewAndExecute(a:cmd, 0)
399 echoerr "No preview window detected."
402 fu! s:CreateNewPreviewWindow() "{{{
403 "this should not be called by anything other than AttemptToCreateAPreviewWindow
404 let horiz = s:IsHorizontal()
405 let filem = s:IsFileMode()
413 fu! s:IsHorizontal() "{{{
414 "NOTE: this can only tell you if horizontal while cursor in browser window
415 let horizGlobal = exists('g:Gitv_OpenHorizontal') && g:Gitv_OpenHorizontal == 1
416 let horizBuffer = exists('b:Gitv_AutoHorizontal') && b:Gitv_AutoHorizontal == 1
417 return horizGlobal || horizBuffer
419 fu! s:AutoHorizontal() "{{{
420 return exists('g:Gitv_OpenHorizontal') &&
421 \ type(g:Gitv_OpenHorizontal) == type("") &&
422 \ g:Gitv_OpenHorizontal ==? 'auto'
424 fu! s:IsFileMode() "{{{
425 return exists('b:Gitv_FileMode') && b:Gitv_FileMode == 1
427 fu! s:ResizeHorizontal() "{{{
428 let lines = line('$')
429 if lines > (&lines/2)-2
430 let lines = (&lines/2)-2
432 exec "resize " . lines
434 fu! s:GetRelativeFilePath() "{{{
435 return exists('b:Gitv_FileModeRelPath') ? b:Gitv_FileModeRelPath : ''
437 fu! s:OpenRelativeFilePath(sha, geditForm) "{{{
438 let relPath = s:GetRelativeFilePath()
442 let cmd = a:geditForm . " " . a:sha . ":" . relPath
443 let cmd = 'call s:RecordBufferExecAndWipe("'.cmd.'", '.(a:geditForm=='Gedit').')'
444 call s:MoveIntoPreviewAndExecute(cmd, 1)
446 "Mapped Functions:"{{{
448 fu! s:OpenGitvCommit(geditForm, forceOpenFugitive) "{{{
449 if getline('.') == "-- Load More --"
450 call s:LoadGitv('', 1, b:Gitv_CommitCount+g:Gitv_CommitStep, b:Gitv_ExtraArgs, s:GetRelativeFilePath())
453 if s:IsFileMode() && getline('.') =~ "^-- \\[.*\\] --$"
454 call s:OpenWorkingCopy(a:geditForm)
457 if getline('.') == s:localUncommitedMsg
458 call s:OpenWorkingDiff(a:geditForm, 0)
461 if getline('.') == s:localCommitedMsg
462 call s:OpenWorkingDiff(a:geditForm, 1)
465 let sha = s:GetGitvSha(line('.'))
469 if s:IsFileMode() && !a:forceOpenFugitive
470 call s:OpenRelativeFilePath(sha, a:geditForm)
472 let cmd = a:geditForm . " " . sha
473 let cmd = 'call s:RecordBufferExecAndWipe("'.cmd.'", '.(a:geditForm=='Gedit').')'
474 call s:MoveIntoPreviewAndExecute(cmd, 1)
477 fu! s:OpenWorkingCopy(geditForm)
478 let fp = s:GetRelativeFilePath()
479 let form = a:geditForm[1:] "strip off the leading 'G'
480 let cmd = form . " " . fugitive#buffer().repo().tree() . "/" . fp
481 let cmd = 'call s:RecordBufferExecAndWipe("'.cmd.'", '.(form=='edit').')'
482 call s:MoveIntoPreviewAndExecute(cmd, 1)
484 fu! s:OpenWorkingDiff(geditForm, staged)
485 let winCmd = a:geditForm[1:] == 'edit' ? '' : a:geditForm[1:]
487 let fp = s:GetRelativeFilePath()
488 let suffix = ' -- '.fp
489 let g:Gitv_InstanceCounter += 1
490 let winCmd = 'new gitv'.'-'.g:Gitv_InstanceCounter
495 let cmd = 'call Gitv_OpenGitCommand(\"diff --no-color --cached'.suffix.'\", \"'.winCmd.'\")'
497 let cmd = 'call Gitv_OpenGitCommand(\"diff --no-color'.suffix.'\", \"'.winCmd.'\")'
499 let cmd = 'call s:RecordBufferExecAndWipe("'.cmd.'", '.(winCmd=='').')'
500 call s:MoveIntoPreviewAndExecute(cmd, 1)
502 fu! s:CheckOutGitvCommit() "{{{
503 let allrefs = s:GetGitvRefs()
504 let sha = s:GetGitvSha(line('.'))
508 let refs = allrefs + [sha]
509 let refstr = join(refs, "\n")
510 let choice = confirm("Checkout commit:", refstr . "\nCancel")
514 let choice = get(refs, choice-1, "")
518 let choice = substitute(choice, "^t:", "", "")
519 let choice = substitute(choice, "^r:", "", "")
521 let relPath = s:GetRelativeFilePath()
522 let choice .= " -- " . relPath
524 exec "Git checkout " . choice
526 fu! s:CloseGitv() "{{{
530 if g:Gitv_WipeAllOnClose
531 silent windo setlocal bufhidden=wipe
533 let moveLeft = tabpagenr() == tabpagenr('$') ? 0 : 1
535 if moveLeft && tabpagenr() != 1
540 fu! s:DiffGitvCommit() range "{{{
542 echom "Diffing is not possible in browser mode."
545 let shafirst = s:GetGitvSha(a:firstline)
546 let shalast = s:GetGitvSha(a:lastline)
547 if shafirst == "" || shalast == ""
550 if a:firstline != a:lastline
551 call s:OpenRelativeFilePath(shafirst, "Gedit")
553 call s:MoveIntoPreviewAndExecute("Gdiff " . shalast, a:firstline != a:lastline)
555 fu! s:StatGitvCommit() range "{{{
556 let shafirst = s:GetGitvSha(a:firstline)
557 let shalast = s:GetGitvSha(a:lastline)
558 if shafirst == "" || shalast == ""
561 let cmd = 'diff '.shafirst
562 if shafirst != shalast
563 let cmd .= ' '.shalast
566 let cmd = "call s:SetupStatBuffer('".cmd."')"
570 call s:MoveIntoPreviewAndExecute(cmd, 1)
573 fu! s:SetupStatBuffer(cmd)
574 silent let res = Gitv_OpenGitCommand(a:cmd, s:IsFileMode()?'vnew':'')
576 silent set filetype=gitv
580 fu! s:JumpToBranch(backward) "{{{
587 fu! s:JumpToRef(backward) "{{{
589 silent! ?^\(\(|\|\/\|\\\|\*\)\s\=\)\+\s\+\zs(
591 silent! /^\(\(|\|\/\|\\\|\*\)\s\?\)\+\s\+\zs(/
594 fu! s:JumpToHead() "{{{
595 silent! /^\(\(|\|\/\|\\\|\*\)\s\?\)\+\s\+\zs(HEAD/
598 "Align And Truncate Functions: "{{{
599 fu! s:Align(seperator, filePath) range "{{{
600 let lines = getline(a:firstline, a:lastline)
601 call map(lines, 'split(v:val, a:seperator)')
603 let newlines = copy(lines)
604 call filter(newlines, 'len(v:val)>1')
605 let maxLens = s:MaxLengths(newlines)
611 for i in range(len(tokens))
612 let token = tokens[i]
613 call add(newline, token . repeat(' ', maxLens[i]-strlen(token)+1))
615 call add(newlines, newline)
617 call add(newlines, tokens)
621 if g:Gitv_TruncateCommitSubjects
622 call s:TruncateLines(newlines, a:filePath)
625 call map(newlines, "join(v:val)")
626 call setline(a:firstline, newlines)
628 fu! s:TruncateLines(lines, filePath) "{{{
629 "truncates the commit subject for any line > &columns
630 call map(a:lines, "s:TruncateHelp(v:val, a:filePath)")
632 fu! s:TruncateHelp(line, filePath) "{{{
633 let length = strlen(join(a:line))
634 let maxWidth = s:IsHorizontal() ? &columns : &columns/2
635 let maxWidth = a:filePath != '' ? winwidth(0) : maxWidth
637 let delta = length - maxWidth
638 "offset = 3 for the elipsis and 1 for truncation
640 if a:line[0][-(delta + offset + 1):] =~ "^\\s\\+$"
643 let extension = "..."
645 let a:line[0] = a:line[0][:-(delta + offset)] . extension
649 fu! s:MaxLengths(colls) "{{{
650 "precondition: coll is a list of lists of strings -- should be rectangular
651 "returns a list of maximum string lengths
654 for y in range(len(x))
655 let length = strlen(x[y])
656 if length > get(lengths, y, 0)
657 if len(lengths)-1 < y
658 call add(lengths, length)
660 let lengths[y] = length
667 "Fugitive Functions: "{{{
668 "These functions are lifted directly from fugitive and modified only to work with gitv.
669 function! s:fugitive_sub(str,pat,rep) abort "{{{
670 return substitute(a:str,'\v\C'.a:pat,a:rep,'')
672 function! s:fugitive_GitComplete(A,L,P) abort "{{{
673 if !exists('s:exec_path')
674 let s:exec_path = s:fugitive_sub(system(g:fugitive_git_executable.' --exec-path'),'\n$','')
676 let cmds = map(split(glob(s:exec_path.'/git-*'),"\n"),'s:fugitive_sub(v:val[strlen(s:exec_path)+5 : -1],"\\.exe$","")')
677 if a:L =~ ' [[:alnum:]-]\+ '
678 return fugitive#buffer().repo().superglob(a:A)
682 return filter(cmds,'v:val[0 : strlen(a:A)-1] ==# a:A')