├── .gitignore ├── README.markdown ├── doc └── mercenary.txt ├── plugin └── mercenary.vim └── syntax └── mercenaryblame.vim /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | mercenary.vim 2 | ============ 3 | 4 | I'm not going to lie to you; mercenary.vim may very well be the worst 5 | Mercurial wrapper of all time. 6 | 7 | Show and Tell 8 | ------------- 9 | 10 | Here are some screenshots showing off some the mercenary's dirty work: 11 | 12 | `:HGblame` will annotate the current file with the blame. 13 | 14 | ![:HGblame](http://i.imgur.com/O7WUC.png) 15 | 16 | `:HGdiff {rev}` will diff the current file against the specified revision. 17 | 18 | ![:HGdiff](http://i.imgur.com/KRava.png) 19 | 20 | `:HGshow {rev}` will show the commit message and diff for the specified 21 | revision. 22 | 23 | ![:HGshow](http://i.imgur.com/x2RzL.png) 24 | 25 | 26 | `:HGcat {rev} {path}` will show the specified file at the specified revision. 27 | 28 | ![:HGcat](http://i.imgur.com/g8OpJ.png) 29 | 30 | Installation 31 | ------------ 32 | 33 | If you don't have a preferred installation method, I recommend 34 | installing [vundle](https://github.com/gmarik/vundle) and adding 35 | 36 | Bundle 'phleet/vim-mercenary' 37 | 38 | to your `.vimrc` then running `:BundleInstall`. 39 | 40 | If you prefer [pathogen.vim](https://github.com/tpope/vim-pathogen), after 41 | installing it, simply copy and paste: 42 | 43 | cd ~/.vim/bundle 44 | git clone git://github.com/phleet/vim-mercenary.git 45 | 46 | Once help tags have been generated, you can view the manual with 47 | `:help mercenary`. 48 | 49 | License 50 | ------- 51 | Mercenary is (c) Jamie Wong. 52 | 53 | Distributed under the same terms as Vim itself. See `:help license`. 54 | 55 | Heavily inspired by vim-fugitive by Tim Pope: 56 | https://github.com/tpope/vim-fugitive 57 | 58 | This started out as a fork of fugitive, but I eventually discovered that the 59 | differences between git and mercurial while minor in functionality are vast in 60 | implementation. So I started from scratch, using fugitive's code as a reference 61 | but re-implementing everything to be less of a monkey patch for mercurial. 62 | -------------------------------------------------------------------------------- /doc/mercenary.txt: -------------------------------------------------------------------------------- 1 | *mercenary.txt* A mercurial wrapper so awesome, you should give it money 2 | 3 | Author: Jamie Wong 4 | License: Same terms as Vim itself (see |license|) 5 | 6 | Heavily inspired by vim-fugitive: https://github.com/tpope/vim-fugitive 7 | 8 | INTRODUCTION *mercenary* 9 | 10 | When editing a file inside a mercurial repository, the following commands will 11 | be made available to you. 12 | 13 | COMMANDS *mercenary-commands* 14 | 15 | *mercenary-:HGblame* 16 | :HGblame Blame the current file, showing the annotations in 17 | a vertical split to the left. NOTE: if lines have 18 | been added or deleted since the last revision, the 19 | blame may not line up. This is a limitation of `hg 20 | blame`, which does not annotate working copy 21 | changes. 22 | 23 | Examples: 24 | 25 | :HGblame 26 | 27 | *mercenary-:HGshow* 28 | :HGshow {rev} Show the full commit message and diff for the 29 | specified revision. 30 | 31 | Examples: 32 | 33 | :HGshow 16132 34 | :HGshow de8c6fb78d6b 35 | :HGshow p1() 36 | 37 | *mercenary-:HGcat* 38 | :HGcat {rev} {path} Show the specified file at the specified revision. 39 | 40 | Examples: 41 | 42 | :HGcat 1 app.yaml 43 | :HGcat p1() app.yaml 44 | :HGcat 42cbe67706ac % 45 | 46 | *mercenary-:HGdiff* 47 | :HGdiff [rev] Open up the current file at the specified revision 48 | (p1() if not specified) in a vertical split, then 49 | colorize the diff between the files. 50 | 51 | In the case of a merge, both parents will be 52 | opened in vertical splits. 53 | 54 | Examples: 55 | 56 | :HGdiff 57 | :HGdiff .^2 58 | :HGdiff 42cbe67706ac 59 | 60 | ABOUT *mercenary-about* 61 | 62 | Grab the latest version or report a bug on GitHub: 63 | 64 | https://github.com/phleet/vim-mercenary 65 | 66 | vim:tw=78:et:ft=help:norl: 67 | -------------------------------------------------------------------------------- /plugin/mercenary.vim: -------------------------------------------------------------------------------- 1 | " mercenary.vim - A mercurial wrapper so awesome, you should give it money 2 | " Maintainer: Jamie Wong 3 | " Version: 0.1 4 | 5 | " TODO(jlfwong): 6 | " * Mappings in blame to open up :HGcat or :HGshow 7 | " * Syntax highlighting for :HGshow 8 | " * Powerline integration 9 | " * Better Autocompletion 10 | " * Error handling 11 | " * Make :HGcat {rev} % do something reasonable if you're already viewing 12 | " a file at a specifi revision 13 | 14 | if exists('g:loaded_mercenary') || &cp 15 | finish 16 | endif 17 | 18 | let g:loaded_mercenary = 1 19 | if !exists('g:mercenary_hg_executable') 20 | let g:mercenary_hg_executable = 'hg' 21 | endif 22 | 23 | " VimL Utilities {{{1 24 | 25 | function! s:clsinit(properties, cls) abort 26 | let proto_ref = {} 27 | for name in keys(a:cls) 28 | let proto_ref[name] = a:cls[name] 29 | endfor 30 | return extend(a:properties, proto_ref, "keep") 31 | endfunction 32 | 33 | function! s:shellslash(path) 34 | if exists('+shellslash') && !&shellslash 35 | return s:gsub(a:path,'\\','/') 36 | else 37 | return a:path 38 | endif 39 | endfunction 40 | 41 | function! s:sub(str,pat,rep) abort 42 | return substitute(a:str,'\v\C'.a:pat,a:rep,'') 43 | endfunction 44 | 45 | function! s:gsub(str,pat,rep) abort 46 | return substitute(a:str,'\v\C'.a:pat,a:rep,'g') 47 | endfunction 48 | 49 | function! s:shellesc(arg) abort 50 | if a:arg =~ '^[A-Za-z0-9_/.-]\+$' 51 | return a:arg 52 | elseif &shell =~# 'cmd' 53 | return '"'.s:gsub(s:gsub(a:arg, '"', '""'), '\%', '"%"').'"' 54 | else 55 | return shellescape(a:arg) 56 | endif 57 | endfunction 58 | 59 | function! s:warn(str) 60 | echohl WarningMsg 61 | echomsg a:str 62 | echohl None 63 | let v:warningmsg = a:str 64 | endfunction 65 | 66 | " }}1 67 | " Mercenary Utilities {{{1 68 | 69 | let s:mercenary_commands = [] 70 | function! s:add_command(definition) abort 71 | let s:mercenary_commands += [a:definition] 72 | endfunction 73 | 74 | function! s:extract_hg_root_dir(path) abort 75 | " Return the absolute path to the root directory of the hg repository, or an 76 | " empty string if the path is not inside an hg directory. 77 | 78 | " Handle mercenary:// paths as special cases 79 | if s:shellslash(a:path) =~# '^mercenary://.*//' 80 | return matchstr(s:shellslash(a:path), '\C^mercenary://\zs.\{-\}\ze//') 81 | endif 82 | 83 | " Expand to absolute path and strip trailing slashes 84 | let root = s:shellslash(simplify(fnamemodify(a:path, ':p:s?[\/]$??'))) 85 | let prev = '' 86 | 87 | while root !=# prev 88 | let dirpath = s:sub(root, '[\/]$', '') . '/.hg' 89 | let type = getftype(dirpath) 90 | if type != '' 91 | " File exists, stop here 92 | return root 93 | endif 94 | let prev = root 95 | 96 | " Move up a directory 97 | let root = fnamemodify(root, ':h') 98 | endwhile 99 | return '' 100 | endfunction 101 | 102 | function! s:gen_mercenary_path(method, ...) abort 103 | let merc_path = 'mercenary://' . s:repo().root_dir . '//' . a:method . ':' 104 | let merc_path .= join(a:000, '//') 105 | return merc_path 106 | endfunction 107 | 108 | " }}}1 109 | " Repo {{{1 110 | 111 | let s:repo_cache = {} 112 | function! s:repo(...) 113 | " Retrieves a Repo instance. If an argument is passed, it is interpreted as 114 | " the root directory of the hg repo (i.e. what `hg root` would output if run 115 | " anywhere inside the repository. Otherwise, the repository containing the 116 | " file in the current buffer is used. 117 | if !a:0 118 | return s:buffer().repo() 119 | endif 120 | 121 | let root_dir = a:1 122 | if !has_key(s:repo_cache, root_dir) 123 | let s:repo_cache[root_dir] = s:Repo.new(root_dir) 124 | endif 125 | 126 | return s:repo_cache[root_dir] 127 | endfunction 128 | 129 | let s:Repo = {} 130 | function! s:Repo.new(root_dir) dict abort 131 | let repo = { 132 | \"root_dir" : a:root_dir 133 | \} 134 | return s:clsinit(repo, self) 135 | endfunction 136 | 137 | function! s:Repo.hg_command(...) dict abort 138 | " Return a full hg command to be executed as a string. 139 | " 140 | " All arguments passed are translated into hg commandline arguments. 141 | let cmd = 'cd ' . self.root_dir 142 | " HGPLAIN is an environment variable that's supposed to override any settings 143 | " that will mess with the hg command 144 | let cmd .= ' && HGPLAIN=1 ' . g:mercenary_hg_executable 145 | let cmd .= ' ' . join(map(copy(a:000), 's:shellesc(v:val)'), ' ') 146 | return cmd 147 | endfunction 148 | 149 | " }}}1 150 | " Buffer {{{1 151 | 152 | let s:buffer_cache = {} 153 | function! s:buffer(...) 154 | " Retrieves a Buffer instance. If an argument is passed, it is interpreted as 155 | " the buffer number. Otherwise the buffer number of the active buffer is used. 156 | let bufnr = a:0 ? a:1 : bufnr('%') 157 | 158 | if !has_key(s:buffer_cache, bufnr) 159 | let s:buffer_cache[bufnr] = s:Buffer.new(bufnr) 160 | endif 161 | 162 | return s:buffer_cache[bufnr] 163 | endfunction 164 | 165 | let s:Buffer = {} 166 | function! s:Buffer.new(number) dict abort 167 | let buffer = { 168 | \"_number" : a:number 169 | \} 170 | return s:clsinit(buffer, self) 171 | endfunction 172 | 173 | function! s:Buffer.path() dict abort 174 | return fnamemodify(bufname(self.bufnr()), ":p") 175 | endfunction 176 | 177 | function! s:Buffer.relpath() dict abort 178 | return fnamemodify(self.path(), ':p') 179 | endfunction 180 | 181 | function! s:Buffer.bufnr() dict abort 182 | return self["_number"] 183 | endfunction 184 | 185 | function! s:Buffer.enable_mercenary_commands() dict abort 186 | " TODO(jlfwong): This is horribly wrong if the buffer isn't active 187 | for command in s:mercenary_commands 188 | exe 'command! -buffer '.command 189 | endfor 190 | endfunction 191 | 192 | " XXX(jlfwong) unused 193 | function! s:Buffer.getvar(var) dict abort 194 | return getbufvar(self.bufnr(), a:var) 195 | endfunction 196 | 197 | " XXX(jlfwong) unused 198 | function! s:Buffer.setvar(var, value) dict abort 199 | return setbufvar(self.bufnr(), a:var, a:value) 200 | endfunction 201 | 202 | function! s:Buffer.repo() dict abort 203 | return s:repo(s:extract_hg_root_dir(self.path())) 204 | endfunction 205 | 206 | function! s:Buffer.onwinleave(cmd) dict abort 207 | call setwinvar(bufwinnr(self.bufnr()), 'mercenary_bufwinleave', a:cmd) 208 | endfunction 209 | 210 | function! s:Buffer_winleave(bufnr) abort 211 | execute getwinvar(bufwinnr(a:bufnr), 'mercenary_bufwinleave') 212 | endfunction 213 | 214 | augroup mercenary_buffer 215 | autocmd! 216 | autocmd BufWinLeave * call s:Buffer_winleave(str2nr(expand(''))) 217 | augroup END 218 | 219 | " }}}1 220 | " :HGblame {{{1 221 | 222 | function! s:Blame() abort 223 | " TODO(jlfwong): hg blame doesn't list uncommitted changes, which can result 224 | " in misalignment if the file has been modified. Figure out a way to fix this. 225 | " 226 | " TODO(jlfwong): Considering switching this to use mercenary://blame 227 | 228 | let realpath = trim(system('realpath ' . s:buffer().path())) 229 | let hg_args = ['blame', '--changeset', '--number', '--user', '--date', '-q'] 230 | let hg_args += ['--', realpath] 231 | let hg_blame_command = call(s:repo().hg_command, hg_args, s:repo()) 232 | 233 | let temppath = resolve(tempname()) 234 | let outfile = temppath . '.mercenaryblame' 235 | let errfile = temppath . '.err' 236 | 237 | " Write the blame output to a .mercenaryblame file in a temp folder somewhere 238 | silent! execute '!' . hg_blame_command . ' > ' . outfile . ' 2> ' . errfile 239 | 240 | " Remember the bufnr that :HGblame was invoked in 241 | let source_bufnr = s:buffer().bufnr() 242 | 243 | " Save the settings in the main buffer to be overridden so they can be 244 | " restored when the buffer is closed 245 | let restore = 'call setwinvar(bufwinnr(' . source_bufnr . '), "&scrollbind", 0)' 246 | if &l:wrap 247 | let restore .= '|call setwinvar(bufwinnr(' . source_bufnr . '), "&wrap", 1)' 248 | endif 249 | if &l:foldenable 250 | let restore .= '|call setwinvar(bufwinnr(' . source_bufnr . '), "&foldenable", 1)' 251 | endif 252 | 253 | " Line number of the first visible line in the window + &scrolloff 254 | let top = line('w0') + &scrolloff 255 | " Line number of the cursor 256 | let current = line('.') 257 | 258 | setlocal scrollbind nowrap nofoldenable 259 | exe 'keepalt leftabove vsplit ' . outfile 260 | setlocal nomodified nomodifiable nonumber scrollbind nowrap foldcolumn=0 nofoldenable filetype=mercenaryblame 261 | 262 | " When the current buffer containing the blame leaves the window, restore the 263 | " settings on the source window. 264 | call s:buffer().onwinleave(restore) 265 | 266 | " Synchronize the window position and cursor position between the blame buffer 267 | " and the code buffer. 268 | " Execute the line number as a command, focusing on that line (e.g. :23) to 269 | " synchronize the buffer scroll positions. 270 | execute top 271 | normal! zt 272 | " Synchronize the cursor position. 273 | execute current 274 | syncbind 275 | 276 | " Resize the window so we show all of the blame information, but none of the 277 | " code (the code is shown in the editing buffer :HGblame was invoked in). 278 | let blame_column_count = strlen(matchstr(getline('.'), '[^:]*:')) - 1 279 | execute "vertical resize " . blame_column_count 280 | 281 | " TODO(jlfwong): Maybe use winfixwidth to stop resizing of the blame window 282 | endfunction 283 | 284 | call s:add_command("HGblame call s:Blame()") 285 | 286 | augroup mercenary_blame 287 | autocmd! 288 | autocmd BufReadPost *.mercenaryblame setfiletype mercenaryblame 289 | augroup END 290 | 291 | " }}}1 292 | " Initialization and Routing {{{1 293 | 294 | let s:method_handlers = {} 295 | 296 | function! s:route(path) abort 297 | let hg_root_dir = s:extract_hg_root_dir(a:path) 298 | if hg_root_dir == '' 299 | return 300 | endif 301 | 302 | let mercenary_spec = matchstr(s:shellslash(a:path), '\C^mercenary://.\{-\}//\zs.*') 303 | 304 | if mercenary_spec != '' 305 | " Route the mercenary:// path 306 | let method = matchstr(mercenary_spec, '\C.\{-\}\ze:') 307 | 308 | " Arguments to the mercenary:// methods are delimited by // 309 | let args = split(matchstr(mercenary_spec, '\C:\zs.*'), '//') 310 | 311 | try 312 | if has_key(s:method_handlers, method) 313 | call call(s:method_handlers[method], args, s:method_handlers) 314 | else 315 | call s:warn('mercenary: unknown mercenary:// method ' . method) 316 | endif 317 | catch /^Vim\%((\a\+)\)\=:E118/ 318 | call s:warn("mercenary: Too many arguments to mercenary://" . method) 319 | catch /^Vim\%((\a\+)\)\=:E119/ 320 | call s:warn("mercenary: Not enough argument to mercenary://" . method) 321 | endtry 322 | end 323 | 324 | call s:buffer().enable_mercenary_commands() 325 | endfunction 326 | 327 | augroup mercenary 328 | autocmd! 329 | autocmd BufNewFile,BufReadPost * call s:route(expand(':p')) 330 | augroup END 331 | 332 | " }}}1 333 | " :HGcat {{{1 334 | 335 | function! s:Cat(rev, path) abort 336 | execute 'edit ' . s:gen_mercenary_path('cat', a:rev, fnamemodify(a:path, ':p')) 337 | endfunction 338 | 339 | call s:add_command("-nargs=+ -complete=file HGcat call s:Cat()") 340 | 341 | " }}}1 342 | " mercenary://root_dir//cat:rev//filepath {{{1 343 | 344 | function! s:method_handlers.cat(rev, filepath) dict abort 345 | " TODO(jlfwong): Error handling - (file not found, rev not fond) 346 | 347 | let args = ['cat', '--rev', a:rev, a:filepath] 348 | let hg_cat_command = call(s:repo().hg_command, args, s:repo()) 349 | 350 | let temppath = resolve(tempname()) 351 | let outfile = temppath . '.out' 352 | let errfile = temppath . '.err' 353 | 354 | silent! execute '!' . hg_cat_command . ' > ' . outfile . ' 2> ' . errfile 355 | 356 | silent! execute 'read ' . outfile 357 | " :read dumps the output below the current line - so delete the first line 358 | " (which will be empty) 359 | 0d 360 | 361 | setlocal nomodified nomodifiable readonly 362 | 363 | if &bufhidden ==# '' 364 | " Delete the buffer when it becomes hidden 365 | setlocal bufhidden=delete 366 | endif 367 | endfunction 368 | 369 | " }}}1 370 | " :HGshow {{{1 371 | 372 | function! s:Show(rev) abort 373 | execute 'edit ' . s:gen_mercenary_path('show', a:rev) 374 | endfunction 375 | 376 | call s:add_command("-nargs=1 HGshow call s:Show()") 377 | 378 | 379 | " }}}1 380 | " mercenary://root_dir//show:rev {{{1 381 | 382 | function! s:method_handlers.show(rev) dict abort 383 | " TODO(jlfwong): DRY this up w/ method_handlers.cat 384 | 385 | let args = ['log', '--stat', '-vpr', a:rev] 386 | let hg_log_command = call(s:repo().hg_command, args, s:repo()) 387 | 388 | let temppath = resolve(tempname()) 389 | let outfile = temppath . '.out' 390 | let errfile = temppath . '.err' 391 | 392 | silent! execute '!' . hg_log_command . ' > ' . outfile . ' 2> ' . errfile 393 | 394 | silent! execute 'read ' . outfile 395 | 0d 396 | 397 | setlocal nomodified nomodifiable readonly 398 | setlocal filetype=diff 399 | 400 | if &bufhidden ==# '' 401 | " Delete the buffer when it becomes hidden 402 | setlocal bufhidden=delete 403 | endif 404 | endfunction 405 | 406 | " }}}1 407 | " :HGdiff {{{1 408 | 409 | function! s:Diff(...) abort 410 | if a:0 == 0 411 | let merc_p1_path = s:gen_mercenary_path('cat', 'p1()', s:buffer().relpath()) 412 | 413 | silent! execute 'keepalt leftabove vsplit ' . merc_p1_path 414 | diffthis 415 | wincmd p 416 | 417 | let hg_parent_check_log_cmd = s:repo().hg_command('log', '--rev', 'p2()') 418 | 419 | if system(hg_parent_check_log_cmd) != '' 420 | let merc_p2_path = s:gen_mercenary_path('cat', 'p2()', s:buffer().relpath()) 421 | silent! execute 'keepalt rightbelow vsplit ' . merc_p2_path 422 | diffthis 423 | wincmd p 424 | endif 425 | 426 | diffthis 427 | elseif a:0 == 1 428 | let rev = a:1 429 | 430 | let merc_path = s:gen_mercenary_path('cat', rev, s:buffer().relpath()) 431 | 432 | silent! execute 'keepalt leftabove vsplit ' . merc_path 433 | diffthis 434 | wincmd p 435 | 436 | diffthis 437 | endif 438 | endfunction 439 | 440 | call s:add_command("-nargs=? HGdiff call s:Diff()") 441 | 442 | " }}}1 443 | -------------------------------------------------------------------------------- /syntax/mercenaryblame.vim: -------------------------------------------------------------------------------- 1 | syn match MercenaryblameBoundary "^\^" 2 | syn match MercenaryblameBlank "^\s\+\s\@=" nextgroup=MercenaryblameAuthor skipwhite 3 | syn match MercenaryblameAuthor "\w\+" nextgroup=MercenaryblameNumber skipwhite 4 | syn match MercenaryblameNumber "\d\+" nextgroup=MercenaryblameChangeset skipwhite 5 | syn match MercenaryblameChangeset "[a-f0-9]\{12\}" nextgroup=MercenaryblameDate skipwhite 6 | syn match MercenaryblameDate "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}" 7 | hi def link MercenaryblameAuthor Keyword 8 | hi def link MercenaryblameNumber Number 9 | hi def link MercenaryblameChangeset Identifier 10 | hi def link MercenaryblameDate PreProc 11 | --------------------------------------------------------------------------------