Commits

ZyX_I committed bc58743

@aurum/drivers/**: Moved these to @%aurum/drivers/**

  • Participants
  • Parent commits d863a06
  • Branches movetoautoload

Comments (0)

Files changed (15)

autoload/aurum/drivers/common/hypsites.vim

+"▶1
+scriptencoding utf-8
+execute frawor#Setup('0.1', {'@/resources': '0.0',})
+"▶1 s:hypsites
+let s:dport='domain.(empty(port)?"":":".port)'
+let s:link='shellescape("http://".'.s:dport.'.path)'
+" TODO cache
+let s:dl=    '(executable("curl")?'.
+            \   '(system("curl -L ".'.s:link.')):'.
+            \'(executable("wget")?'.
+            \   '(system("wget -O- ".'.s:link.'))'.
+            \':'.
+            \   '(0)))'
+unlet s:link
+let s:bbdict={
+\       'html': '"https://".domain.path."/src/".hex."/".file',      'hline': '"cl-".line',
+\        'raw': '"https://".domain.path."/raw/".hex."/".file',
+\   'annotate': '"https://".domain.path."/annotate/".hex."/".file', 'aline': '"line-".line',
+\   'filehist': '"https://".domain.path."/history/".file',
+\     'bundle': '"https://".domain.path."/get/".hex.".tar.bz2"',
+\  'changeset': '"https://".domain.path."/changeset/".hex',
+\        'log': '"https://".domain.path."/changesets"',
+\      'clone': '"https://".domain.path',
+\       'push': '"ssh://hg@".domain.path',
+\}
+let s:hyp={}
+let s:gcproj='matchstr(domain, "\\v^[^.]+")'
+"▶1 mercurial
+"  https://bitbucket.org/ZyX_I/aurum / ssh://hg@bitbucket.org/ZyX_I/aurum
+"  ssh://zyxsf@translit3.hg.sourceforge.net/hgroot/translit3/translit3 /
+"       http://translit3.hg.sourceforge.net:8000/hgroot/translit3/translit3
+"  https://vim-pyinteractive-plugin.googlecode.com/hg/
+"  http://hg.assembla.com/CMakeLua
+"  https://zyx@zyx.codebasehq.com/test/test.hg /
+"       ssh://hg@codebasehq.com/zyx/test/test.hg
+"  https://hg01.codeplex.com/visualhg
+"  http://mercurial.intuxication.org/hg/tryton-client_ru
+"  https://mirrors.kilnhg.com/Repo/Mirrors/Hg/Mercurial
+"  http://hg.mozdev.org/maf/ / ssh://USER:PASS@hg.mozdev.org/maf
+"u https://projectkenai.com/hg/sonichg~test (rev numbers must match)
+"  https://hg.kenai.com/hg/sonichg~test / ssh://user@hg.kenai.com/sonichg~test
+"  http://hg.savannah.nongnu.org/hgweb/mechsys/
+"  https://sharesource.org/hg/alqua/
+"  http://mercurial.tuxfamily.org/mercurialroot/slitaz/tazlito/
+"t http://anonscm.debian.org/hg/minicom/
+" len("hgroot")=6
+let s:pkbase='"http://".matchstr(domain, ''\v[^.]+\.[^.]+$'')."/projects/".matchstr(path, ''\v.*\/\zs[^~]+'').'.
+            \                                                '"/sources/". matchstr(path, "\\v[^~]+$")'
+let s:cpbase='"http://".path[1:].".codeplex.com/SourceControl'
+let s:cbbase='"https://".%s.".".domain."/projects/".%s."/repositories/".%s'
+let s:cbssh=printf(s:cbbase, 'matchstr(path, "\\v^[^/]+", 1)',
+            \                'matchstr(path, ''\v[^/]+%(\/[^/]+\/?$)'')',
+            \                'matchstr(path[:-4], "\\v[^/]+$")')
+let s:cbhttps=printf(s:cbbase, 'matchstr(domain, "\\v^[^.]+")',
+            \                  'matchstr(path, "\\v^[^/]+")',
+            \                  'matchstr(path[:-4], "\\v[^/]+$")')
+unlet s:cbbase
+let s:hgwebdict={
+\       'html': '"http://".'.s:dport.'.path."/file/".hex."/".file',     'hline': '"l".line',
+\        'raw': '"http://".'.s:dport.'.path."/raw-file/".hex."/".file',
+\   'annotate': '"http://".'.s:dport.'.path."/annotate/".hex."/".file', 'aline': '"l".line',
+\   'filehist': '"http://".'.s:dport.'.path."/log/".hex."/".file',
+\  'changeset': '"http://".'.s:dport.'.path."/rev/".hex',
+\        'log': '"http://".'.s:dport.'.path."/graph"',
+\      'clone': '"http://".'.s:dport.'.path',
+\}
+let s:hyp.mercurial=[
+\['domain is? "bitbucket.org"', s:bbdict],
+\['domain =~? "\\Vhg.sourceforge.net\\$"',
+\ {     'html': '"http://".domain."/hgweb".path[7:]."/file/".hex."/".file',     'hline': '"l".line',
+\        'raw': '"http://".domain."/hgweb".path[7:]."/raw-file/".hex."/".file',
+\   'annotate': '"http://".domain."/hgweb".path[7:]."/annotate/".hex."/".file', 'aline': '"l".line',
+\   'filehist': '"http://".domain."/hgweb".path[7:]."/log/".hex."/".file',
+\  'changeset': '"http://".domain."/hgweb".path[7:]."/rev/".hex',
+\        'log': '"http://".domain."/hgweb".path[7:]."/graph"',
+\      'clone': '"http://".domain.":8000".path',
+\       'push': '"ssh://".user."@".domain.path',}],
+\['domain =~? "\\Vgooglecode.com\\$" && path[:2] is? "/hg"',
+\ {     'html': '"http://code.google.com/p/".'.s:gcproj.'."/source/browse/".file."?r=".hex', 'hline': 'line',
+\        'raw': '"http://".domain."/hg-history/".hex."/".file',
+\   'filehist': '"http://code.google.com/p/".'.s:gcproj.'."/source/list?path=/".file."&r=".hex',
+\  'changeset': '"http://code.google.com/p/".'.s:gcproj.'."/source/detail?r=".hex',
+\        'log': '"http://code.google.com/p/".'.s:gcproj.'."/source/list"',
+\      'clone': 'url',
+\       'push': 'url',}],
+\['domain is? "hg.assembla.com"',
+\ {     'html': '"http://trac-".domain.path."/browser/".file."?rev=".hex',                'hline': '"L".line',
+\   'annotate': '"http://trac-".domain.path."/browser/".file."?annotate=blame&rev=".hex', 'aline': '"L".line',
+\   'filehist': '"http://trac-".domain.path."/log/".file."?rev=".hex',
+\  'changeset': '"http://trac-".domain.path."/changeset/".hex',
+\        'log': '"http://trac-".domain.path."/log"',
+\      'clone': '"http://".domain.path',}],
+\['domain is? "codebasehq.com" && path[-3:] is? ".hg"',
+\ {     'html': s:cbssh.'."/blob/".hex."/".file', 'hline': '"L".line',
+\        'raw': s:cbssh.'."/raw/".hex."/".file',
+\   'annotate': s:cbssh.'."/blame/".hex."/".file',
+\   'filehist': s:cbssh.'."/commits/".hex."/".file',
+\     'bundle': s:cbssh.'."/archive/zip/".hex',
+\  'changeset': s:cbssh.'."/commit/".hex',
+\        'log': s:cbssh.'."/commits/tip"',
+\      'clone': '"https://".matchstr(path, "\\v^[^/]+", 1).".".domain.matchstr(path, ''\v[^/]+\/[^/]+$'')',
+\       'push': '"ssh://hg@".domain.path',}],
+\['domain =~? "\\Vcodebasehq.com\\$" && path[-3:] is? ".hg"',
+\ {     'html': s:cbhttps.'."/blob/".hex."/".file', 'hline': '"L".line',
+\        'raw': s:cbhttps.'."/raw/".hex."/".file',
+\   'annotate': s:cbhttps.'."/blame/".hex."/".file',
+\   'filehist': s:cbhttps.'."/commits/".hex."/".file',
+\     'bundle': s:cbhttps.'."/archive/zip/".hex',
+\  'changeset': s:cbhttps.'."/commit/".hex',
+\        'log': s:cbhttps.'."/commits/tip"',
+\      'clone': '"https://".domain.path',
+\       'push': '"ssh://hg@".matchstr(domain, ''\v\.@<=.*$'')."/".matchstr(domain, "\\v^[^.]+").path',}],
+\['domain =~? "\\V\\^hg\\d\\+.codeplex.com\\$"',
+\ {     'html': s:cpbase.'"/changeset/view/".hex[:11]."#".substitute(file, "/", "%2f", "g")',
+\     'bundle': '"http://download.codeplex.com/Download/SourceControlFileDownload.ashx'.
+\                       '?ProjectName=".path[1:]."&changeSetId=".hex[:11]',
+\  'changeset': s:cpbase.'"/changeset/changes/".hex[:11]',
+\        'log': s:cpbase.'"/list/changesets"',
+\      'clone': '"https://".domain.path',
+\       'push': '"https://".domain.path',}],
+\['domain =~? "\\Vkilnhg.com\\$"',
+\ {     'html': '"https://".domain.path."/File/".file."?rev=".hex',               'hline': 'line',
+\        'raw': '"https://".domain.path."/FileDownload/".file."?rev=".hex',
+\   'annotate': '"https://".domain.path."/File/".file."?rev=".hex&view=annotate', 'aline': 'line',
+\   'filehist': '"https://".domain.path."/FileHistory/".file."?rev=".hex',
+\  'changeset': '"https://".domain.path."/History/".hex',
+\        'log': '"https://".domain.path',
+\      'clone': '"https://".domain.path',}],
+\['domain =~? ''\V\%(project\)\?kenai.com\$'' && (path[:2] is? "/hg" || domain[:2] is? "hg.")',
+\ {     'html': s:pkbase.'."/content/".file."?rev=".repo.functions.getcsprop(repo, hex, "rev")',
+\        'raw': s:pkbase.'."/content/".file."?raw=true&rev=".repo.functions.getcsprop(repo, hex, "rev")',
+\   'filehist': s:pkbase.'."/history/".file',
+\  'changeset': s:pkbase.'."/revision/".repo.functions.getcsprop(repo, hex, "rev")',
+\        'log': s:pkbase.'."/history"',
+\      'clone': '"https://".domain."/hg/".matchstr(path, "\\v[^/]+$")',
+\       'push': '"ssh://".domain."/".matchstr(path, "\\v[^/]+$")',}],
+\['domain is? "sharesource.org" && path[:2] is? "/hg"',
+\ map(copy(s:hgwebdict), 'substitute(v:val, "http", "https", "")')],
+\[ 'domain =~? ''\v^%(mercurial\.%(intuxication|tuxfamily)|hg\.mozdev|hg\.savannah\.%(non)?gnu)\.org$'' || '.
+\ '(domain is? "anonscm.debian.org" && path[:2] is? "/hg") || '.
+\ '('.s:dl.'=~#''\V<link rel="icon" href="\[^"]\*static/hgicon.png" type="image/png" />'')',
+\ s:hgwebdict],
+\]
+unlet s:hgwebdict s:pkbase s:cpbase s:cbssh s:cbhttps
+"▶1 git
+"  ssh://git@github.com:MarcWeber/vim-addon-manager / git://github.com/MarcWeber/vim-addon-manager
+"  git://vimpluginloader.git.sourceforge.net/gitroot/vimpluginloader/vam-test-repository
+"       / ssh://zyxsf@vimpluginloader.git.sourceforge.net/gitroot/vimpluginloader/vam-test-repository
+"  git://repo.or.cz/test2.git / http://repo.or.cz/r/test2.git /
+"       ssh://repo.or.cz/srv/git/test2.git
+"  git://gitorious.org/test4/test.git / https://git.gitorious.org/test4/test.git
+"       / ssh://git@gitorious.org:test4/test.git
+"  git://git.kitenet.net/mr.git / http://git.kitenet.net/git/mr.git
+"       / ssh://git.kitenet.net/srv/git/mr.git
+"  (unable to clone with hg-git) https://code.google.com/p/tortoisegit/
+let s:ghpath='substitute(path, "\\v^[:/]|\\.git$", "", "g")'
+let s:roproj='matchstr(path, ''\v\/@<=[^/]{-1,}%(%(\.git)?\/*$)@='').".git"'
+let s:robase='"http://".domain."/w/".'.s:roproj
+let s:godomain='substitute(domain, "^git\\.", "", "")'
+let s:gobase='"http://".'.s:godomain.'."/".'.s:ghpath
+let s:hyp.git=[
+\['domain is? "bitbucket.org"', s:bbdict],
+\['domain is? "github.com"',
+\ {     'html': '"https://".domain."/".'.s:ghpath.'."/blob/".hex."/".file',   'hline': '"L".line',
+\                                                                             'hlines': '"L".line1."-L".line2',
+\        'raw': '"https://".domain."/".'.s:ghpath.'."/raw/". hex."/".file',
+\   'annotate': '"https://".domain."/".'.s:ghpath.'."/blame/". hex."/".file', 'aline': '"LID".line',
+\   'filehist': '"https://".domain."/".'.s:ghpath.'."/commits/".hex."/".file',
+\     'bundle': '"https://".domain."/".'.s:ghpath.'."/zipball/".hex',
+\  'changeset': '"https://".domain."/".'.s:ghpath.'."/commit/".hex',
+\        'log': '"https://".domain."/".'.s:ghpath.'."/commits"',
+\      'clone': '"git://".domain."/".'.s:ghpath,
+\       'push': '"ssh://git@".domain.":".'.s:ghpath.'.".git"',}],
+\['domain =~? "\\Vgit.sourceforge.net\\$"',
+\ {     'html': '"http://".domain."/git/gitweb.cgi?p=".path[9:].";a=blob;hb=".hex.";f=".file', 'hline': '"l".line',
+\        'raw': '"http://".domain."/git/gitweb.cgi?p=".path[9:].";a=blob_plain;hb=".hex.";f=".file',
+\   'filehist': '"http://".domain."/git/gitweb.cgi?p=".path[9:].";a=history;hb=".hex.";f=".file',
+\  'changeset': '"http://".domain."/git/gitweb.cgi?p=".path[9:].";a=commitdiff;hb=".hex',
+\        'log': '"http://".domain."/git/gitweb.cgi?p=".path[9:].";a=log"',
+\      'clone': '"http://".domain.":8000".path',
+\       'push': '"ssh://".user."@".domain.path',}],
+\['domain is? "code.google.com"',
+\ {     'html': '"http://code.google.com/".substitute(path, "/$", "", "")."/source/browse/".file."?r=".hex',}],
+\['domain =~? ''\v^%(git\.)?gitorious\.org$''',
+\ {     'html': s:gobase.'."/blobs/".hex."/".file',       'hline': '"line".line',
+\        'raw': s:gobase.'."/blobs/raw/".hex."/".file',
+\   'annotate': s:gobase.'."/blobs/blame/".hex."/".file', 'aline': '"line".line',
+\   'filehist': s:gobase.'."/blobs/history/".hex."/".file',
+\  'changeset': s:gobase.'."/commit/".hex',
+\        'log': s:gobase.'."/commits/".hex',
+\      'clone': '"git://".'.s:godomain.'."/".'.s:ghpath,
+\       'push': '"ssh://git@".'.s:godomain.'.":".'.s:ghpath.'.".git"',}],
+\['domain is? "repo.or.cz"',
+\ {     'html': s:robase.'."/blob/".hex.":/".file',       'hline': '"l".line',
+\        'raw': s:robase.'."/blob_plain/".hex.":/".file',
+\   'annotate': s:robase.'."/blame/".hex.":/".file',      'aline': '"l".line',
+\   'filehist': s:robase.'."/history/".hex.":/".file',
+\  'changeset': s:robase.'."/commit/".hex',
+\        'log': s:robase.'."/log/".hex',
+\      'clone': '"git://".domain."/".'.s:roproj,
+\       'push': '"ssh://".domain."/srv/git/".'.s:roproj,}],
+\['domain =~? "\\Vgit.kitenet.net\\$"',
+\ {     'html': '"http://".domain."/?p=".'.s:roproj.'.";a=blob;hb=".hex.";f=".file', 'hline': '"l".line',
+\        'raw': '"http://".domain."/?p=".'.s:roproj.'.";a=blob_plain;hb=".hex.";f=".file',
+\   'filehist': '"http://".domain."/?p=".'.s:roproj.'.";a=history;hb=".hex.";f=".file',
+\  'changeset': '"http://".domain."/?p=".'.s:roproj.'.";a=commitdiff;hb=".hex',
+\        'log': '"http://".domain."/?p=".'.s:roproj.'.";a=log"',
+\      'clone': '"git://".domain."/".'.s:roproj,
+\       'push': '"ssh://".domain."/srv/git/".'.s:roproj,}],
+\]
+unlet s:ghpath s:roproj s:robase s:godomain s:gobase
+"▶1 subversion
+"  https://vimpluginloader.svn.sourceforge.net/svnroot/vimpluginloader
+"  http://conque.googlecode.com/svn/trunk
+let s:svngcbase='"http://code.google.com/p/".'.s:gcproj
+let s:svngcfile='path[5:]."/".file'
+let s:hyp.svn=[
+\['domain =~? "\\Vsvn.sourceforge.net\\$"',
+\ {     'html': '"http://".domain."/viewvc".path[8:]."/".file."?view=markup&pathrev=".hex', 'hline': '"l".line',
+\        'raw': '"http://".domain."/viewvc".path[8:]."/".file."?pathrev=".hex',             'aline': '"l".line',
+\   'annotate': '"http://".domain."/viewvc".path[8:]."/".file."?annotate=".hex',
+\     'bundle': '"http://".domain."/viewvc".path[8:]."?view=tar&pathrev=".hex',
+\        'log': '"http://".domain."/viewvc".path[8:]."?view=log"',
+\      'clone': 'url',}],
+\['domain =~? "\\Vgooglecode.com\\$" && path[:3] is? "/svn"',
+\ {     'html': s:svngcbase.'."/source/browse/".'.s:svngcfile.'."?rev=".hex', 'hline': 'line',
+\        'raw': '"http://".domain."/svn-history/r".hex.'.s:svngcfile,
+\   'filehist': s:svngcbase.'."/source/list?path=/".'.s:svngcfile.'."&r=".hex',
+\        'log': s:svngcbase.'."/source/list"',
+\      'clone': 'url',}],
+\['domain is? "svn.gna.org',
+\ {     'html': '"http://".domain."/viewvcs".path[4:]."/".file."?view=markup&revision=".hex',   'hline': '"l".line',
+\        'raw': '"http://".domain."/viewvcs/*checkout*".path[4:]."/".file."?view=markup&revision=".hex',
+\   'annotate': '"http://".domain."/viewvcs".path[4:]."/".file."?annotate=".hex',               'aline': '"l".line',
+\   'filehist': '"http://".domain."/viewvcs".path[4:]."/".file."?view=log"',
+\        'log': '"http://".domain."/viewvcs".path[4:]."?view=log"',
+\      'clone': '"svn://".domain.path',
+\       'push': '"svn+ssh://".user."@".domain.path',}],
+\]
+unlet s:svngcbase s:svngcfile
+"▶1 post resource
+unlet s:gcproj s:dl s:bbdict s:dport
+call s:_f.postresource('hypsites', s:hyp)
+unlet s:hyp
+"▶1
+call frawor#Lockvar(s:, '_pluginloaded')
+" vim: ft=vim ts=4 sts=4 et fmr=▶,▲

autoload/aurum/drivers/common/utils.vim

+"▶1
+scriptencoding utf-8
+execute frawor#Setup('0.0', {'@/resources': '0.0',
+            \                       '@/os': '0.2'})
+let s:utils={}
+"▶1 utils.getcmd :: cmd, args, kwargs, esc → sh
+function s:utils.getcmd(cmd, args, kwargs, esc)
+    let cmd=a:cmd
+    if !empty(a:kwargs)
+        let cmd.=' '.join(map(filter(items(a:kwargs), 'v:val[1] isnot 0'),
+                \             '((v:val[1] is 1)?'.
+                \               '(repeat("-", 1+(len(v:val[0])>1)).v:val[0]):'.
+                \               '(repeat("-", 1+(len(v:val[0])>1)).v:val[0].'.
+                \                              '" ="[len(v:val[0])>1].'.
+                \                              'shellescape(v:val[1],a:esc)))'))
+    endif
+    if !empty(a:args)
+        let cmd.=' '.join(map(copy(a:args), 'shellescape(v:val, a:esc)'))
+    endif
+    return cmd
+endfunction
+"▶1 utils.run :: sh, hasnulls::0|1|2 → [String] + shell
+function s:utils.run(cmd, hasnulls, cdpath)
+    if a:hasnulls==2 && !empty(&shellredir)
+        return call(s:_r.os.readsystem, [a:cmd]+(empty(a:cdpath)?
+                    \                               ([]):
+                    \                               ([a:cdpath])), {})
+    elseif a:hasnulls
+        let savedlazyredraw=&lazyredraw
+        let savedeventignore=&eventignore
+        set eventignore=all
+        set lazyredraw
+        try
+            try
+                tabnew
+            catch /^Vim(tabnew):E523:/
+                let r=s:utils.run(a:cmd, 2, a:cdpath)
+                if empty(r[-1])
+                    call remove(r, -1)
+                endif
+                return r
+            endtry
+            setlocal buftype=nofile modifiable noreadonly
+            if !empty(a:cdpath)
+                execute 'lcd' fnameescape(a:cdpath)
+            endif
+            " XXX this is not able to distinguish between output with and 
+            " without trailing newline, and also is “smart” about lineendings
+            silent execute '%!'.a:cmd
+            let r=getline(1, '$')
+            bwipeout!
+        finally
+            let &lazyredraw=savedlazyredraw
+            let &eventignore=savedeventignore
+        endtry
+    else
+        let cmd=a:cmd
+        if !empty(a:cdpath)
+            let cmd='cd '.shellescape(a:cdpath).' && '.cmd
+        endif
+        let r=split(system(cmd), "\n", 1)
+    endif
+    return r
+endfunction
+"▶1 utils.printm :: sh, hasnulls::Bool → + :echom, shell
+function s:utils.printm(m)
+    let prevempty=0
+    for line in a:m
+        if empty(line)
+            let prevempty+=1
+        else
+            if prevempty
+                while prevempty
+                    echom ' '
+                    let prevempty-=1
+                endwhile
+            endif
+            echom line
+        endif
+    endfor
+endfunction
+"▶1 utils.diffopts :: opts, opts, difftrans → diffopts
+function s:utils.diffopts(opts, defaultdiffopts, difftrans)
+    let opts=extend(copy(a:defaultdiffopts), a:opts)
+    let r={}
+    call map(filter(copy(a:difftrans), 'has_key(opts, v:key)'),
+            \'extend(r, {v:val : opts[v:key]})')
+    if has_key(opts, 'dates') && has_key(a:difftrans, 'dates')
+        let r[a:difftrans.dates]=!opts.dates
+    endif
+    return r
+endfunction
+"▶1 utils.addfiles :: repo, files + status → + add, forget
+function s:utils.addfiles(repo, files)
+    let status=a:repo.functions.status(a:repo, 0, 0, a:files)
+    for file in status.unknown
+        call a:repo.functions.add(a:repo, file)
+    endfor
+    for file in status.deleted
+        call a:repo.functions.forget(a:repo, file)
+    endfor
+endfunction
+"▶1 utils.usefile :: repo, message, kw, kw, func, args, kwargs, emes
+function s:utils.usefile(repo, message, kwfile, kwmes, Func, args, kwargs, ...)
+    if a:message=~#'\v[\r\n]'
+        let tmpfile=tempname()
+        call writefile(split(a:message, "\n", 1), tmpfile, 'b')
+        let a:kwargs[a:kwfile]=tmpfile
+        let usingfile=1
+    else
+        let a:kwargs[a:kwmes]=a:message
+        let usingfile=0
+    endif
+    try
+        return call(a:Func, [a:repo, 'commit', a:args, a:kwargs]+a:000, {})
+    finally
+        if usingfile && filereadable(tmpfile)
+            call delete(tmpfile)
+        endif
+    endtry
+endfunction
+"▶1 post resource
+call s:_f.postresource('utils', s:utils)
+"▶1
+call frawor#Lockvar(s:, '_pluginloaded')
+" vim: ft=vim ts=4 sts=4 et fmr=▶,▲

autoload/aurum/drivers/common/xml.vim

+"▶1
+scriptencoding utf-8
+execute frawor#Setup('0.0', {'@/resources': '0.0',})
+let s:xml={}
+"▶1 messages
+let s:_messages={
+            \ 'allblanks': 'No non-blank characters found',
+            \'tstartnfnd': 'Expected tag start (“<tagname”), but got “%s”',
+            \  'tendnfnd': 'Expected tag end (“>”), but got “%s”',
+            \  'ctagnfnd': 'Expected “%s”, but got “%s”',
+            \     'ctend': 'Expected “%s”, but got nothing',
+            \     'ttend': 'Expected tag end, but got nothing',
+        \}
+"▶1 new :: [String] → xml
+function s:F.new(lines)
+    let xml=deepcopy(s:xml)
+    let xml.lines=copy(a:lines)
+    let xml.tags=[]
+    return xml
+endfunction
+"▶1 skipws :: &
+function s:xml.skipws()
+    try
+        while self.lines[0]!~#'\S'
+            call remove(self.lines, 0)
+        endwhile
+        let self.lines[0]=substitute(self.lines[0], '\v^\s+', '', '')
+    catch /\m^Vim(\l*):E684:/
+        call s:_f.throw('allblanks')
+    endtry
+endfunction
+"▶1 decodeentities :: String → String
+let s:entities={
+            \  'lt': '<',
+            \  'gt': '>',
+            \ 'amp': '&',
+            \'quot': '"',
+        \}
+function s:F.decodeentities(s)
+    return substitute(a:s, '\v\&([lg]t|amp)\;', '\=s:entities[submatch(1)]','g')
+endfunction
+"▶1 skip :: &(len)
+function s:xml.skip(len)
+    let self.lines[0]=self.lines[0][(a:len):]
+    return self.skipws()
+endfunction
+"▶1 parsetag :: & → (tagname, attributes)
+" Assumes that tag names have only alphabetic characters
+function s:xml.parsetag()
+    call self.skipws()
+    let m=matchlist(self.lines[0], '\v\<(\a+)')[:1]
+    if empty(m)
+        call s:_f.throw('tstartnfnd', self.lines[0])
+    endif
+    let [match, tagname]=m
+    let self.tags+=[tagname]
+    call self.skip(len(match))
+    let attributes={}
+    while 1
+        let m=matchlist(self.lines[0], '\v^([a-zA-Z0-9_\-]+)\=\"(.{-})\"')[:2]
+        if empty(m)
+            break
+        endif
+        let [match, attrname, attrval]=m
+        call self.skip(len(match))
+        let attributes[attrname]=s:F.decodeentities(attrval)
+    endwhile
+    if self.lines[0][0] isnot# '>'
+        call s:_f.throw('tendnfnd', self.lines[0])
+    endif
+    let self.lines[0]=self.lines[0][1:]
+    return [tagname, attributes]
+endfunction
+"▶1 checkctag :: & → Bool
+function s:xml.checkctag()
+    return (!empty(self.lines) && self.lines[0][:1] is# '</')
+endfunction
+"▶1 skipctag :: &
+function s:xml.skipctag()
+    let tagname=remove(self.tags, -1)
+    let e='</'.tagname.'>'
+    let le=len(e)
+    try
+        if self.lines[0][:(le)] isnot# e
+            call s:_f.throw('ctagnfnd', e, self.lines[0])
+        endif
+        let self.lines[0]=self.lines[0][(le):]
+    catch /\m^Vim(\l*):E684:/
+        call s:_f.throw('ctend', e)
+    endtry
+endfunction
+"▶1 parsetextintag :: & → text
+function s:xml.parsetextintag()
+    try
+        let text=[]
+        while 1
+            let idx=stridx(self.lines[0], '<')
+            if idx!=-1
+                break
+            endif
+            let text+=remove(self.lines, 0, 0)
+        endwhile
+        if idx
+            let text+=[self.lines[0][:(idx-1)]]
+            let self.lines[0]=self.lines[0][(idx):]
+        else
+            let text+=['']
+        endif
+        call map(text, 's:F.decodeentities(v:val)')
+        call self.skipctag()
+        return text
+    catch /\m^Vim(\l*):E684:/
+        call s:_f.throw('ttend')
+    endtry
+endfunction
+"▶1 post resource
+call s:_f.postresource('xml', {'new': s:F.new,
+            \       'decodeentities': s:F.decodeentities})
+"▶1
+call frawor#Lockvar(s:, '_pluginloaded')
+" vim: ft=vim ts=4 sts=4 et fmr=▶,▲

autoload/aurum/drivers/git.vim

+"▶1
+scriptencoding utf-8
+execute frawor#Setup('0.1', {'@%aurum/drivers/common/hypsites': '0.0',
+            \                                   '@%aurum/repo': '5.0',
+            \                   '@%aurum/drivers/common/utils': '0.0',
+            \                                           '@/os': '0.1',
+            \                                      '@/options': '0.0',})
+let s:_messages={
+            \   'hexf': 'Failed to obtain hex string for revision %s '.
+            \           'in the repository %s: %s',
+            \   'logf': 'Failed to list all revisions in the repository %s: %s',
+            \  'rlogf': 'Failed to list revisions %s..%s '.
+            \           'in the repository %s: %s',
+            \    'csf': 'Failed to obtain information about revision %s '.
+            \           'in the repository %s: %s',
+            \    'cif': 'Failed to commit changes to the repository %s: %s',
+            \   'updf': 'Failed to checkout commit %s in the repository %s: %s',
+            \    'mvf': 'Failed to move file %s to %s in the repository %s: %s',
+            \    'rmf': 'Failed to remove file %s in the repository %s: %s',
+            \    'fgf': 'Failed to forget file %s in the repository %s: %s',
+            \  'filef': 'Failed to get revision %s of the file %s '.
+            \           'from the repository %s: %s',
+            \   'annf': 'Failet to annotate revision %s of the file %s '.
+            \           'in the repository %s: %s',
+            \  'difff': 'Failed to get diff between %s and %s for files %s '.
+            \           'in the repository %s: %s',
+            \ 'sdifff': 'Failed to get status information '.
+            \           'for the repository %s: %s',
+            \ 'rdifff': 'Failed to property %s for changeset %s '.
+            \           'in the repository %s: %s',
+            \    'lsf': 'Failed to list files in the changeset %s '.
+            \           'of the repository %s: %s',
+            \'statusf': 'Failed to obtain status of the repository %s: %s',
+            \    'rlf': 'Failed to list commits in repository %s: %s',
+            \    'lbf': 'Failed to create/remove %s %s for revision %s '.
+            \           'in the repository %s: %s',
+            \'branchf': 'Failed to get list of branches '.
+            \           'from the repository %s: %s',
+            \  'grepf': 'Failed to search through the repository %s: %s',
+            \   'tagf': 'Failed to get list of tags from the repository %s: %s',
+            \   'addf': 'Failed to add file %s to the repository %s: %s',
+            \ 'cbnimp': 'Git driver is not able to close branch',
+            \   'nloc': 'Git driver does not suppport local tags or branches',
+            \   'chbf': 'Failed to create branch %s in the repository %s: %s',
+            \  'nocfg': 'Failed to get property %s of repository %s',
+            \ 'invrng': 'Range %s..%s is invalid for the repository %s, '.
+            \           'as well as reverse',
+            \    'ppf': 'Failed to run “git %s” for the repository %s: %s',
+        \}
+let s:git={}
+let s:_options={
+            \'git_maxitercsnum': {'default': 1000, 'checker': 'range 0 inf'},
+        \}
+"▶1 s:hypsites
+let s:hypsites=s:_r.hypsites.git
+"▶1 refile :: gitfname → path
+function s:F.refile(fname)
+    return a:fname[0] is# '"' ? eval(a:fname) : a:fname
+endfunction
+"▶1 gitcmd :: repo, cmd, args, kwargs, esc → String
+function s:F.gitcmd(repo, ...)
+    return 'git --git-dir='.  shellescape(a:repo.path.'/.git', a:4).
+                \' --work-tree='.shellescape(a:repo.path,         a:4).
+                \' '.call(s:_r.utils.getcmd, a:000, {})
+endfunction
+"▶1 git :: repo, cmd, args, kwargs, has0[, msgid[, marg1[, …]]] → [String] + ?
+function s:F.git(repo, cmd, args, kwargs, hasnulls, ...)
+    let cmd=s:F.gitcmd(a:repo, a:cmd, a:args, a:kwargs, a:hasnulls)
+    let r=s:_r.utils.run(cmd, a:hasnulls, a:repo.path)
+    if v:shell_error && a:0
+        call call(s:_f.throw, a:000+[a:repo.path, join(r[:-1-(a:hasnulls)],
+                    \                                  "\n")], {})
+    endif
+    return r
+endfunction
+"▶1 gitm :: {git args} → + :echom
+function s:F.gitm(...)
+    return s:_r.utils.printm(call(s:F.git, a:000, {}))
+endfunction
+"▶1 parsecs :: csdata, lstart::UInt → (cs, line::UInt)
+" hash-parent hashes-timestamp
+"  (refs)
+" author name
+" author email
+" 1-indented commit message
+let s:logformat='%h-%H-%P-%at%n%an%n%ae%n%d%n%w(0,1,1)%B'
+let s:logkwargs={'format': s:logformat, 'encoding': 'utf-8', 'date-order': 1}
+function s:F.parsecs(csdata)
+    let cs={'branch': 'default'}
+    let [rev, hex, parents, time]=split(remove(a:csdata, 0), '-', 1)
+    let cs.hex=hex
+    let cs.parents=split(parents)
+    let cs.time=+time
+    let cs.rev=rev
+    let aname=remove(a:csdata, 0)
+    let aemail=remove(a:csdata, 0)
+    let cs.user=aname.' <'.aemail.'>'
+    let cs.tags=split(remove(a:csdata, 0)[2:-2], ', ')
+    let cs.bookmarks=[]
+    "▶2 get description
+    let description=[]
+    while !empty(a:csdata) && a:csdata[0][0] is# ' '
+        let description+=[remove(a:csdata, 0)[1:]]
+    endwhile
+    let cs.description=join(description, "\n")
+    if get(a:csdata, 0, 0) is# ''
+        call remove(a:csdata, 0)
+    endif
+    "▲2
+    return cs
+endfunction
+"▶1 git.getcs :: repo, rev → cs
+function s:git.getcs(repo, rev)
+    let cs=s:F.parsecs(s:F.git(a:repo, 'log', ['-n1', a:rev], s:logkwargs,
+                \              0, 'csf', a:rev))
+    " XXX This construct is used to preserve information like “allfiles” etc
+    let a:repo.changesets[cs.hex]=extend(get(a:repo.changesets, cs.hex, {}), cs)
+    return a:repo.changesets[cs.hex]
+endfunction
+"▶1 git.getwork :: repo → cs
+function s:git.getwork(repo)
+    return a:repo.functions.getcs(a:repo, 'HEAD')
+endfunction
+"▶1 prepgitargs
+function s:F.prepgitargs(repo, ...)
+    "▶2 Prepare s:F.git arguments
+    let args=[]
+    let kwargs=copy(s:logkwargs)
+    let revargs=[]
+    let revkwargs={}
+    if a:0
+        let revargs+=[((a:1 is 0)?(''):(a:1.'^..')).
+                    \ ((a:2 is 0)?(''):(a:2))]
+    else
+        let revkwargs.all=1
+        let revkwargs['full-history']=1
+    endif
+    let gitargs=[a:repo, 'log', args, kwargs, 0]
+    if a:0
+        let gitargs+=['rlogf', a:1, a:2]
+    else
+        let gitargs+=['logf']
+    endif
+    "▶2 Prepare hexslist in case git_maxitercsnum is greater then zero
+    if a:repo.maxitercsnum
+        let gitrlargs=copy(gitargs)
+        let gitrlargs[2]=copy(gitrlargs[2])+revargs
+        let gitrlargs[3]=extend(extend(copy(gitrlargs[3]),
+                    \           s:rlkwargs),
+                    \           revkwargs)
+        let hexlist=call(s:F.git, gitrlargs, {})[:-2]
+        let kwargs['no-walk']=1
+    else
+        call extend(kwargs, revkwargs)
+        call extend(  args, revargs  )
+        let hexlist=1
+    endif
+    "▲2
+    return [hexlist, gitargs]
+endfunction
+"▶1 git.getchangesets :: repo[, rangestart[, rangeend]] → [cs]
+let s:rlkwargs={'format': '%H'}
+function s:git.getchangesets(repo, ...)
+    let [hexlist, gitargs]=call(s:F.prepgitargs, [a:repo]+a:000, {})
+    let args=gitargs[2]
+    let cslist=[]
+    while !empty(hexlist)
+        if hexlist is 1
+            let hexlist=0
+        else
+            let curhexs=remove(hexlist, 0,
+                        \      min([a:repo.maxitercsnum, len(hexlist)])-1)
+            let gitargs[2]=args+curhexs
+        endif
+        let log=call(s:F.git, gitargs, {})[:-2]
+        "▶2 Parse changeset information
+        while !empty(log)
+            let cs=s:F.parsecs(log)
+            let a:repo.changesets[cs.hex]=extend(get(a:repo.changesets, cs.hex,
+                        \                            {}),
+                        \                        cs)
+            call insert(cslist, a:repo.changesets[cs.hex])
+        endwhile
+        "▲2
+    endwhile
+    return cslist
+endfunction
+"▶1 git.revrange :: repo, rev1, rev2 → [cs]
+let s:git.revrange=s:git.getchangesets
+"▶1 git.updatechangesets :: repo → _
+function s:git.updatechangesets(...)
+endfunction
+"▶1 git.getrevhex :: repo, rev → hex
+let s:prevrevhex={}
+function s:git.getrevhex(repo, rev)
+    if a:rev=~#'\v^[0-9a-f]{40}$'
+        if has_key(s:prevrevhex, a:repo.path)
+            unlet s:prevrevhex[a:repo.path]
+        endif
+        return a:rev
+    endif
+    let r=s:F.git(a:repo, 'rev-parse', [a:rev], {}, 0, 'hexf', a:rev)[0]
+    let s:prevrevhex[a:repo.path]=[a:rev, r]
+    return r
+endfunction
+"▶1 git.gettiphex
+" XXX Uses master or working directory revision instead of latest revision
+function s:git.gettiphex(repo)
+    try
+        return a:repo.functions.getrevhex(a:repo, 'master')
+    catch
+        return a:repo.functions.gettiphex(a:repo)
+    endtry
+endfunction
+"▶1 git.getworkhex :: repo → hex
+function s:git.getworkhex(repo)
+    return a:repo.functions.getrevhex(a:repo, 'HEAD')
+endfunction
+"▶1 git.setcsprop :: repo, cs, propname → propvalue
+function s:git.setcsprop(repo, cs, prop)
+    if a:prop is# 'allfiles'
+        let r=map(s:F.git(a:repo, 'ls-tree', ['--', a:cs.hex],
+                    \                        {'name-only': 1, 'r': 1}, 0,
+                    \     'lsf', a:cs.hex)[:-2], 's:F.refile(v:val)')
+    elseif a:prop is# 'children'
+        let lines=filter(map(s:F.git(a:repo, 'rev-list', [], {'all': 1,
+                    \                                'full-history': 1,
+                    \                                    'children': 1}, 0,
+                    \                'rlf')[:-2],
+                    \        'split(v:val)'),
+                    \    'has_key(a:repo.changesets, v:val[0])')
+        for [hex; children] in lines
+            let a:repo.changesets[hex].children=children
+        endfor
+        return a:cs.children
+    elseif       a:prop is# 'renames' || a:prop is# 'copies' ||
+                \a:prop is# 'changes' || a:prop is# 'files'  ||
+                \a:prop is# 'removes'
+        let lparents=len(a:cs.parents)
+        if lparents==0
+            let allfiles=a:repo.functions.getcsprop(a:repo, a:cs, 'allfiles')
+            let a:cs.renames={}
+            let a:cs.copies={}
+            let a:cs.changes=copy(allfiles)
+            let a:cs.files=copy(a:cs.changes)
+            let a:cs.removes=[]
+        elseif lparents==1
+            let args=[a:cs.parents[0].'..'.a:cs.hex]
+            let kwargs={'name-status': 1, 'M': 1, 'diff-filter': 'ADMR'}
+            if a:prop is# 'copies'
+                let kwargs.C=1
+                let kwargs['find-copies-harder']=1
+                let kwargs['diff-filter'].='C'
+            endif
+            let d=map(s:F.git(a:repo, 'diff', args, kwargs, 'rdifff', a:prop,
+                        \     a:cs.hex)[:-2], 'split(v:val, "\t")')
+            if a:prop is# 'copies'
+                let a:cs.copies={}
+            endif
+            let a:cs.renames={}
+            let a:cs.files=[]
+            let a:cs.removes=[]
+            for [status; files] in d
+                call map(files, 's:F.refile(v:val)')
+                if status[0] is# 'M' || status[0] is# 'A'
+                    let a:cs.files+=[files[0]]
+                elseif status[0] is# 'D'
+                    let a:cs.removes+=[files[0]]
+                elseif status[0] is# 'R'
+                    let a:cs.renames[files[1]]=files[0]
+                elseif status[0] is# 'C'
+                    let a:cs.copies[files[1]]=files[0]
+                endif
+            endfor
+            let a:cs.changes=a:cs.files+a:cs.removes
+        elseif lparents>=2
+            " FIXME Here must be files that had merge conflicts
+            let a:cs.renames={}
+            let a:cs.copies={}
+            let a:cs.changes=[]
+            let a:cs.files=[]
+            let a:cs.removes=[]
+        endif
+        return a:cs[a:prop]
+    endif
+    let a:cs[a:prop]=r
+    return r
+endfunction
+"▶1 nullnl :: [String] → [String]
+" Convert between lines (NL separated strings with NULLs represented as NLs) and 
+" NULL separated strings with NLs represented by NLs.
+function s:F.nullnl(text)
+    let r=['']
+    for nlsplit in map(copy(a:text), 'split(v:val, "\n", 1)')
+        let r[-1].="\n".nlsplit[0]
+        call extend(r, nlsplit[1:])
+    endfor
+    if empty(r[0])
+        call remove(r, 0)
+    else
+        let r[0]=r[0][1:]
+    endif
+    return r
+endfunction
+"▶1 git.status :: repo[, rev1[, rev2[, files[, clean]]]]
+let s:statchars={
+            \'A': 'added',
+            \'M': 'modified',
+            \'D': 'removed',
+        \}
+let s:initstatdct={
+            \'modified': [],
+            \   'added': [],
+            \ 'removed': [],
+            \ 'deleted': [],
+            \ 'unknown': [],
+            \ 'ignored': [],
+            \   'clean': [],
+        \}
+function s:git.status(repo, ...)
+    let r=deepcopy(s:initstatdct)
+    let requiresclean=(a:0>3 && a:4)
+    if a:0 && (a:1 isnot 0 || (a:0>1 && a:2 isnot 0))
+        let args=((a:0>2 && !empty(a:3))?(['--']+a:3):([]))
+        let rspec=[]
+        let reverse=0
+        if a:1 is 0
+            if a:0>1 && a:2 isnot 0
+                let reverse=1
+            endif
+        else
+            let rspec+=[a:1]
+        endif
+        if a:0>1 && a:2 isnot 0
+            let rspec+=[a:2]
+        endif
+        call insert(args, join(rspec, '..'))
+        let kwargs={'diff-filter': 'AMD', 'name-status': 1, 'no-renames': 1}
+        let d=s:F.git(a:repo, 'diff', args, kwargs, 0, 'sdifff')[:-2]
+        let files=map(copy(d), 's:F.refile(v:val[2:])')
+        call map(copy(d), 'add(r[s:statchars[v:val[0]]], files[v:key])')
+        if reverse
+            let [r.deleted, r.unknown]=[r.unknown, r.deleted]
+            let [r.added,   r.removed]=[r.removed, r.added  ]
+        endif
+        if requiresclean
+            let allfiles=a:repo.functions.getcsprop(a:repo,rspec[0],'allfiles')
+        endif
+    else
+        let args=((a:0>2 && !empty(a:3))?(['--']+a:3):([]))
+        let kwargs={'porcelain': 1, 'z': 1}
+        let s=s:F.nullnl(s:F.git(a:repo,'status',args,kwargs,2,'statusf'))[:-2]
+        let files={}
+        while !empty(s)
+            let line=remove(s, 0)
+            let status=line[:1]
+            let file=line[3:]
+            let files[file]=1
+            if status[0] is# 'R'
+                let r.added+=[file]
+                let r.removed+=[remove(s, 0)]
+            elseif status[0] is# 'C'
+                let r.added+=[file]
+                let origfile=remove(s, 0)
+                " FIXME What should be done with origfile?
+            elseif status[0] is# 'D'
+                let r.removed+=[file]
+            elseif status[1] is# 'D'
+                let r.deleted+=[file]
+            elseif status[0] is# 'A'
+                let r.added+=[file]
+            elseif stridx(status, 'M')!=-1
+                let r.modified+=[file]
+            elseif status is# '??'
+                let r.unknown+=[file]
+            endif
+        endwhile
+        if requiresclean
+            let allfiles=a:repo.functions.getcsprop(a:repo, 'HEAD', 'allfiles')
+        endif
+    endif
+    if exists('allfiles')
+        if a:0>2 && !empty(a:3)
+            let allfiles=filter(copy(allfiles), 'index(a:3, v:val)!=-1')
+        endif
+        let r.clean=filter(copy(allfiles), '!has_key(files, v:val)')
+    endif
+    return r
+endfunction
+"▶1 git.commit :: repo, message[, files[, user[, date[, _]]]]
+function s:git.commit(repo, message, ...)
+    let kwargs={'cleanup': 'verbatim'}
+    let args=[]
+    if a:0
+        if empty(a:1)
+            let kwargs.all=1
+        else
+            let args+=['--']+a:1
+            call s:_r.utils.addfiles(a:repo, a:1)
+        endif
+        if a:0>1 && !empty(a:2)
+            let kwargs.author=a:2
+        endif
+        if a:0>2 && !empty(a:3)
+            let kwargs.date=a:3
+        endif
+        if a:0>3 && !empty(a:4)
+            call s:_f.throw('cbnimp')
+        endif
+    else
+        let kwargs.all=1
+    endif
+    return s:_r.utils.usefile(a:repo, a:message, 'file', 'message',
+                \             s:F.gitm, args, kwargs, 0, 'cif')
+endfunction
+"▶1 git.branch :: repo, branchname, force → + FS
+function s:git.branch(repo, branch, force)
+    if a:force
+        return s:F.gitm(a:repo, 'checkout', [a:branch], {'B': 1}, 0,
+                    \   'chbf', a:branch)
+    else
+        call a:repo.functions.label(a:repo, 'branch', a:branch, 'HEAD', 0, 0)
+        call a:repo.functions.update(a:repo, a:branch, 0)
+    endif
+endfunction
+"▶1 git.label :: repo, type, label, rev, force, local → + FS
+function s:git.label(repo, type, label, rev, force, local)
+    if a:local
+        call s:_f.throw('nloc')
+    endif
+    let args=['--', a:label]
+    let kwargs={}
+    if a:force
+        let kwargs.force=1
+    endif
+    if a:rev is 0
+        let kwargs.d=1
+    else
+        let args+=[a:rev]
+    endif
+    return s:F.gitm(a:repo, a:type, args, kwargs, 0,
+                \   'lbf', a:type, a:label, a:rev)
+endfunction
+"▶1 git.update :: repo, rev, force → + FS
+" XXX This must not transform {rev} into hash: it will break rf-branch()
+function s:git.update(repo, rev, force)
+    let kwargs={}
+    if a:force
+        let kwargs.force=1
+    endif
+    "▶2 XXX (hacks): Avoid “detached HEAD” state if possible
+    if a:rev=~#'\v^[0-9a-z]{40}$'
+        if has_key(s:prevrevhex, a:repo.path) &&
+                    \a:rev is# s:prevrevhex[a:repo.path][1] &&
+                    \filereadable(s:_r.os.path.join(a:repo.githpath,
+                    \                             s:prevrevhex[a:repo.path][0]))
+            let rev=s:prevrevhex[a:repo.path][0]
+            unlet s:prevrevhex[a:repo.path]
+        else
+            for [d, ds, fs] in s:_r.os.walk(a:repo.githpath)
+                for f in fs
+                    let reffile=s:_r.os.path.join(d, f)
+                    if a:rev is# get(readfile(reffile, 'b'), 0, 0)
+                        let rev=join(s:_r.os.path.split(
+                                    \s:_r.os.path.relpath(reffile,
+                                    \                     a:repo.githpath))[1:],
+                                    \"/")
+                        break
+                    endif
+                endfor
+            endfor
+            if !exists('rev')
+                let rev=a:rev
+            endif
+        endif
+    else
+        let rev=a:rev
+    endif
+    "▲2
+    let args=[rev]
+    return s:F.gitm(a:repo, 'checkout', args, kwargs, 0, 'updf', a:rev)
+endfunction
+"▶1 git.move :: repo, force, source, destination → + FS
+function s:git.move(repo, force, source, destination)
+    return s:F.gitm(a:repo, 'mv', ['--', a:source, a:destination],
+                \   a:force ? {'force': 1} : {}, 0, 'mvf',
+                \   a:source, a:destination)
+endfunction
+"▶1 git.add :: repo, file → + FS
+function s:git.add(repo, file)
+    return s:F.gitm(a:repo, 'add', ['--', a:file], {}, 0, 'addf',
+                \   escape(a:file, '\'))
+endfunction
+"▶1 git.forget :: repo, file → + FS
+function s:git.forget(repo, file)
+    return s:F.gitm(a:repo, 'rm', ['--', a:file], {'cached': 1}, 0, 'fgf',
+                \   escape(a:file, '\'))
+endfunction
+"▶1 git.remove :: repo, file → + FS
+function s:git.remove(repo, file)
+    return s:F.gitm(a:repo, 'rm', ['--', a:file], {}, 0, 'rmf',
+                \   escape(a:file, '\'))
+endfunction
+"▶1 addtoignfile :: file, line → + FS
+function s:F.addtoignfile(ignfile, line)
+    let r=[]
+    if filereadable(a:ignfile)
+        let r+=readfile(a:ignfile, 'b')
+    endif
+    if !empty(r) && empty(r[-1])
+        call remove(r, -1)
+    endif
+    let r+=[a:line, '']
+    return writefile(r, a:ignfile, 'b')
+endfunction
+"▶1 git.ignore :: repo, file → + FS
+function s:git.ignore(repo, file)
+    return s:F.addtoignfile(s:_r.os.path.join(a:repo.path, '.gitignore'),
+                \           '/'.escape(a:file, '\*?[]'))
+endfunction
+"▶1 git.ignoreglob :: repo, glob → + FS
+function s:git.ignoreglob(repo, glob)
+    return s:F.addtoignfile(s:_r.os.path.join(a:repo.path,'.gitignore'), a:glob)
+endfunction
+"▶1 git.grep :: repo, files, revisions, ignorecase, wdfiles::Bool → qflist
+"▶2 parsegrep :: lines → [(file, lnum, String)]
+function s:F.parsegrep(lines)
+    let r=[]
+    let contline=0
+    while !empty(a:lines)
+        let sp=split(remove(a:lines, 0), "\n", 1)
+        if contline
+            let r[-1][0].="\n".sp[0]
+            if len(sp)>1
+                let r[-1]+=sp[1:]
+                let contline=0
+            endif
+        elseif len(sp)==1
+            let contline=1
+            let r+=[sp]
+        else
+            let r+=[sp]
+        endif
+    endwhile
+    call map(r, '[v:val[0], str2nr(v:val[1]), v:val[2]]')
+    return r
+endfunction
+"▲2
+function s:git.grep(repo, pattern, files, revisions, ic, wdfiles)
+    let args=['-e', a:pattern, '--']+a:files
+    let kwargs={'full-name': 1, 'extended-regexp': 1, 'n': 1, 'z': 1}
+    let gitargs=[a:repo, 'grep', args, kwargs, 1, 0]
+    let r=[]
+    if !empty(a:revisions)
+        let revs=[]
+        for s in a:revisions
+            if type(s)==type([])
+                let revs+=map(copy(a:repo.functions.revrange(a:repo,s[0],s[1])),
+                            \ 'v:val.hex')
+            else
+                let revs+=[a:repo.functions.getrevhex(a:repo, s)]
+            endif
+            unlet s
+        endfor
+        call extend(args, revs, 2)
+        for [revfile, lnum, text] in s:F.parsegrep(call(s:F.git, gitargs, {}))
+            let cidx=stridx(revfile, ':')
+            let rev=revfile[:(cidx-1)]
+            let file=revfile[(cidx+1):]
+            let r+=[{'filename': [rev, file], 'lnum': lnum, 'text': text}]
+        endfor
+    else
+        for [file, lnum, text] in s:F.parsegrep(call(s:F.git, gitargs, {}))
+            let r+=[{'filename': file, 'lnum': lnum, 'text': text}]
+        endfor
+    endif
+    return r
+endfunction
+"▶1 git.readfile :: repo, rev, file → [String]
+function s:git.readfile(repo, rev, file)
+    return s:F.git(a:repo, 'cat-file', ['blob', a:rev.':'.a:file], {}, 2,
+                \  'filef', a:rev, a:file)
+endfunction
+"▶1 git.annotate :: repo, rev, file → [(file, hex, linenumber)]
+function s:git.annotate(repo, rev, file)
+    let args=[a:rev, '--', a:file]
+    let kwargs={'porcelain': 1, 'C': 1, 'M': 1}
+    let lines=s:F.git(a:repo, 'blame', args, kwargs, 1, 'annf', a:rev, a:file)
+    call filter(lines, 'v:val=~#''\v^(\x{40}\ |filename\ )''')
+    let r=[]
+    let filename=a:file
+    while !empty(lines)
+        let line=remove(lines, 0)
+        if !empty(lines) && lines[0][:8] is# 'filename '
+            let filename=s:F.refile(remove(lines, 0)[9:])
+        endif
+        let r+=[[filename, line[:39], str2nr(line[41:])]]
+    endwhile
+    return r
+endfunction
+"▶1 git.diff :: repo, rev, rev, files, opts → [String]
+let s:difftrans={
+            \  'reverse': 'R',
+            \ 'ignorews': 'ignore-all-space',
+            \'iwsamount': 'ignore-space-change',
+            \ 'numlines': 'unified',
+            \  'alltext': 'text',
+        \}
+function s:git.diff(repo, rev1, rev2, files, opts)
+    let diffopts=s:_r.utils.diffopts(a:opts, a:repo.diffopts, s:difftrans)
+    if has_key(diffopts, 'unified')
+        let diffopts.unified=''.diffopts.unified
+    endif
+    let kwargs=copy(diffopts)
+    let args=[]
+    if empty(a:rev2)
+        if !empty(a:rev1)
+            let args+=[a:rev1.'^..'.a:rev1]
+        endif
+    else
+        let args+=[a:rev2]
+        if !empty(a:rev1)
+            let args[-1].='..'.a:rev1
+        endif
+    endif
+    if !empty(a:files)
+        let args+=['--']+a:files
+    endif
+    let r=s:F.git(a:repo, 'diff', args, kwargs, 1,
+                \ 'difff', a:rev1, a:rev2, join(a:files, ', '))
+    return r
+endfunction
+"▶1 git.diffre :: _, opts → Regex
+let s:diffre='\m^diff --git \v((\")?%s\/.{-}\2) \2%s\/'
+function s:git.diffre(repo, opts)
+    if get(a:opts, 'reverse', 0)
+        return printf(s:diffre, 'b', 'a')
+    else
+        return printf(s:diffre, 'a', 'b')
+    endif
+endfunction
+"▶1 git.diffname :: _, line, diffre, _ → rpath
+function s:git.diffname(repo, line, diffre, opts)
+    let file=get(matchlist(a:line, a:diffre), 1, 0)
+    if file is 0
+        return 0
+    else
+        return s:F.refile(file)[2:]
+    endif
+endfunction
+"▶1 git.getrepoprop :: repo, propname → a
+function s:git.getrepoprop(repo, prop)
+    if a:prop is# 'branch'
+        let branches=s:F.git(a:repo, 'branch', [], {'l': 1}, 0,
+                    \        'branchf')[:-2]
+        return get(filter(branches, 'v:val[0] is# "*"'), 0, '')[2:]
+    elseif a:prop is# 'url'
+        let r=get(s:F.git(a:repo, 'config', ['remote.origin.pushurl'], {}, 0),
+                    \0, 0)
+        if v:shell_error || r is 0
+            let r=get(s:F.git(a:repo, 'config', ['remote.origin.url'], {}, 0),
+                        \0, 0)
+        endif
+        if r isnot 0
+            return r
+        endif
+    elseif a:prop is# 'branchslist' || a:prop is# 'brancheslist'
+        " XXX stridx(v:val, " ")==-1 filters out “(no branch)” item
+        return filter(map(s:F.git(a:repo, 'branch', [], {'l': 1}, 0,
+                    \             'branchf')[:-2], 'v:val[2:]'),
+                    \     'stridx(v:val, " ")==-1')
+    elseif a:prop is# 'tagslist'
+        return s:F.git(a:repo, 'tag', [], {}, 0, 'tagf')[:-2]
+    elseif a:prop is# 'bookmarkslist'
+        return []
+    endif
+    call s:_f.throw('nocfg', a:prop, a:repo.path)
+endfunction
+"▶1 pushpull :: cmd, repo, dryrun, force[, URL[, rev]] → + ?
+function s:F.pushpull(cmd, repo, dryrun, force, ...)
+    let kwargs={'all': 1}
+    let args=[]
+    if a:0
+        if a:1 isnot 0
+            let args+=['--', a:1]
+        endif
+        if a:0>1 && a:2 isnot 0
+            if empty(args)
+                let args+=['--', 'origin']
+            endif
+            let args+=[a:2]
+        endif
+    endif
+    if (a:cmd is# 'fetch' && !empty(args)) || len(args)>2
+        unlet kwargs.all
+    endif
+    if a:force
+        let kwargs.force=1
+    endif
+    if a:dryrun
+        let kwargs['dry-run']=1
+    endif
+    return s:F.git(a:repo, a:cmd, args, kwargs, 0, 'ppf', a:cmd)
+endfunction
+"▶1 git.push :: repo, dryrun, force[, URL[, rev]]
+function s:git.push(...)
+    return call(s:F.pushpull, ['push']+a:000, {})
+endfunction
+"▶1 git.pull :: repo, dryrun, force[, URL[, rev]]
+function s:git.pull(...)
+    return call(s:F.pushpull, ['fetch']+a:000, {})
+endfunction
+"▶1 git.repo :: path → repo
+function s:git.repo(path)
+    let repo={'path': a:path, 'changesets': {}, 'mutable': {},
+                \'local': (stridx(a:path, '://')==-1),
+                \'labeltypes': ['tag', 'branch'],
+                \'hasrevisions': 0, 'requires_sort': 0,
+                \'githpath': s:_r.os.path.join(a:path, '.git', 'refs', 'heads'),
+                \'hypsites': deepcopy(s:hypsites),
+                \'maxitercsnum': s:_f.getoption('git_maxitercsnum'),
+                \'iterfuncs': deepcopy(s:iterfuncs),
+                \}
+    if has_key(s:prevrevhex, a:path)
+        unlet s:prevrevhex[a:path]
+    endif
+    return repo
+endfunction
+"▶1 git.checkdir :: dir → Bool
+function s:git.checkdir(dir)
+    return s:_r.os.path.isdir(s:_r.os.path.join(a:dir, '.git'))
+endfunction
+"▶1 iterfuncs
+let s:iterfuncs={}
+"▶2 nextcs
+function s:F.nextcs()
+    if empty(self.log)
+        if empty(self.hexlist)
+            return 0
+        endif
+        let curhexs=remove(self.hexlist, 0, min([self.maxitercsnum,
+                    \                            len(self.hexlist)])-1)
+        let gitargs=copy(self.gitargs)
+        let gitargs[2]=gitargs[2]+curhexs
+        let self.log=call(s:F.git, gitargs, {})[:-2]
+    endif
+    let cs=s:F.parsecs(self.log)
+    let self.repo.changesets[cs.hex]=extend(get(self.repo.changesets, cs.hex,
+                \                               {}),
+                \                          cs)
+    return self.repo.changesets[cs.hex]
+endfunction
+"▶2 iterfuncs.revrange, iterfuncs.changesets
+function s:iterfuncs.revrange(repo, opts)
+    let pgaargs=[a:repo]
+    if has_key(a:opts, 'revrange')
+        let pgaargs+=a:opts.revrange
+    elseif has_key(a:opts, 'revision')
+        let pgaargs+=[0, a:opts.revision]
+    endif
+    let [hexlist, gitargs]=call(s:F.prepgitargs, pgaargs, {})
+    let r={'repo': a:repo, 'maxitercsnum': a:repo.maxitercsnum,
+                \'gitargs': gitargs, 'next': s:F.nextcs}
+    if hexlist is 1
+        let hexlist=0
+        let log=call(s:F.git, gitargs, {})[:-2]
+    else
+        let log=[]
+        let r.csnum=len(hexlist)
+    endif
+    let r.hexlist=hexlist
+    let r.log=log
+    return r
+endfunction
+let s:iterfuncs.changesets=s:iterfuncs.revrange
+let s:iterfuncs.ancestors=s:iterfuncs.revrange
+"▶1 Register driver
+call s:_f.regdriver('Git', s:git)
+"▶1
+call frawor#Lockvar(s:, '_pluginloaded,prevrevhex')
+" vim: ft=vim ts=4 sts=4 et fmr=▶,▲

autoload/aurum/drivers/mercurial.vim

+"▶1
+scriptencoding utf-8
+execute frawor#Setup('0.2', {'@%aurum/drivers/common/hypsites': '0.0',
+            \                                   '@%aurum/repo': '5.0',
+            \                   '@%aurum/drivers/common/utils': '0.0',
+            \                                       '@/python': '0.0',
+            \                                           '@/os': '0.0',
+            \                                      '@/options': '0.0',})
+let s:hg={}
+let s:iterfuncs={}
+let s:usepythondriver=0
+if has('python')
+    let s:py='python'
+    try
+        " execute s:py "try:\n".
+                    " \"    if type(aurum).__name__=='module':\n".
+                    " \"        reload(aurum)\n".
+                    " \"except NameError:\n".
+                    " \"    pass"
+        execute s:py 'import aurum'
+        let s:usepythondriver=1
+    catch
+        " s:usepythondriver stays equal to 0, errors are ignored
+    endtry
+    " FIXME Does not work in python3. Not very problematic as mercurial does not 
+    " do this either, but it will be necessary to review these lines after 
+    " python3 support in mercurial will be finished.
+    if s:usepythondriver
+        execute s:py 'reload(aurum)'
+    endif
+endif
+let s:nullrev=repeat('0', 40)
+"▶1 Messages
+let s:_messages={
+            \ 'norepo': 'Repository %s not found',
+            \ 'norev' : 'No revision %s in repository %s',
+            \ 'nofile': 'File %s is not present in revision %s '.
+            \           'from repository %s',
+            \  'csuns': 'Unable to get changeset from repository %s: '.
+            \           'operation not supported',
+            \'statuns': 'Unable to get working directory status '.
+            \           'of repository %s: operation not supported',
+            \ 'comuns': 'Unable to commit to repository %s: '.
+            \           'operation not supported',
+            \'diffuns': 'Unable to create a diff for repository %s: '.
+            \           'operation not supported',
+            \ 'upduns': 'Unable to update working directory state for '.
+            \           'repository %s: operation not supported',
+            \ 'destex': 'Cannot copy %s to %s: destination already exists',
+            \ 'nhgiwr': 'File %s is either a directory or not writeable',
+            \'reponwr': 'Unable to write to repository root (%s)',
+            \  'nocfg': 'No such property of repository %s: %s',
+            \'failcfg': 'Failed to get property %s of repository %s',
+            \'nlocbms': 'Bookmarks can’t be local',
+            \'parsefail': 'Failed to parse changeset information',
+            \ 'filefail': 'Failed to get file %s '.
+            \             'from the repository %s: %s',
+            \ 'difffail': 'Failed to get diff between %s and %s '.
+            \             'for files %s from the repository %s: %s',
+            \ 'statfail': 'Failed to obtain status information '.
+            \             'for the repository %s: %s',
+            \  'annfail': 'Failed to annotate revision %s of file %s '.
+            \             'in the repository %s: %s',
+            \  'cspfail': 'Failed to get property %s for changeset %s '.
+            \             'in the repository %s: %s',
+            \  'logfail': 'Failed to get log for the repository %s: %s',
+            \  'keyfail': 'Failed to get %s for the repository %s: %s',
+            \   'csfail': 'Failed to get changeset %s '.
+            \             'from the repository %s: %s',
+            \    'bfail': 'Failed to get current branch '.
+            \             'from the repository %s: %s',
+            \  'renfail': 'Failed to get renames list for revision %s '.
+            \             'in the repository %s: %s',
+            \  'cmdfail': 'Failure while running command %s '.
+            \             'for the repository %s: %s',
+            \ 'grepfail': 'Failed to search through the repository %s: %s',
+            \   'scfail': 'Failed to show [paths] section '.
+            \             'for the repository %s: %s',
+            \  'svnfail': 'Failed to run “hg svn info” '.
+            \             'for the repository %s: %s',
+            \ 'stat1mis': 'You must specify first revision as well',
+            \ 'nosvnrev': 'Failed to find revision in “hg svn info” output '.
+            \             'in the repository %s. Output:%s',
+            \ 'nohggitc': 'It appears that hg-git is not enabled '.
+            \             '(tried options extensions.hggit and extensions.git)',
+            \  'nohggit': 'It appears that hg-git is not installed '.
+            \             '(importing GitHandler threw ImportError)',
+            \ 'nogitrev': 'No git revision associated with revision %s '.
+            \             'in repository %s',
+        \}
+"▶1 Options
+let s:_options={
+            \'hg_useshell':  {'default': [],
+            \                  'filter':
+            \                    '(if type "" (|=split(@.@,''\L\+'')) any '.
+            \                     'list match /\v^\l+$/)'},
+        \}
+            " \'hg_itercsnum': {'default': ((s:usepythondriver)?(0):(1000)),
+            " \                 'checker': 'range 0 inf'},
+"▶1 s:hypsites
+let s:hypsites=[]
+let s:bookmarks='repo.functions.getcsprop(repo, hex, "bookmarks")'
+let s:tags='repo.functions.getcsprop(repo, hex, "tags")'
+if s:usepythondriver
+    let s:gitrev='repo.functions.githex(repo, hex)'
+else
+    let s:gitrev='((!empty('.s:bookmarks.'))?'.
+                \      '('.s:bookmarks.'[0]):'.
+                \   '((!empty('.s:tags.'))?'.
+                \      '(matchstr(get(filter(copy('.s:tags.'), '.
+                \                           '"stridx(v:val, ''/'')!=-1"), 0, '.
+                \           '"master"), "\\v[^/]+$"))'.
+                \   ':'.
+                \      '("master")))'
+endif
+unlet s:bookmarks s:tags
+let s:hypsites+=map(copy(s:_r.hypsites.git), '["protocol[:2] is# ''git'' && (".v:val[0].")", '.
+            \                                 'map(copy(v:val[1]), '.
+            \                                     '''(v:key is# "clone" || v:key is# "push")?'.
+            \                                           '(substitute(v:val, "\\v^\"%(git)@!", "\"git+", "")):'.
+            \                                           '(substitute(v:val, "\\v<hex>", s:gitrev, "g"))'')]')
+unlet s:gitrev
+let s:hypsites+=s:_r.hypsites.mercurial
+let s:svnrev='repo.functions.svnrev(repo, hex)'
+let s:hypsites+=map(copy(s:_r.hypsites.svn), '[v:val[0], map(copy(v:val[1]), '.
+            \                                               '''substitute(v:val, "\\v<hex>", s:svnrev, "g")'')]')
+unlet s:svnrev
+"▶1 removechangesets :: repo, start_rev_num → + repo
+function s:F.removechangesets(repo, start)
+    let changesets=a:repo.changesets
+    for cs in remove(a:repo.mutable.cslist, a:start, -1)
+        let hex=cs.hex
+        for parenthex in filter(cs.parents, 'has_key(changesets, v:val)')
+            call filter(changesets[parenthex].children, 'v:val isnot# cs.hex')
+        endfor
+        for childhex in filter(cs.children, 'has_key(changesets, v:val)')
+            call filter(changesets[childhex].parents, 'v:val isnot# cs.hex')
+        endfor
+    endfor
+endfunction
+"▶1 hgcmd :: repo, cmd, args, kwargs, esc → String
+function s:F.hgcmd(repo, ...)
+    return 'hg -R '.shellescape(a:repo.path, a:4).' '.
+                \call(s:_r.utils.getcmd, a:000, {})
+endfunction
+"▶1 runshellcmd :: repo, attr, args, kwargs → + ?
+function s:F.runshellcmd(repo, attr, args, kwargs, ...)
+    let e=(a:0 && a:1)
+    let args=copy(a:args)
+    if !empty(args)
+        call insert(args, '--')
+    endif
+    return s:_r.utils.printm(s:F.hg(a:repo, a:attr, args, a:kwargs, e)[:-2+e])
+endfunction
+"▶1 hg :: repo, cmd, args, kwargs, esc, msgid[, throwarg1[, …]] → [String]
+function s:F.hg(repo, cmd, args, kwargs, hasnulls, ...)
+    let cmd=s:F.hgcmd(a:repo, a:cmd, a:args, a:kwargs, a:hasnulls)
+    let r=s:_r.utils.run(cmd, a:hasnulls, a:repo.path)
+    if v:shell_error && a:0
+        let args=copy(a:000)
+        let args[0].='fail'
+        call call(s:_f.throw, args+[a:repo.path, join(r[:-1-(a:hasnulls)],
+                    \                                 "\n")], {})
+    endif
+    return r
+endfunction
+if s:usepythondriver "▶1
+"▶2 addchangesets :: repo, [cs] → _ + repo
+function s:F.addchangesets(repo, css)
+    call map(copy(a:css), 'extend(a:repo.changesets, {v:val.hex : v:val})')
+    for cs in a:css
+        for parenthex in cs.parents
+            call add(a:repo.changesets[parenthex].children, cs.hex)
+        endfor
+    endfor
+endfunction
+"▶2 runcmd :: repo, attr, args, kwargs → + ?
+function s:F.runcmd(repo, attr, args, kwargs)
+    if index(s:_f.getoption('hg_useshell'), a:attr)!=-1
+        return s:F.runshellcmd(a:repo, a:attr, a:args, a:kwargs)
+    endif
+    execute s:py 'aurum.call_cmd(vim.eval("a:repo.path"), '.
+                \               'vim.eval("a:attr"), '.
+                \               '*vim.eval("a:args"), '.
+                \               '**vim.eval("a:kwargs"))'
+endfunction
+"▲2
+else "▶1
+"▶2 addchangesets :: repo, [cs] → _ + repo
+function s:F.addchangesets(repo, css)
+    call map(copy(a:css), 'extend(a:repo.changesets, {v:val.hex : v:val})')
+    for cs in a:css
+        call map(cs.parents, 'type(v:val)=='.type(0).' ? '.
+                    \           'a:repo.mutable.cslist[v:val].hex : '.
+                    \           'v:val')
+        for parenthex in cs.parents
+            call add(a:repo.changesets[parenthex].children, cs.hex)
+        endfor
+    endfor
+endfunction
+"▶2 unesc :: String → String
+let s:F.unesc=function('eval')
+"▶2 refile :: path → path
+function s:F.refile(path)
+    return join(s:_r.os.path.split(a:path)[1:], '/')
+endfunction
+"▶2 parsecs :: csdata, lstart::UInt → (cs, line::UInt)
+let s:stylefile=s:_r.os.path.join(s:_frawor.runtimepath,
+                \                 'misc', 'map-cmdline.csinfo')
+let s:chars = [['P', 'parents'  ],
+            \  ['T', 'tags'     ],
+            \  ['B', 'bookmarks']]
+let s:fchars= [['C', 'changes'  ],
+            \  ['R', 'removes'  ]]
+function s:F.parsecs(csdata, lstart)
+    "▶3 Initialize variables, check for changeset start
+    let cs={}
+    let line=a:lstart
+    let lcsdata=len(a:csdata)
+    if lcsdata<6 || a:csdata[line] isnot# ':'
+        call s:_f.throw('parsefail')
+    endif
+    let line+=1
+    "▶3 Simple keys: rev, hex, branch, time, user
+    let cs.rev    = str2nr(a:csdata[line])    | let line+=1
+    let cs.hex    = a:csdata[line]            | let line+=1
+    let cs.branch = s:F.unesc(a:csdata[line]) | let line+=1
+    let cs.time   = str2nr(a:csdata[line])    | let line+=1
+    let cs.user   = s:F.unesc(a:csdata[line]) | let line+=1
+    "▶3 List keys: parents, tags, bookmarks, changes, removes
+    for [char, key] in s:chars
+        let cs[key]=[]
+        while line<lcsdata && a:csdata[line][0] is# char
+            let cs[key]+=[s:F.unesc(a:csdata[line][1:])]
+            let line+=1
+        endwhile
+    endfor
+    "▶3 List file keys: changes, removes
+    for [char, key] in s:fchars
+        let cs[key]=[]
+        while line<lcsdata && a:csdata[line][0] is# char
+            let cs[key]+=[s:F.refile(s:F.unesc(a:csdata[line][1:]))]
+            let line+=1
+        endwhile
+    endfor
+    "▶3 Add data to cs.parents in case it is empty
+    if empty(cs.parents)
+        if cs.rev>0
+            let cs.parents=[cs.rev-1]
+        else
+            let cs.parents=[s:nullrev]
+        endif
+    endif
+    "▶3 Filter cs.removes, add cs.files
+    call filter(cs.removes, 'index(cs.changes, v:val)!=-1')
+    let cs.files=filter(copy(cs.changes), 'index(cs.removes, v:val)==-1')
+    "▶3 Copies, renames
+    let copies={}
+    while line<lcsdata && a:csdata[line][0] is# 'D'
+        let copies[s:F.refile(s:F.unesc(a:csdata[line][1:]))]=
+                    \                   s:F.refile(s:F.unesc(a:csdata[line+1]))
+        let line+=2
+    endwhile
+    let cs.copies  = filter(copy(copies), 'index(cs.removes, v:val)==-1')
+    let cs.renames = filter(copy(copies), '!has_key(cs.copies, v:key)')
+    "▶3 Description
+    let cs.description=s:F.unesc(a:csdata[line][1:])
+    let line+=1
+    "▲3
+    return [cs, line]
+endfunction
+"▶2 getcslist :: repo, start, end
+function s:F.getcslist(repo, start, end)
+    let kwargs={'style': s:stylefile}
+    let lines=s:F.hg(a:repo, 'log', [],
+                \    extend({'rev': a:start.':'.a:end}, kwargs), 0, 'log')[:-2]
+    let css=[]
+    if has_key(a:repo.changesets, s:nullrev)
+        let cs0=a:repo.changesets[s:nullrev]
+    else
+        let lines0=s:F.hg(a:repo, 'log', [], extend({'rev': s:nullrev}, kwargs),
+                    \     0, 'log')
+        let cs0=s:F.parsecs(lines0, 0)[0]
+    endif
+    let llines=len(lines)
+    let line=0
+    let prevrev=-1
+    while line<llines
+        let [cs, line]=s:F.parsecs(lines, line)
+        if cs.rev-prevrev!=1 && prevrev!=-1
+            let css+=map(range(prevrev+1, cs.rev-1),
+                        \'s:F.parsecs(s:F.hg(a:repo, "log", [], '.
+                        \            'extend({"rev": "".v:val}, kwargs), 0, '.
+                        \            '"log"), 0)[0]')
+        endif
+        let css+=[cs]
+        let prevrev=cs.rev
+    endwhile
+    let css+=[cs0]
+    return css
+endfunction
+"▶2 getkeylist :: repo, key → [(name, rev)]
+function s:F.getkeylist(repo, key)
+    let lines=s:F.hg(a:repo, a:key, [], {}, 0, 'key', a:key)[:-2]
+    if len(lines)==1 && lines[0]!~#'\v\ [1-9]\d*\:\x{12}$'
+        return []
+    endif
+    return map(copy(lines), 'matchlist(v:val, '.
+                \                         '''\v^(.{-})\ +(\d+)\:\x{12}'')[1:2]')
+endfunction
+"▲2
+let s:F.runcmd=s:F.runshellcmd
+endif
+"▶1 getcs :: repo, rev → cs
+if s:usepythondriver "▶2
+    function s:F.getcs(repo, rev)
+        let cs={}
+        try
+            execute s:py 'aurum.get_cs(vim.eval("a:repo.path"), '.
+                        \             'vim.eval("a:rev"))'
+        endtry
+        return cs
+    endfunction
+else "▶2
+    function s:F.getcs(repo, rev)
+        let csdata=s:F.hg(a:repo, 'log', [], {'rev': a:rev, 'style': s:stylefile},0,
+                    \     'cs', a:rev)
+        let cs=s:F.parsecs(csdata, 0)[0]
+        call map(cs.parents,
+                    \'type(v:val)=='.type(0).'? '.
+                    \   'a:repo.functions.getrevhex(a:repo, v:val): '.
+                    \   'v:val')
+        return cs
+    endfunction
+endif
+"▶1 hg.getcs :: repo, rev → cs
+function s:hg.getcs(repo, rev)
+    if type(a:rev)==type('') && has_key(a:repo.changesets, a:rev)
+        return a:repo.changesets[a:rev]
+    elseif type(a:rev)==type(0)
+        if !empty(a:repo.mutable.cslist) && a:rev<a:repo.csnum
+            return a:repo.mutable.cslist[a:rev]
+        endif
+    endif
+    if a:rev=~#'\v^[0-9a-f]{40}$'
+        let hex=a:rev
+    else
+        let hex=a:repo.functions.getrevhex(a:repo, a:rev)
+        if has_key(a:repo.changesets,hex)
+            return a:repo.changesets[hex]
+        endif
+    endif
+    let cs=s:F.getcs(a:repo, hex)
+    let a:repo.changesets[cs.hex]=cs
+    return cs
+endfunction
+"▶1 hg.getwork :: repo → hex
+function s:hg.getwork(repo)
+    return a:repo.functions.getcs(a:repo, '.')
+endfunction
+"▶1 hg.getchangesets :: repo → changesets + repo.changesets
+function s:hg.getchangesets(repo)
+    call a:repo.functions.updatechangesets(a:repo, 1)
+    return a:repo.mutable.cslist[:-2]
+endfunction
+"▶1 hg.revrange :: repo, rev, rev → [cs]
+function s:hg.revrange(repo, rev1, rev2)
+    if empty(a:repo.mutable.cslist)
+        let cslist=a:repo.functions.getchangesets(a:repo)
+    else
+        let cslist=a:repo.mutable.cslist
+    endif
+    let rev1=a:repo.functions.getcs(a:repo, a:rev1).rev
+    let rev2=a:repo.functions.getcs(a:repo, a:rev2).rev
+    if rev1>rev2
+        let [rev1, rev2]=[rev2, rev1]
+    endif
+    return cslist[(rev1):(rev2)]
+endfunction
+"▶1 hg.updatechangesets :: repo → + repo
+"▶2 updatewithtagsdict
+function s:F.updatewithtagsdict(csmap, d)
+    for key in ['tags', 'bookmarks']
+        if has_key(a:d, key) && !empty(a:d[key])
+            let cd=a:d[key]
+            let expr='get(cd, v:key, [])'
+        else
+            let expr='[]'
+        endif
+        call map(copy(a:csmap), 'extend(v:val, {"'.key.'": '.expr.'})')
+    endfor
+endfunction
+"▶2 python
+if s:usepythondriver
+    function s:F.getupdates(repo, start)
+        let d={}
+        try
+            " XXX get_updates also modifies a:repo
+            " execute s:py 'import cProfile as profile'
+            " execute s:py 'profile.run("aurum.get_updates(vim.eval(''a:repo.path''), '.a:start.')", "python.profile")'
+            execute s:py 'aurum.get_updates(vim.eval("a:repo.path"), '.
+                        \                   a:start.')'
+        endtry
+        return d
+    endfunction
+    function s:F.updatetags(repo)
+        if empty(a:repo.changesets)
+            return
+        endif
+        let d={}
+        try
+            execute s:py 'aurum.get_tags(vim.eval("a:repo.path"))'
+        endtry
+        call s:F.updatewithtagsdict(a:repo.changesets, d)
+    endfunction
+"▶2 no python
+else
+    function s:F.getupdates(repo, start)
+        let r={}
+        let tip_hex=a:repo.functions.getrevhex(a:repo, 'tip')
+        if a:start
+            try
+                let oldtip=a:repo.functions.getcs(a:repo, a:start)
+                if tip_hex is# oldtip.hex
+                    return r
+                endif
+                let startrev=oldtip.rev
+            catch
+                let startrev=0
+            endtry
+        else
+            let startrev=0
+        endif
+        let r.startrev=startrev
+        let r.css=s:F.getcslist(a:repo, startrev, -1)
+        for key in ['tags', 'bookmarks']
+            let list=s:F.getkeylist(a:repo, key)
+            let r[key]={}
+            for [name, rev] in filter(copy(list), 'v:val[1]<'.a:start)
+                let r[key][name]=a:repo.mutable.cslist[rev].hex
+            endfor
+        endfor
+        let a:repo.csnum=a:start+len(r.css)
+        return r
+    endfunction
+    function s:F.updatetags(repo)
+        if empty(a:repo.changesets)
+            return
+        endif
+        let revmap={}
+        call map(copy(a:repo.changesets), 'extend(revmap, {v:val.rev : v:val})')
+        let d={}
+        for key in ['tags', 'bookmarks']
+            try
+                let list=s:F.getkeylist(a:repo, key)
+            catch
+                continue
+            endtry
+            let d[key]={}
+            let cd=d[key]
+            call map(copy(list), 'has_key(cd, v:val[1])?'.
+                        \           'add(cd[v:val[1]], v:val[0]):'.
+                        \           'extend(cd, {v:val[1] : [v:val[0]]})')
+            call map(values(cd), 'sort(v:val)')
+        endfor
+        call s:F.updatewithtagsdict(revmap, d)
+    endfunction
+endif
+"▶2 hg.updatechangesets
+" TODO test updating in cases of rollback
+function s:hg.updatechangesets(repo, ...)
+    if !(a:0 && a:1) && empty(a:repo.mutable.cslist)
+        return s:F.updatetags(a:repo)
+    endif
+    let d={}
+    let start=len(a:repo.mutable.cslist)-2
+    if start<0
+        let start=0
+    endif
+    " XXX getupdates may also modify repo
+    let d=s:F.getupdates(a:repo, start)
+    if empty(d)
+        return
+    endif
+    call map(d.css, 'extend(v:val, {"children": []})')
+    if !empty(a:repo.mutable.cslist)
+        call s:F.removechangesets(a:repo, d.startrev)
+    endif
+    for key in ['tags', 'bookmarks']
+        call map(a:repo.mutable.cslist, 'extend(v:val, {key : []})')
+        for [name, hex] in filter(items(d[key]),
+                    \             'has_key(a:repo.changesets, v:val[1])')
+            let cs=a:repo.changesets[hex]
+            let cs[key]+=[name]
+            call sort(cs[key])
+        endfor
+    endfor
+    let a:repo.mutable.cslist+=d.css
+    call s:F.addchangesets(a:repo, d.css)
+endfunction
+"▶1 hg.getrevhex :: repo, rev → rev(hex)
+if s:usepythondriver "▶2
+function s:hg.getrevhex(repo, rev)
+    try
+        execute s:py 'vim.command(''return "''+'.
+                    \      'aurum.g_cs(aurum.g_repo(vim.eval("a:repo.path")), '.
+                    \               'vim.eval("a:rev")).hex()+''"'')'
+    catch /\v^Frawor:/
+        throw v:exception
+    catch
+        call s:_f.throw('norev', a:rev, a:repo.path)
+    endtry
+endfunction
+else "▶2
+let s:getrevhextemplate='{node}'
+function s:hg.getrevhex(repo, rev)
+    if type(a:rev)==type('') && (has_key(a:repo.changesets, a:rev) ||
+                \                a:rev=~#'\v^[0-9a-f]{40}$')
+        return a:rev
+    elseif type(a:rev)==type(0) && a:rev<len(a:repo.mutable.cslist)
+        return a:repo.mutable.cslist[a:rev].hex
+    endif
+    let hex=get(s:F.hg(a:repo, 'log', [], {'template': s:getrevhextemplate,
+                \                               'rev': ''.a:rev}, 0, 'log'),0,0)
+    if hex is 0
+        call s:_f.throw('norev', a:rev, a:repo.path)
+    endif
+    return hex
+endfunction
+endif
+"▶1 hg.gettiphex :: repo → hex
+function s:hg.gettiphex(repo)
+    return a:repo.functions.getrevhex(a:repo, 'tip')
+endfunction
+"▶1 hg.getworkhex :: repo → hex
+function s:hg.getworkhex(repo)
+    return a:repo.functions.getrevhex(a:repo, '.')
+endfunction
+"▶1 hg.setcsprop :: repo, cs, propname → a
+if s:usepythondriver "▶2
+function s:hg.setcsprop(repo, cs, prop)
+    try
+        execute s:py 'aurum.get_cs_prop(vim.eval("a:repo.path"), '.
+                    \                  'vim.eval("a:cs.hex"), '.
+                    \                  'vim.eval("a:prop"))'
+    endtry
+endfunction
+else "▶2
+function s:hg.setcsprop(repo, cs, prop)
+    if a:prop is# 'allfiles'
+        let r=s:F.hg(a:repo, 'manifest', [], {'rev': a:cs.hex}, 0,
+                    \'csp', a:prop, a:cs.rev)[:-2]
+    elseif a:prop is# 'children'
+        " XXX str2nr('123:1f6de') will return number 123
+        let r=map(split(s:F.hg(a:repo, 'log', [],
+                    \          {'rev': a:cs.hex, 'template': '{children}'},
+                    \          0, 'csp', a:prop, a:cs.rev)[0]), 'str2nr(v:val)')
+        if empty(a:repo.mutable.cslist)
+            call map(r, 'a:repo.functions.getrevhex(a:repo, v:val)')
+        else
+            call map(r, 'a:repo.mutable.cslist[v:val].hex')
+        endif
+    endif
+    let a:cs[a:prop]=r
+    return r
+endfunction
+endif
+"▶1 hg.status :: repo[, rev1[, rev2[, files[, clean]]]] → {type : [file]}
+" type :: "modified" | "added" | "removed" | "deleted" | "unknown" | "ignored"
+"       | "clean"
+if s:usepythondriver "▶2
+function s:hg.status(repo, ...)
+    let revargs=join(map(copy(a:000), 'v:val is 0? "None": string(v:val)'), ',')
+    let d={}
+    try
+        execute s:py 'aurum.get_status(vim.eval("a:repo.path"), '.revargs.')'
+    endtry
+    return d
+endfunction
+else "▶2
+let s:statchars={
+            \'M': 'modified',
+            \'A': 'added',
+            \'R': 'removed',
+            \'!': 'deleted',
+            \'?': 'unknown',
+            \'I': 'ignored',
+            \'C': 'clean',
+        \}
+let s:initstatdct={}
+call map(values(s:statchars), 'extend(s:initstatdct, {v:val : []})')
+" TODO test whether zero revision may cause bugs in some commands
+function s:hg.status(repo, ...)
+    let args=[]
+    let kwargs={'modified': 1,
+                \  'added': 1,
+                \'removed': 1,
+                \'deleted': 1,
+                \'unknown': 1,
+                \'ignored': 1}
+    if (a:0>3 && a:4)
+        let kwargs.clean=1
+    endif
+    let reverse=0
+    if a:0
+        if a:1 is 0
+            if a:0>1 && a:2 isnot 0
+                let reverse=1
+            endif
+        else
+            let args+=['--rev', a:1]
+        endif
+        if a:0>1 && a:2 isnot 0
+            let args+=['--rev', a:2]
+        endif
+        if a:0>2 && !empty(a:3)
+            let args+=['--']+a:3
+        endif
+    endif
+    let slines=s:F.hg(a:repo, 'status', args, kwargs, 0, 'stat')[:-2]
+    if !empty(filter(copy(slines), '!has_key(s:statchars, v:val[0])'))
+        call s:_f.throw('statfail', a:repo.path, join(slines, "\n"))
+    endif
+    let r=deepcopy(s:initstatdct)
+    call map(copy(slines),'add(r[s:statchars[v:val[0]]],s:F.refile(v:val[2:]))')
+    if reverse
+        let [r.deleted, r.unknown]=[r.unknown, r.deleted]
+        let [r.added,   r.removed]=[r.removed, r.added  ]
+    endif
+    return r
+endfunction
+endif
+"▶1 hg.dirty :: repo, file → Bool
+if s:usepythondriver "▶2
+function s:hg.dirty(repo, file)
+    try
+        let r=0
+        execute s:py 'aurum.dirty(vim.eval("a:repo.path"), '.
+                    \            'vim.eval("a:file"))'
+        return r
+    endtry
+endfunction
+endif
+"▶1 hg.commit :: repo, message[, files[, user[, date[, closebranch[, force]]]]]
+function s:hg.commit(repo, message, ...)
+    let kwargs={}
+    let args=[]
+    if a:0
+        if !empty(a:1)
+            let args+=a:1
+            let kwargs.addremove=1
+        endif
+        if a:0>1 && !empty(a:2)
+            let kwargs.user=a:2
+        endif
+        if a:0>2 && !empty(a:3)
+            let kwargs.date=a:3
+        endif
+        if a:0>3 && !empty(a:4)
+            let kwargs.close_branch=1
+        endif
+    endif
+    return s:_r.utils.usefile(a:repo, a:message, 'logfile', 'message',
+                \             s:F.runcmd, args, kwargs)
+endfunction
+"▶1 hg.branch :: repo, branchname, force → + FS
+function s:hg.branch(repo, branch, force)
+    return s:F.runcmd(a:repo, 'branch', [a:branch], a:force ? {'force': 1} : {})
+endfunction
+"▶1 hg.label :: repo, type, label, rev, force, local → + FS
+function s:hg.label(repo, type, label, rev, force, local)
+    let kwargs={}
+    if a:force
+        let kwargs.force=1
+    endif
+    if a:type is# 'tag'
+        if a:local
+            let kwargs.local=1
+        endif
+        if a:rev is 0
+            let kwargs.remove=1
+        else