1 " ==============================================================================
3 " Author: Srinath Avadhanula (srinathava [AT] gmail [DOT] com)
4 " Help: vng.vim is a plugin which implements some simple mappings
5 " to make it easier to use vng, David Roundy's distributed
7 " Copyright: Srinath Avadhanula
8 " License: Vim Charityware license. (:help uganda)
10 " As of right now, this script provides the following mappings:
12 " <Plug>VngVertDiffsplit
13 " This mapping vertically diffsplit's the current window with the
14 " last recorded version.
16 " Default map: <leader>dkv
18 " <Plug>VngIneractiveDiff
19 " This mapping displays the changes for the current file. The user
20 " can then choose two revisions from the list using the 'f' (from)
21 " and 't' (to) keys. After choosing two revisions, the user can press
22 " 'g' at which point, the delta between those two revisions is
23 " displayed in two veritcally diffsplit'ted windows.
25 " Default map: <leader>dki
27 " <Plug>VngStartCommitSession
28 " This mapping starts an interactive commit console using which the
29 " user can choose which hunks to include in the record and then also
30 " write out a patch name and commit message.
32 " Default map: <leader>dkc
33 " ==============================================================================
35 " ==============================================================================
37 " ==============================================================================
38 " Functions related to diffsplitting current file with last recorded {{{
40 nmap <Plug>VngVertDiffsplit :call Vng_DiffSplit()<CR>
41 if !hasmapto('<Plug>VngVertDiffsplit')
42 nmap <unique> <leader>dkv <Plug>VngVertDiffsplit
45 " Vng_DiffSplit: diffsplits the current file with last recorded version {{{
46 function! Vng_DiffSplit()
47 let origDir = getcwd()
48 call Vng_CD(expand('%:p:h'))
50 " first find out the location of the repository by searching upwards
51 " from the current directory for a _vng* directory.
52 if Vng_SetVngDirectory() < 0
56 " Find out if the current file has any changes.
57 if Vng_System('vng whatsnew '.expand('%')) =~ '\(^\|\n\)No changes'
58 echomsg "vng says there are no changes"
62 " Find out the relative path name from the parent dir of the _vng
63 " directory to the location of the presently edited file
64 let relPath = strpart(expand('%:p'), strlen(b:vng_repoDirectory))
66 " The last recorded file lies in
67 " $DIR/_vng/current/relative/path/to/file
69 " $DIR/relative/path/to/file
70 " is the actual path of the file
71 let lastRecFile = b:vng_repoDirectory . '/'
72 \ . '_vng/current' . relPath
74 call Vng_Debug(':Vng_DiffSplit: lastRecFile = '.lastRecFile)
76 call Vng_NoteViewState()
78 execute 'vert diffsplit '.lastRecFile
79 " The file in _vng/current should not be edited by the user.
82 nmap <buffer> q <Plug>VngRestoreViewAndQuit
88 " Vng_SetVngDirectory: finds which _vng directory contains this file {{{
89 " Description: This function searches upwards from the current directory for a
91 function! Vng_SetVngDirectory()
92 let filename = expand('%:p')
93 let origDir = getcwd()
96 call Vng_CD(expand('%:p:h'))
97 let lastDir = getcwd()
98 while glob('_vng*') == ''
100 " If we cannot go up any further, then break
101 if lastDir == getcwd()
104 let lastDir = getcwd()
107 " If a _vng directory was never found, then quit...
108 if glob('_vng*') == ''
110 echo "_vng directory not found in or above current directory"
115 let b:vng_repoDirectory = getcwd()
125 " Functions related to interactive diff splitting {{{
127 nmap <Plug>VngIneractiveDiff :call Vng_StartInteractiveDiffSplit()<CR>
128 if !hasmapto('<Plug>VngIneractiveDiff')
129 nmap <unique> <leader>dki <Plug>VngIneractiveDiff
132 " Vng_StartInteractiveDiffSplit: initializes the interactive diffsplit session {{{
133 function! Vng_StartInteractiveDiffSplit()
134 let origdir = getcwd()
135 let filedir = expand('%:p:h')
136 let filename = expand('%:p:t')
140 call Vng_OpenScratchBuffer()
142 let changelog = Vng_FormatChangelog(filename)
144 echomsg "vng does not know about this file"
147 0put=Vng_FormatChangelog(filename)
152 " reading in stuff creates an extra line ending
156 \ '====[ Vng diff console: Mappings ]==========================' . "\n" .
158 \ 'j/k : next/previous patch' . "\n" .
159 \ 'f : set "from" patch' . "\n" .
160 \ 't : set "to" patch' . "\n" .
161 \ 'g : proceed with diff (Go)' . "\n" .
162 \ 'q : quit without doing anything further'. "\n" .
163 \ '=============================================================='
167 let b:vng_FROM_hash = ''
168 let b:vng_TO_hash = ''
169 let b:vng_orig_ft = filetype
170 let b:vng_orig_file = filename
171 let b:vng_orig_dir = filedir
173 nmap <buffer> j <Plug>VngGotoNextChange
174 nmap <buffer> k <Plug>VngGotoPreviousChange
175 nmap <buffer> f <Plug>VngSetFromHash
176 nmap <buffer> t <Plug>VngSetToHash
177 nmap <buffer> g <Plug>VngProceedWithDiffSplit
178 nmap <buffer> q :q<CR>:<BS>
180 call search('^\s*\*')
184 " Vng_FormatChangelog: creates a Changelog with hash for a file {{{
185 function! Vng_FormatChangelog(filename)
187 " remove the first <created_as ../> entry
188 let xmloutput = Vng_System('vng changes --xml-output '.a:filename)
189 " TODO: We need to see if there were any errors generated by vng or
190 " if the xmloutput is empty.
192 let idx = match(xmloutput, '<\/created_as>')
193 let xmloutput = strpart(xmloutput, idx + strlen('</created_as>'))
197 " For each patch in the xml output
198 let idx = match(xmloutput, '</patch>')
200 " extract the actual patch.
201 let patch = strpart(xmloutput, 0, idx)
203 " From the patch, extract the various fields.
204 let name = matchstr(patch, '<name>\zs.*\ze<\/name>')
205 let comment = matchstr(patch, '<comment>\zs.*\ze</comment>')
207 let pattern = ".*<patch author='\\(.\\{-}\\)' date='\\(.\\{-}\\)' local_date='\\(.\\{-}\\)' inverted='\\(True\\|False\\)' hash='\\(.\\{-}\\)'>.*"
209 let author = substitute(patch, pattern, '\1', '')
210 let date = substitute(patch, pattern, '\2', '')
211 let local_date = substitute(patch, pattern, '\3', '')
212 let inverted = substitute(patch, pattern, '\4', '')
213 let hash = substitute(patch, pattern, '\5', '')
215 " Create the formatted changelog entry for this patch.
216 " Unfortunately, we do not have the +M -5 +10 kind of line in this
217 " output. Price to pay...
218 let formatted = "[hash ".hash."]\n".local_date.' '.author."\n * ".name."\n".
219 \ substitute(comment, "\n", "\n ", 'g')."\n\n"
221 let changelog = changelog.formatted
223 " Once the patch has been processed, snip if from the xml output.
224 let xmloutput = strpart(xmloutput, idx + strlen('</patch>'))
225 let idx = match(xmloutput, '</patch>')
229 let changelog = "Changes in ".a:filename.":\n\n".changelog
231 let s:sub_{'''} = "'"
232 let s:sub_{'"'} = '"'
233 let s:sub_{'&'} = '&'
234 let s:sub_{'<'} = '<'
235 let s:sub_{'>'} = '>'
238 if exists("s:sub_{'".a:expr."'}")
239 return s:sub_{a:expr}
245 let changelog = substitute(changelog, '&\w\{2,4};', '\=Eval(submatch(0))', 'g')
256 " Vng_SetHash: remembers the user's from/to hashs for the diff {{{
258 nnoremap <Plug>VngSetToHash :call Vng_SetHash('TO')<CR>
259 nnoremap <Plug>VngSetFromHash :call Vng_SetHash('FROM')<CR>
261 function! Vng_SetHash(fromto)
263 call search('^\s*\*', 'bw')
265 " First remove any previous mark set.
266 execute '% s/'.a:fromto.'$//e'
268 " remember the present line's hash
269 let b:vng_{a:fromto}_hash = matchstr(getline(line('.')-2), '\[hash \zs.\{-}\ze\]')
271 call setline(line('.')-1, getline(line('.')-1).' '.a:fromto)
275 " Vng_GotoChange: goes to the next previous change {{{
277 nnoremap <Plug>VngGotoPreviousChange :call Vng_GotoChange('b')<CR>
278 nnoremap <Plug>VngGotoNextChange :call Vng_GotoChange('')<CR>
280 function! Vng_GotoChange(dirn)
281 call search('^\s*\*', a:dirn.'w')
285 " Vng_ProceedWithDiff: proceeds with the actual diff between requested versions {{{
288 nnoremap <Plug>VngProceedWithDiffSplit :call Vng_ProceedWithDiff()<CR>
290 function! Vng_ProceedWithDiff()
291 call Vng_Debug('+Vng_ProceedWithDiff')
293 if b:vng_FROM_hash == '' && b:vng_TO_hash == ''
295 echo "You need to set at least one of the FROM or TO hashes"
300 let origDir = getcwd()
302 call Vng_CD(b:vng_orig_dir)
304 let ft = b:vng_orig_ft
305 let filename = b:vng_orig_file
306 let fromhash = b:vng_FROM_hash
307 let tohash = b:vng_TO_hash
309 " quit the window which shows the Changelog
312 " First copy the present file into a temporary location
313 let tmpfile = tempname().'__present'
315 call Vng_System('cp '.filename.' '.tmpfile)
318 let tmpfilefrom = tempname().'__from'
319 call Vng_RevertToState(filename, fromhash, tmpfilefrom)
320 let file1 = tmpfilefrom
326 let tmpfileto = tempname().'__to'
327 call Vng_RevertToState(filename, tohash, tmpfileto)
328 let file2 = tmpfileto
333 call Vng_Debug(':Vng_ProceedWithDiff: file1 = '.file1.', file2 = '.file2)
335 execute 'split '.file1
336 execute "nnoremap <buffer> q :q\<CR>:e ".file2."\<CR>:q\<CR>"
338 execute 'vert diffsplit '.file2
340 execute "nnoremap <buffer> q :q\<CR>:e ".file1."\<CR>:q\<CR>"
344 call Vng_Debug('-Vng_ProceedWithDiff')
348 " Vng_RevertToState: reverts a file to a previous state {{{
349 function! Vng_RevertToState(filename, patchhash, tmpfile)
350 call Vng_Debug('+Vng_RevertToState: pwd = '.getcwd())
352 let syscmd = "vng diff --from-match \"hash ".a:patchhash."\" ".a:filename.
353 \ " | patch -R ".a:filename." -o ".a:tmpfile
354 call Vng_Debug(':Vng_RevertToState: syscmd = '.syscmd)
355 call Vng_System(syscmd)
357 let syscmd = "vng diff --match \"hash ".a:patchhash."\" ".a:filename.
358 \ " | patch ".a:tmpfile
359 call Vng_Debug(':Vng_RevertToState: syscmd = '.syscmd)
360 call Vng_System(syscmd)
362 call Vng_Debug('-Vng_RevertToState')
368 " Functions related to interactive comitting {{{
370 nmap <Plug>VngStartCommitSession :call Vng_StartCommitSession()<CR>
371 if !hasmapto('<Plug>VngStartCommitSession')
372 nmap <unique> <leader>dkc <Plug>VngStartCommitSession
375 " Vng_StartCommitSession: start an interactive commit "console" {{{
376 function! Vng_StartCommitSession()
378 let origdir = getcwd()
379 let filename = expand('%:p:t')
380 let filedir = expand('%:p:h')
383 let wn = Vng_System('vng whatsnew --no-summary --dont-look-for-adds')
386 if wn =~ '^No changes!'
387 echo "No changes seen by vng"
391 " read in the contents of the `vng whatsnew` command
392 call Vng_SetSilent('silent')
394 " opens a scratch buffer for the commit console
395 " Unfortunately, it looks like the temporary file has to exist in the
396 " present directory because at least on Windows, vng doesn't want to
397 " handle absolute path names containing ':' correctly.
398 exec "top split ".Vng_GetTempName(expand('%:p:h'))
401 " Delete the end and beginning markers
403 " Put an additional four spaces in front of all lines and a little
404 " checkbox in front of the hunk specifier lines
409 \ '====[ Vng commit console: Mappings ]========================' . "\n" .
411 \ 'J/K : next/previous hunk' . "\n" .
412 \ 'Y/N : accept/reject this hunk' . "\n" .
413 \ 'F/S : accept/reject all hunks from this file' . "\n" .
414 \ 'A/U : accept/reject all (remaining) hunks' . "\n" .
415 \ 'q : quit this session without committing' . "\n" .
416 \ 'L : goto log area to record description'. "\n" .
417 \ 'R : done! finish record' . "\n" .
418 \ '=============================================================='
422 \ '====[ Vng commit console: Commit log description ]==========' . "\n" .
425 \ 'Write the long patch description in this area.'. "\n".
426 \ 'The first line in this area will be the patch name.'. "\n".
427 \ 'Everything in this area from the above ***VNG*** line on '."\n".
428 \ 'will be ignored.'. "\n" .
429 \ '=============================================================='
433 call Vng_SetSilent('notsilent')
435 " Set the fold expression so that things get folded up nicely.
437 set foldexpr=Vng_WhatsNewFoldExpr(v:lnum)
439 " Finally goto the first hunk
441 call search('^\[ ', 'w')
443 nnoremap <buffer> J :call Vng_GotoHunk('')<CR>:<BS>
444 nnoremap <buffer> K :call Vng_GotoHunk('b')<CR>:<BS>
446 nnoremap <buffer> Y :call Vng_SetHunkVal('y')<CR>:<BS>
447 nnoremap <buffer> N :call Vng_SetHunkVal('n')<CR>:<BS>
449 nnoremap <buffer> A :call Vng_SetRemainingHunkVals('y')<CR>:<BS>
450 nnoremap <buffer> U :call Vng_SetRemainingHunkVals('n')<CR>:<BS>
452 nnoremap <buffer> F :call Vng_SetFileHunkVals('y')<CR>:<BS>
453 nnoremap <buffer> S :call Vng_SetFileHunkVals('n')<CR>:<BS>
455 nnoremap <buffer> L :call Vng_GotoLogArea()<CR>
456 nnoremap <buffer> R :call Vng_FinishCommitSession()<CR>
458 nnoremap <buffer> q :call Vng_DeleteTempFile(expand('%:p'))<CR>
460 let b:vng_orig_file = filename
461 let b:vng_orig_dir = filedir
465 " Vng_WhatsNewFoldExpr: foldexpr function for a commit console {{{
466 function! Vng_WhatsNewFoldExpr(lnum)
467 if matchstr(getline(a:lnum), '^\[[yn ]\]') != ''
469 elseif matchstr(getline(a:lnum+1), '^====\[ .* log description') != ''
477 " Vng_GotoHunk: goto next/previous hunk in a commit console {{{
478 function! Vng_GotoHunk(dirn)
479 call search('^\[[yn ]\]', a:dirn.'w')
483 " Vng_SetHunkVal: accept/reject a hunk for committing {{{
484 function! Vng_SetHunkVal(yesno)
485 if matchstr(getline('.'), '\[[yn ]\]') == ''
488 execute "s/^\\[.\\]/[".a:yesno."]"
489 call Vng_GotoHunk('')
493 " Vng_SetRemainingHunkVals: accept/reject all remaining hunks {{{
494 function! Vng_SetRemainingHunkVals(yesno)
495 execute "% s/^\\[ \\]/[".a:yesno."]/e"
499 " Vng_SetFileHunkVals: accept/reject all hunks from this file {{{
500 function! Vng_SetFileHunkVals(yesno)
501 " If we are not on a hunk line for some reason, then do not do
503 if matchstr(getline('.'), '\[[yn ]\]') == ''
507 " extract the file name from the current line
508 let filename = matchstr(getline('.'),
509 \ '^\[[yn ]\] hunk \zs\f\+\ze')
514 " mark all hunks belonging to the file with yes/no
515 execute '% s/^\[\zs[yn ]\ze\] hunk '.escape(filename, "\\/").'/'.a:yesno.'/e'
517 call Vng_GotoHunk('')
521 " Vng_GotoLogArea: records the log description of the commit {{{
522 function! Vng_GotoLogArea()
525 echo "There are still some hunks which are neither accepted or rejected"
526 echo "Please set the status of all hunks before proceeding to log"
532 call search('\M^***VNG***')
538 " Vng_FinishCommitSession: finishes the interactive commit session {{{
539 function! Vng_FinishCommitSession()
541 " First make sure that all hunks have been set as accpeted or rejected.
545 echo "There are still some hunks which are neither accepted or rejected"
546 echo "Please set the status of all hunks before proceeding to log"
552 " Then make a list of the form "ynyy..." from the choices made by the
555 g/^\[[yn]\]/let yesnolist = yesnolist.matchstr(getline('.'), '^\[\zs.\ze\]')
557 " make sure that a valid log message has been written.
558 call search('====\[ Vng commit console: Commit log description \]', 'w')
561 execute "normal! V/\\M***VNG***/s\<CR>k\"ky"
566 call Vng_Debug(':Vng_FinishCommitSession: logMessage = '.logMessage)
567 call Vng_Debug(':Vng_FinishCommitSession: yesnolist = ['.yesnolist.']')
569 if logMessage !~ '[[:graph:]]'
572 echo "The log message is either ill formed or empty"
573 echo "Please repair the mistake and retry"
579 " Remove everything from the file except the log file.
585 let origDir = getcwd()
586 call Vng_CD(b:vng_orig_dir)
588 let vngOut = Vng_System('echo '.yesnolist.' | vng record --logfile='.expand('%:p:t'))
596 \ "=================================================================\n".
597 \ "Press q to delete this temporary file and quit the commit session\n".
598 \ "If you quit this using :q, then a temp file will remain in the \n".
599 \ "present directory\n".
600 \ "================================================================="
608 " Vng_DeleteTempFile: deletes temp file created during commit session {{{
609 function! Vng_DeleteTempFile(fname)
610 call Vng_Debug('+Vng_DeleteTempFile: fname = '.a:fname.', bufnr = '.bufnr(a:fname))
611 if bufnr(a:fname) > 0
612 execute 'bdelete! '.bufnr(a:fname)
614 let sysout = Vng_System('rm '.a:fname)
621 " ==============================================================================
623 " ==============================================================================
624 " Vng_System: runs a system command escaping quotes {{{
626 " By default we will use python if available.
627 if !exists('g:Vng_UsePython')
628 let g:Vng_UsePython = 1
631 function! Vng_System(syscmd)
632 if has('python') && g:Vng_UsePython == 1
633 execute 'python pySystem(r"""'.a:syscmd.'""")'
636 if &shellxquote =~ '"'
637 return system(escape(a:syscmd, '"'))
639 return system(a:syscmd)
644 if has('python') && g:Vng_UsePython == 1
649 def pySystem(command):
650 (cstdin, cstdout) = os.popen2(command)
654 vim.command("""let retval = "%s" """ % re.sub(r'"|\\', r'\\\g<0>', out))
660 " Vng_CD: cd's to a directory {{{
661 function! Vng_CD(dirname)
662 execute "cd ".escape(a:dirname, ' ')
666 " Vng_OpenScratchBuffer: opens a scratch buffer {{{
667 function! Vng_OpenScratchBuffer()
675 " Vng_NoteViewState: notes the current fold related settings of the buffer {{{
676 function! Vng_NoteViewState()
677 let b:vng_old_diff = &l:diff
678 let b:vng_old_foldcolumn = &l:foldcolumn
679 let b:vng_old_foldenable = &l:foldenable
680 let b:vng_old_foldmethod = &l:foldmethod
681 let b:vng_old_scrollbind = &l:scrollbind
682 let b:vng_old_wrap = &l:wrap
686 " Vng_ResetViewState: restores the fold related settings of a buffer {{{
687 function! Vng_ResetViewState()
688 let &l:diff = b:vng_old_diff
689 let &l:foldcolumn = b:vng_old_foldcolumn
690 let &l:foldenable = b:vng_old_foldenable
691 let &l:foldmethod = b:vng_old_foldmethod
692 let &l:scrollbind = b:vng_old_scrollbind
693 let &l:wrap = b:vng_old_wrap
696 nnoremap <Plug>VngRestoreViewAndQuit :q<CR>:call Vng_ResetViewState()<CR>:<BS>
699 " Vng_GetTempName: get the name of a temporary file in specified directory {{{
700 " Description: Unlike vim's native tempname(), this function returns the name
701 " of a temporary file in the directory specified. This enables
702 " us to create temporary files in a specified directory.
703 function! Vng_GetTempName(dirname)
704 let prefix = 'vngVimTemp'
705 let slash = (a:dirname =~ '\\\|/$' ? '' : '/')
707 while filereadable(a:dirname.slash.prefix.i.'.tmp') && i < 1000
710 if filereadable(a:dirname.slash.prefix.i.'.tmp')
711 echoerr "Temporary file could not be created in ".a:dirname
714 return expand(a:dirname.slash.prefix.i.'.tmp', ':p')
717 " Vng_SetSilent: sets options to make vim "silent" {{{
718 function! Vng_SetSilent(isSilent)
719 if a:isSilent == 'silent'
720 let s:_showcmd = &showcmd
722 let s:_report = &report
727 let &showcmd = s:_showcmd
729 let &report = s:_report
732 " Vng_Debug: appends the argument into s:debugString {{{
733 if !exists('g:Vng_Debug')
735 let s:debugString = ''
737 function! Vng_Debug(str)
738 let s:debugString = s:debugString.a:str."\n"
740 " Vng_PrintDebug: prings s:debugString {{{
741 function! Vng_PrintDebug()
744 " Vng_ClearDebug: clears the s:debugString string {{{
745 function! Vng_ClearDebug(...)
746 let s:debugString = ''