2013/06/08

vcscommand.vimを少し便利に使う

本記事は Vim Advent Calendar 2012 の189日目です。188日目は @supermomonga さんの Vimのチートシート生成サービス作りました。感じ感じ。 | かなりすごいブログ でした。

ここでは Vim からバージョン管理システム上のファイルを扱う際に便利なプラグイン vcscommand.vim とその少し便利な使い方について紹介します。

vcscommand.vimとは

名前の通り Vim からバージョン管理システム(Version Control System:VCS)に対する操作を行うためのプラグインです。特定の VCS を目的としたものではなく、CVS、Subversion、SVK、Git、Mercurial、Bazaar など複数の VCS を同じように操作できるのが特徴です。

操作と言っても複数のファイルをまとめて扱う類のものではなく、開いている単一のファイルに対する操作が中心になります。

インストール

プラグインマネージャを使う場合

プラグインのインストール方法はいくつかありますが、Vundle や NeoBundle を使用している場合は名前だけで導入できて楽です。(以下は Vundle の例)

Bundle 'vcscommand.vim'
このプラグインに限りませんが何らかのプラグインマネージャを導入しておくと気軽に試せるのでオススメです。

zipファイルを解凍する場合

runtimepath にこだわりが無い場合は vim-online から zip ファイルをダウンロードして ~/.vim 以下に展開します。

% curl -L -o ./vcscommand-1.99.47.zip http://www.vim.org/scripts/download_script.php?src_id=19809

% unzip ./vcscommand-1.99.47.zip -d ~/.vim
Archive:  ./vcscommand-1.99.47.zip
c0d27010dd0c96884cfd34c3af621ae009109c69
   creating: /home/yoshikaw/.vim/doc/
  inflating: /home/yoshikaw/.vim/doc/vcscommand.txt
   creating: /home/yoshikaw/.vim/plugin/
  inflating: /home/yoshikaw/.vim/plugin/vcsbzr.vim
  inflating: /home/yoshikaw/.vim/plugin/vcscommand.vim
  inflating: /home/yoshikaw/.vim/plugin/vcscvs.vim
  inflating: /home/yoshikaw/.vim/plugin/vcsgit.vim
  inflating: /home/yoshikaw/.vim/plugin/vcshg.vim
  inflating: /home/yoshikaw/.vim/plugin/vcssvk.vim
  inflating: /home/yoshikaw/.vim/plugin/vcssvn.vim
   creating: /home/yoshikaw/.vim/syntax/
  inflating: /home/yoshikaw/.vim/syntax/cvsannotate.vim
  inflating: /home/yoshikaw/.vim/syntax/gitannotate.vim
  inflating: /home/yoshikaw/.vim/syntax/hgannotate.vim
  inflating: /home/yoshikaw/.vim/syntax/svkannotate.vim
  inflating: /home/yoshikaw/.vim/syntax/svnannotate.vim
  inflating: /home/yoshikaw/.vim/syntax/vcscommit.vim
ファイル一覧をみるとサポートしている各 VCS の実装があることが分かります。

使い方

VCS で一般的な操作に対応したコマンドが用意されています。CVS 専用のコマンドもありますが、長いこと使ってないのでコマンドの意味は忘れてしまいました。このプラグインを使う上では、よほど頻繁に使う場合を除いて VCS 固有のコマンドは覚えなくても良いかなと思っています。詳しくは :h vcscommand でご確認ください。

コマンド

