" File: vp4.vim " Description: vim global plugin for perforce integration " Last Change: Nov 22, 2016 " Author: Emily Ng " {{{ Explorer global data structures " directory object data " dir_data = { " '' : { " 'name' : "/", " 'folded' : <0 folded, 1 unfolded>, " 'files' : [ " {'name': , 'flags': }, ... " ], " 'children' : [] " }, " ... " } " " root = //main " parent/ //main " child/ //main/parent " file0.txt //main/parent/child " file1.txt //main/parent/child " file2.txt //main/parent let s:directory_data = {} " line number to directory prefix map let s:line_map = {} " depot directory to local directory map let s:directory_map = {} " }}} " {{{ Helper functions " {{{ Generic Helper functions function! s:BufferIsEmpty() return line('$') == 1 && getline(1) == '' endfunction " Pad string by appending spaces until length of string 's' is equal to 'amt' function! s:Pad(s,amt) return a:s . repeat(' ',a:amt - len(a:s)) endfunction " Pad string by prepending spaces until length of string 's' is equal to 'amt' function! s:PrePad(s,amt) return repeat(' ', a:amt - len(a:s)) . a:s endfunction " Echo an error message without the annoying 'Detected error in ...' header function! s:EchoError(msg) echohl ErrorMsg echom a:msg echohl None endfunction " Echo a warning message function! s:EchoWarning(msg) echohl WarningMsg echom a:msg echohl None endfunction function! s:GoToWindowForBufferName(name) if bufwinnr(bufnr(a:name)) != -1 exe bufwinnr(bufnr(a:name)) . "wincmd w" return 1 else return 0 endif endfunction " }}} " {{{ Perforce system functions " Return result of calling p4 command function! s:PerforceSystem(cmd) if has('win64') || has('win32') let command = g:vp4_perforce_executable . " " . a:cmd . " 2> NUL" else let command = g:vp4_perforce_executable . " " . a:cmd . " 2> /dev/null" endif if g:perforce_debug echom "DBG sys: " . command endif let retval = system(command) return retval endfunction " Append results of p4 command to current buffer function! s:PerforceRead(cmd) let _modifiable = &modifiable set modifiable let command = '$read !' . g:vp4_perforce_executable . " " . a:cmd if g:perforce_debug echom "DBG read: " . command endif " Populate the window and get rid of the extra line at the top execute command 1 execute 'normal! dd' let &modifiable=_modifiable endfunction " Use current buffer as stdin to p4 command function! s:PerforceWrite(cmd) let command = 'write !' . g:vp4_perforce_executable . " " . a:cmd if g:perforce_debug echom "DBG write: " . command endif execute command endfunction " Function to get the path of the file function! s:ExpandPath(file) if exists("g:vp4_base_path_replacements") if g:perforce_debug echom "We have a base path replacements" endif let l:oldPath = expand('%:p') let l:replacements = items(g:vp4_base_path_replacements) for item in l:replacements if g:perforce_debug echom "does " . l:oldPath . " match " . item[0] endif if l:oldPath =~ item[0] " We have a match if g:perforce_debug echom "Matched string " . item[0] . " in " . l:oldPath endif let l:newFile = substitute(l:oldPath, item[0], item[1], "") if g:perforce_debug echom "New path " . l:newFile endif return l:newFile endif endfor if g:perforce_debug echom "Did not find replacement, return" endif return expand(a:file) else if g:perforce_debug echom "Using default pathing" endif return expand(a:file) endif endfunction " }}} " {{{ Perforce checker infrastructure " Returns the value of a fstat field " Throws an error if it failed. It is up to the *caller* to catch the error " and issue an appropriate message. function! s:PerforceFstat(field, filename) " NB: for some reason fstat was designed not to return an error code if " 1. no such file " 2. no such revision " 3. not shelved in changelist " It always starts a valid line with '...'; use it to validate response. " It does return -1 if an invalid field was requested. let s = s:PerforceSystem('fstat -T ' . a:field . ' ' . a:filename) if v:shell_error || matchstr(s, '\.\.\.') == '' if matchstr(s, 'P4PASSWD') != '' call s:EchoError(split(s, '\n')[0]) return 0 else throw 'PerforceFstatError' endif endif " Extract the value from the string which looks like: " ... headRev 65\n\n let val = split(split(substitute(s, '\r', '', ''), '\n')[0])[2] if g:perforce_debug echom 'fstat got value ' . val . ' for field ' . a:field \ . ' on file ' . a:filename endif return val endfunction " Assert fstat field function! s:PerforceAssert(field, filename, msg) try let retval = s:PerforceFstat(a:field, a:filename) catch /PerforceFstatError/ call s:EchoError(a:msg) return 0 endtry return retval endfunction " Query fstat field function! s:PerforceQuery(field, filename) try let retval = s:PerforceFstat(a:field, a:filename) catch /PerforceFstatError/ return 0 endtry return retval endfunction " }}} " {{{ Perforce field checkers " Tests for existence in depot. Issues error message upon failure. " Can be used to test existence of a specific revision, or shelved in a " particular changelist by adding revision specifier to filename. " " Abbreviated summary: " #n - revision 'n' " #have - have revision " @=n - at changelist 'n' (shelved) function! s:PerforceAssertExists(filename) let msg = a:filename . ' does not exist on the server' return s:PerforceAssert('headRev', a:filename, msg) != '' endfunction " Tests for opened. Issues error message upon failure. function! s:PerforceAssertOpened(filename) let msg = a:filename . ' not opened for change' return s:PerforceAssert('action', a:filename, msg) != '' endfunction " Tests for opened. function! s:PerforceExists(filename) return s:PerforceQuery('headRev', a:filename) != '' endfunction " Tests for whether a given path is a directory in perforce " given either a local path or a server path function! s:PerforceGetDirectory(filepath) let filepath = a:filepath " p4 commands do not expect trailing '/' if strpart(filepath, strlen(filepath) - 1, 1) == '/' let filepath = strpart(filepath, 0, strlen(filepath) - 1) endif " get server path if filepath[0:1] == '//' " given server path let perforce_filepath = filepath else " given local path let perforce_filepath = filepath let command = 'where ' . filepath " NB: `p4 where` only works on directories below the root " e.g. `p4 where //main` will fail if 'main' is the root let retval = s:PerforceSystem(command) if v:shell_error || strlen(retval) == 0 return '' endif let perforce_filepath = split(retval)[0] endif " verify server path " TODO potentially use parent directory as input if given file let command = 'dirs ' . perforce_filepath let retval = s:PerforceSystem(command) let retval = trim(retval) if v:shell_error || (retval != perforce_filepath) return '' endif return perforce_filepath endfunction " Tests for opened. function! s:PerforceOpened(filename) return s:PerforceQuery('action', a:filename) != '' endfunction " Return changelist that given file is open in function! s:PerforceGetCurrentChangelist(filename) return s:PerforceQuery('change', a:filename) endfunction " Return have revision number function! s:PerforceHaveRevision(filename) return s:PerforceQuery('haveRev', a:filename) endfunction " }}} " {{{ Perforce revision specification helpers " Return filename with any revision specifier stripped function! s:PerforceStripRevision(filename) return split(a:filename, '#')[0] endfunction " Return filename with appended revision specifier " " Priority list: " 1. Embedded revision specifier in filename " 2. Synced revision " 3. Head revision (no specifier required) function! s:PerforceAddRevision(filename) " embedded revision let embedded_rev = matchstr(a:filename, '#\zs[0-9]\+\ze') if embedded_rev != '' return a:filename endif " have revision let have_revision = s:PerforceHaveRevision(a:filename) if have_revision return a:filename . '#' . have_revision endif " no specifier return a:filename endfunction " Return filename with appended 'have revision - 1' specifier " If editing a file with the revision aleady embedded in the name, return " the revision before that instead. function! s:PerforceAddPrevRevision(filename) let embedded_rev = matchstr(a:filename, '#\zs[0-9]\+\ze') if embedded_rev != '' let prev_rev = embedded_rev - 1 return substitute(a:filename, embedded_rev, prev_rev, "") else let prev_rev = s:PerforceHaveRevision(a:filename) - 1 return a:filename . '#' . prev_rev endif endfunction " }}} " }}} " {{{ Main functions " {{{ System function! vp4#PerforceSystemWr(...) let cmd = join(map(copy(a:000), 'expand(v:val)')) " open a preview window pedit __vp4_scratch__ wincmd P " call p4 describe normal! ggdG silent call s:PerforceRead(cmd) setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile nowrap " return to original windown wincmd p endfunction " }}} " {{{ File editing " Call p4 add. function! vp4#PerforceAdd() let filename = s:ExpandPath('%') call s:PerforceSystem('add ' .filename) endfunction " Call p4 delete. function! vp4#PerforceDelete(bang) let filename = s:ExpandPath('%') if !s:PerforceAssertExists(filename) | return | endif if !a:bang let do_delete = input('Are you sure you want to delete ' . filename \ . '? [y/n]: ') endif if a:bang || do_delete ==? 'y' call s:PerforceSystem('delete ' .filename) bdelete endif endfunction " Call p4 edit. function! vp4#PerforceEdit() let filename = s:ExpandPath('%') if !s:PerforceAssertExists(filename) | return | endif call s:PerforceSystem('edit ' .filename) " reload the file to refresh &readonly attribute execute 'edit ' filename endfunction " Call p4 revert. Confirms before performing the revert. function! vp4#PerforceRevert(bang) let filename = s:ExpandPath('%') if !s:PerforceAssertOpened(filename) | return | endif if !a:bang let do_revert = input('Are you sure you want to revert ' . filename \ . '? [y/n]: ') endif if a:bang || do_revert ==? 'y' call s:PerforceSystem('revert ' .filename) set nomodified endif " reload the file to refresh &readonly attribute execute 'edit ' filename endfunction " }}} " {{{ Change specification " Call p4 shelve function! vp4#PerforceShelve(bang) let filename = s:ExpandPath('%') if !s:PerforceAssertOpened(filename) | return | endif let perforce_command = 'shelve' let cl = s:PerforceGetCurrentChangelist(filename) if cl !~# 'default' let perforce_command .= ' -c ' . cl if a:bang | let perforce_command .= ' -f' | endif let msg = split(s:PerforceSystem(perforce_command . ' ' . filename), '\n') if v:shell_error | call s:EchoError(msg[-1]) | endif let msg = filename . ' shelved in p4:' . cl echom msg else call s:EchoError('Files open in the default changelist' \ . ' may not be shelved. Create a changelist first.') endif endfunction " Use contents of buffer to send a change specification function! s:PerforceWriteChange() silent call s:PerforceWrite('change -i') " If the change was made successfully, mark the file as no longer modified " (so that Vim doesn't warn user that a file has been modified but not " written on exit) and close the window. " " Note: leaves an open buffer. Unloading a buffer in an autocommand issues " an error message, so this buffer has been intentionally left open by the " author. if !v:shell_error set nomodified close endif endfunction " Call p4 change " Uses the -o/-i options to avoid the confirmation on abort. " Works by opening a new window to write your change description. function! vp4#PerforceChange() let filename = s:ExpandPath('%') let perforce_command = 'change -o' let lnr = 25 " If this file is already in a changelist, allow the user to modify that " changelist by calling `p4 change -o `. Otherwise, call for default " changelist by omitting the changelist argument. if s:PerforceOpened(filename) let changelist = s:PerforceGetCurrentChangelist(filename) if changelist let perforce_command .= ' ' . changelist let lnr = 27 endif endif " Open a new split to hold the change specification. Clear it in case of " any previous invocations. topleft new __vp4_change__ normal! ggdG silent call s:PerforceRead(perforce_command) " Reset the 'modified' option so that only user modifications are captured set nomodified " Put cursor on the line where users write the changelist description. execute lnr " Replace write command (:w) with call to write change specification. " Prevents the buffer __vp4_change__ from being written to disk augroup WriteChange autocmd! * autocmd BufWriteCmd call PerforceWriteChange() augroup END endfunction " Call `p4 describe` on the changelist of the current file, if any. Show the " output in a preview window. function! vp4#PerforceDescribe() let filename = s:ExpandPath('%') let current_changelist = s:PerforceGetCurrentChangelist(filename) if !current_changelist call s:EchoWarning(filename . ' is not open in a named changelist') return endif " open a preview window pedit __vp4_describe__ wincmd P " call p4 describe normal! ggdG let perforce_command = "describe " . current_changelist silent call s:PerforceRead(perforce_command) setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile nowrap " return to original windown wincmd p endfunction " Prompt the user to move file currently being edited to a different changelist. " Present the user with a list of current changes. function! vp4#PerforceReopen() let filename = s:ExpandPath('%') if !s:PerforceAssertOpened(filename) | return | endif " Get the pending changes in the current client let perforce_command = "changes -u $USER -s pending -c $P4CLIENT" let changes = split(s:PerforceSystem(perforce_command), '\n') " Prepend with choice numbers, starting at 1 call map(changes, 'v:key + 1 . ". " . v:val') " Prompt the user let currentchangelist = s:PerforceGetCurrentChangelist(filename) echom filename . ' is currently open in change "' . currentchangelist \ . '" Select a changelist to move to: ' let change = inputlist(changes + [len(changes) + 1 . '. default']) " From the user's input, get the actual changelist number if !change | return | endif let change_number = change > len(changes) ? 'default' \ : split(changes[change - 1], ' ')[2] echom 'Moving ' . filename . ' to change ' . change_number " Perform the reopen command let perforce_command = 'reopen -c ' . change_number . ' ' . filename call s:PerforceSystem(perforce_command) endfunction " }}} " {{{ Analysis " Open repository revision in diff mode " Options: " s diffs with shelved in file's current changelist " @cl diffs with shelved in given changelist " p diffs with previous revision (i.e. have revision - 1) " #rev diffs with given revision " diffs with have revision function! vp4#PerforceDiff(...) let filename = s:ExpandPath('%') " Check for options " 'a:0' is set to the number of extra arguments " a:1 is the first extra argument, a:2 the second, etc. " @cl: Diff with shelved in a:1 if a:0 >= 1 && a:1[0] == '@' let cl = split(a:1, '@')[0] let filename .= '@=' . trim(cl) " #rev: Diff with revision a:1 elseif a:0 >= 1 && a:1[0] == '#' let filename = s:PerforceStripRevision(filename) . trim(a:1) " s: Diff with shelved in current changelist elseif a:0 >= 1 && a:1 =~? 's' let filename .= '@=' . s:PerforceGetCurrentChangelist(filename) " p: Diff with previous version elseif a:0 >= 1 && a:1 =~? 'p' let filename = s:PerforceAddPrevRevision(filename) " default: diff with have revision else if !s:PerforceAssertOpened(filename) | return | endif let filename .= '#have' endif " Assert valid revision if !s:PerforceAssertExists(filename) | return | endif " Setup current window let filetype = &filetype diffthis " Create the new window and populate it execute 'leftabove vnew ' . shellescape(filename, 1) let perforce_command = 'print' if g:vp4_diff_suppress_header let perforce_command .= ' -q' endif let perforce_command .= ' ' . shellescape(filename, 1) silent call s:PerforceRead(perforce_command) " Set local buffer options setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile nowrap setlocal nomodifiable setlocal nomodified execute "set filetype=" . filetype diffthis nnoremap q :bdelete :windo diffoff endfunction " Syntax highlighting for annotation data function! s:PerforceAnnotateHighlight() syn match VP4Change /\v\d+$/ syn match VP4Date /\v\d{4}\/\d{2}\/\d{2}/ syn match VP4Time /\v\d{2}:\d{2}:\d{2}( [A-Z]{3})?/ hi def link VP4Change Number hi def link VP4Date Comment hi def link VP4Time Comment hi def link VP4User Keyword endfunction " Populate change metadata, namely: user, date, description. Assumes buffer " contains one changelist number per line. function! s:PerforceAnnotateFull(lbegin, lend) let data = {} let last_cl = 0 set modifiable let lnr = a:lbegin while lnr && lnr <= a:lend let line = getline(lnr) " Only query the changelist information from perforce if we have not " seen this change before. While this could take up significant amounts " of memory for a large file, it should still be much faster than " additional calls to `p4 change` if !has_key(data, line) let data[line] = {} let cl_data = split(s:PerforceSystem('change -o ' . line), '\n') try let description_index = match(cl_data, '^Description') let data[line]['description'] = substitute(join(cl_data[description_index + 1:-1]), \ "\t", "", "g") " Format: 'Date:\t