デフォルト
マッピング
コマンド説明
<Leader>caVCSAddファイルを VCS 管理に追加
<Leader>cnVCSAnnotate
(VCSBlame)
行毎にリビジョンと最終更新者の情報を表示
! でアノテーションをファイルと別のバッファに分割して同期表示
<Leader>cNVCSCommitVCS 管理のファイルをコミット
! で空のコミットログ
<Leader>cDVCSDelete
(VCSRemove)
ファイルを VCS 管理から削除
<Leader>cdVCSDiffファイルをリポジトリの現在のリビジョンと比較(Unified Diff 形式)
引数を1つ与えた場合、そのリビジョンとファイルを比較
引数を2つ与えた場合、その2つのリビジョンを比較
<Leader>cgVCSGotoOriginalVCS 管理のファイルを開いているバッファを表示
<Leader>cGVCSGotoOriginal!VCS 管理のファイルを開いているバッファを表示して残りのスクラッチバッファを除去
<Leader>ciVCSInfoファイルの VCS 管理情報を表示
<Leader>clVCSLogVCS 管理ログを表示
引数を1つ与えた場合、そのリビジョンのログを表示
それ以外の引数は VCS コマンドに渡される
<Leader>cLVCSLockVCS 管理のファイルをロック(VCSに依存)
VCSRevertファイルを VCS 管理のリビジョンに戻す
<Leader>crVCSReview引数で与えた特定のリビジョンを別バッファに表示
引数を与えない場合は現在のリビジョンを表示
<Leader>csVCSStatusファイルの VCS 管理情報を別バッファに表示
<Leader>cuVCSUpdateVCS 管理の最新ファイルに更新
<Leader>cUVCSUnlockVCS 管理のファイルをアンロック
<Leader>cvVCSVimDiffVCS 管理との差分を VimDiff 形式で表示

CVS専用のコマンド

デフォルト
マッピング
コマンド
<Leader>ceCVSEdit
<Leader>cECVSEditors
<Leader>ctCVSUnedit
CVSWatch
<Leader>cwaCVSWatchAdd (CVSWatch add)
<Leader>cwnCVSWatchOn (CVSWatch on)
<Leader>cwfCVSWatchOff (CVSWatch off)
<Leader>cwrCVSWatchRemove (CVSWatch remove)
<Leader>cwCVSWatchers

マッピング接頭辞をカスタマイズ

mapleader を設定していない場合は \ c l で VCS ログを見る、となります。これはこれで覚えてしまえばよいのですが、g:VCSCommandMapPrefix でマッピングの接頭辞(<Leader>c)を変更できます。

私は次のように設定していますので , v l で VCS ログを見る、となります。

let mapleader = ','
nnoremap [VCS] <Nop>
nmap <Leader>v [VCS]
let g:VCSCommandMapPrefix = '[VCS]'
よく使うものだけマッピングを書いても良いのですが、こうやって接頭辞だけカスタマイズして用意されているデフォルトマッピングを手軽に自分流にできるのも便利ですね。

よく使うコマンド

私がよく使うコマンドについて Subversion での実行例をあわせて紹介します。

実行例の背景画像は @IMAGEDRIVE さんのイラストを @supermomonga さんの Vim チートシート化したものを使用しています。素敵なイラストとツール、たいへん有り難く使わせて頂きます。

VCSLog

VCS といったらまずは変更履歴。管理されているファイルを開いて VCSLog すると、別のバッファに変更履歴が表示されます。

VCSAnnotate

「この変更を入れたのは誰だあっ!!」というのを調べるときに少し役立つ VCSAnnotate。アノテーションがハイライトされたバッファが分割表示されます。

なおこのコマンドは ! をつけて実行(VCSAnnotate!)するとアノテーションとコードを別のバッファにして同期表示してくれますが、区切りが残念な感じになることが多いです。AUTHOR の最大文字数に合わせざるをえないので難しいところですね。

VCSDiff

手元に加えた変更との差分は VCSDiff で Unified 形式で確認できます。

2つ引数を与えると、任意のリビジョン間の差分が確認できます。

VCSVimDiff

VCSVimDiff で vimdiff コマンドと同じように表示できます。このコマンドも引数にリビジョンを指定できます。見比べながらのマージ作業などに役立ちます。

VCSCommit

変更をコミットするときは VCSCommit。実行するとコミットメッセージを入力するバッファが開きます。バッファを保存するとコミットされます。

もう少し使いやすくする

ここまで紹介したコマンドを個別に使うだけでも便利なのですが、どれも単機能で少し物足りません。

vcscommand.vim ではバッファを作る際に VCSBufferCreated というイベントを発生します。これを利用して新規バッファに対してマッピングなどを仕込むことで、q でバッファを手軽に閉じるなどのカスタマイズができます。(詳しくは :h vcscommand-events

実際に使用している設定を以下に貼り付けていますが、具体的にどうカスタマイズしているかというと、

  • VCSLog で開いたバッファで <CR> するとカーソル行のログのリビジョンを取得して VCSDiffv すると VCSVimDiffi すると VCSInfo
  • VCSDiff で開いたバッファで l するとそのリビジョンの VCSLog
  • VCSAnnotate で開いたバッファで v するとカーソル行のリビジョンを取得して VCSDiffv すると VCSVimDiffl するとそのリビジョンの VCSLogi すると VCSInfo
  • ウィンドウが1つの時は vsplit して新規バッファを表示(通常は split)
などとしています。これでカーソル行のあるリビジョンについて、ログ表示→差分表示→ログ表示→差分表示、などと追跡が容易になります。対象とするリビジョンは各 VCS の出力メッセージから取得していますが、いまのところ私が使用することのある Subversion、git、Mercurial しか対応していません。
augroup VCSCommand
autocmd!
autocmd User VCSBufferCreated call s:vcscommand_buffer_settings()
augroup END
function! s:vcscommand_buffer_settings() "{{{3
if !exists('b:VCSCommandCommand') | return | endif
if b:VCSCommandCommand !=# 'commitlog' | setlocal readonly | endif
if b:VCSCommandCommand !=# 'vimdiff' | setlocal nofoldenable | endif
if &filetype ==# 'gitlog' | setlocal syntax=git | endif
nmap <unique> <buffer> <silent> q :bwipeout<CR>
if &filetype =~# '^\(svnlog\|gitlog\|hglog\)$'
nnoremap <silent> <buffer> <CR> :<C-u>call <SID>vcscommand_filetype('log', 'VCSDiff')<CR>gg
nnoremap <silent> <buffer> v :<C-u>call <SID>vcscommand_filetype('log', 'VCSVimDiff')<CR>gg
nnoremap <silent> <buffer> r :<C-u>call <SID>vcscommand_filetype('log', 'VCSReview')<CR>gg
nnoremap <silent> <buffer> i :<C-u>call <SID>vcscommand_filetype('log', 'VCSInfo')<CR>gg
elseif b:VCSCommandCommand =~# '.*annotate'
nnoremap <silent> <buffer> <CR> :<C-u>call <SID>vcscommand_filetype('annotate', 'VCSDiff')<CR>gg
nnoremap <silent> <buffer> v :<C-u>call <SID>vcscommand_filetype('annotate', 'VCSVimDiff')<CR>gg
nnoremap <silent> <buffer> r :<C-u>call <SID>vcscommand_filetype('annotate', 'VCSReview')<CR>gg
nnoremap <silent> <buffer> l :<C-u>call <SID>vcscommand_filetype('annotate', 'VCSLog')<CR>gg
nnoremap <silent> <buffer> i :<C-u>call <SID>vcscommand_filetype('annotate', 'VCSInfo')<CR>gg
endif
endfunction "}}}
function! s:vcscommand_exec(command, option) "{{{3
if a:command =~# '^\(VCSDiff\|VCSLog\)$'
let g:VCSCommandSplit = winnr('$') == 1 ? 'vertical' : 'horizontal'
endif
execute a:command a:option
unlet! g:VCSCommandSplit
endfunction "}}}
function! s:vcscommand_log(...) "{{{3
let option = join(a:000)
if exists('b:VCSCommandVCSType')
if exists('b:VCSCommandCommand')
if b:VCSCommandCommand ==# 'log'
echo "Sorry, you cannot open vcslog on vcslog buffer"
unlet option
elseif b:VCSCommandCommand =~# 'diff\|review'
if !exists('b:VCSCommandStatusText')
echo "Sorry, you are on a working buffer"
unlet option
else
" Shows only the target revision/commit
if b:VCSCommandVCSType ==# 'SVN'
let matched = matchlist(b:VCSCommandStatusText, '(\d\+ : \(\d\+\))')
if len(matched) | let option = matched[1] | endif
elseif b:VCSCommandVCSType ==# 'git'
let matched = matchlist(b:VCSCommandStatusText, '\S\+ \(\w\+\)')
if len(matched) | let option = '-n 1 ' . matched[1] | endif
elseif b:VCSCommandVCSType ==# 'HG'
let matched = matchlist(b:VCSCommandStatusText, '(\(\d\+\) : \w\+)')
if len(matched) | let option = matched[1] | endif
endif
endif
endif
elseif v:count
if b:VCSCommandVCSType ==# 'SVN'
let limit_option = '-l'
elseif b:VCSCommandVCSType ==# 'git'
let limit_option = '-n'
elseif b:VCSCommandVCSType ==# 'HG'
let limit_option = '-l'
endif
let option = limit_option . ' ' . v:count
endif
endif
if exists('option')
call s:vcscommand_exec('VCSLog', option)
endif
endfunction "}}}
function! s:vcscommand_filetype(filetype, command) " {{{3
let given_count1 = v:count1
let revision = s:vcscommand_get_revision_on_cursor_line(a:filetype)
if strlen(revision)
let option = s:vcscommand_make_vcs_option(a:command, revision, given_count1)
call s:vcscommand_exec(a:command, option)
endif
endfunction "}}}
function! s:vcscommand_get_revision_on_cursor_line(filetype) " {{{3
let save_cursor = getpos('.')
let save_yank_register = getreg('"')
if a:filetype ==# 'log'
if &filetype ==# 'svnlog'
normal! j
?^r\d\+\ |
normal! 0lye
elseif &filetype ==# 'gitlog'
normal! j
?^commit\ \w\+$
normal! 0wy7l
elseif b:VCSCommandVCSType ==# 'HG'
normal! j
?^changeset:\ \+\d\+:\w\+$
normal! Wyw
endif
elseif a:filetype ==# 'annotate'
if b:VCSCommandVCSType ==# 'SVN'
normal! 0wye
elseif b:VCSCommandVCSType ==# 'git'
normal! 0t yb
elseif b:VCSCommandVCSType ==# 'HG'
normal! 0f:yb
endif
endif
let revision = @"
call setpos('.', save_cursor)
call setreg('"', save_yank_register)
return revision
endfunction "}}}
function! s:vcscommand_make_vcs_option(command, revision, given_count1) " {{{3
let option = a:revision
if b:VCSCommandVCSType ==# 'SVN'
if a:command ==# 'VCSLog'
let older = a:given_count1 == 1 ? '' : a:given_count1 . ':'
let option = '-r ' . older . a:revision
elseif a:command ==# 'VCSInfo'
let option = '-r ' . a:revision
elseif a:command =~# 'VCSDiff\|VCSVimDiff'
let older = a:given_count1 == 1 ? str2nr(a:revision) - 1 : a:given_count1
let option = older . ' ' . a:revision
endif
elseif b:VCSCommandVCSType ==# 'git'
if a:command ==# 'VCSLog'
let option = '-n ' . a:given_count1 . ' ' . a:revision
elseif a:command =~# 'VCSDiff\|VCSVimDiff'
let older = a:revision . '~' . a:given_count1
let option = older . ' ' . a:revision
endif
elseif b:VCSCommandVCSType ==# 'HG'
if a:command ==# 'VCSLog'
let option = '-l ' . a:given_count1 . ' -r ' . a:revision
elseif a:command =~# 'VCSDiff\|VCSVimDiff'
let older = a:given_count1 == 1 ? str2nr(a:revision) - 1 : a:given_count1
"let option = '-r ' . older . ' -r ' . a:revision
let option = a:revision
endif
endif
return option
endfunction "}}}

おわりに

今回の Vim Advent Calendar では git に関する話題が多くて Subversion 使いとしては肩身が狭かったのですが、普段 Windows 端末の TortoiseSVN で行っているような履歴の追跡に近いことを vcscommand でも出来ますよ!ということを紹介させていただきました。

明日は @basyura さんです。楽しみですね。

0 件のコメント:

コメントを投稿