Repository: tyru/open-browser-github.vim Branch: master Commit: ac7c034e300f Files: 12 Total size: 47.4 KB Directory structure: gitextract_urosfd0w/ ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── autoload/ │ ├── openbrowser/ │ │ └── github.vim │ ├── vital/ │ │ ├── _open_browser_github/ │ │ │ ├── Data/ │ │ │ │ └── List.vim │ │ │ └── System/ │ │ │ └── Filepath.vim │ │ ├── _open_browser_github.vim │ │ └── open_browser_github.vital │ └── vital.vim ├── doc/ │ └── openbrowser-github.txt └── plugin/ └── openbrowser/ └── github.vim ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ doc/tags ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2013, Takuya Fujiwara All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ release: git archive HEAD plugin autoload doc --output=open-browser-github-$(shell git describe --tags HEAD).zip .PHONY: release ================================================ FILE: README.md ================================================ # open-browser-github.vim ## About Opens GitHub URL of current file, etc. from Vim. Also supports GitHub Enterprise. ## Install This plugin requires: * [open-browser.vim](https://github.com/tyru/open-browser.vim) * `git` command in your PATH ## Usage There are 4 commands. ### `:OpenGithubFile` Opens a specific file in github.com repository(it also opens in the current branch by default). ```vimL " Opens current files URL in github.com :OpenGithubFile " Opens current files highlighted place in github.com :'<,'>OpenGithubFile " Opens a specific file in github.com :OpenGithubFile PATH/TO/FILE ``` ### `:OpenGithubIssue` Opens a specific Issue. ```vimL " Opens current repositories Issue #1 :OpenGithubIssue 1 " Opens a specific repositories Issue #1 :OpenGithubIssue 1 tyru/open-browser.vim " Opens current repositories Issue List :OpenGithubIssue " Opens a specific repositories Issue list :OpenGithubIssue tyru/open-browser.vim ``` ### `:OpenGithubPullReq` This command opens `/pulls` page when it has no argument. Otherwise, it does entirely the same thing as `:OpenGithubIssue` since GitHub redirects `/issues/1` to `/pull/1` if #1 is a Pull Request. ### `:OpenGithubProject` Opens a project page. ```vimL " Opens current opening file's repository. " ex) https://{hostname}/{user}/{name} :OpenGithubProject " Opens current opening file's repository. " ex) https://{hostname}/tyru/open-browser.vim :OpenGithubProject tyru/open-browser.vim ``` ## GitHub Enterprise setting ### If you have `hub` command If you have [hub command](https://github.com/github/hub) in your PATH, `openbrowser-github` executes the following command: ``` hub browse -u -- {path} ``` And it will open the returned (output) URL. ### If you _don't_ have `hub` command If you don't have `hub` command in your PATH, `openbrowser-github` tries to get each part of URL from the following gitconfig key: * hub.host You can specify GitHub Enterprise repository URL by setting above keys in gitconfig. For example, you can set `hub.host` by executing the following command in your git repository which you want to specify GitHub Enterprise repository URL. ``` git config --local hub.host my.git.org ``` ================================================ FILE: autoload/openbrowser/github.vim ================================================ " vim:foldmethod=marker:fen: scriptencoding utf-8 " Saving 'cpoptions' {{{ let s:save_cpo = &cpo set cpo&vim " }}} let s:V = vital#of('open_browser_github') let s:Filepath = s:V.import('System.Filepath') let s:List = s:V.import('Data.List') unlet s:V function! openbrowser#github#load() abort " dummy function to load this script. endfunction function! openbrowser#github#file(args, rangegiven, firstlnum, lastlnum) abort let file = s:resolve(expand(get(a:args, 0, '%'))) let worktree = s:lookup_git_worktree(file) let lastlnum = max([a:firstlnum, a:lastlnum]) " a:firstlnum could be bigger in NeoVim call s:call_with_temp_dir(worktree, 's:cmd_file', [a:args, a:rangegiven, a:firstlnum, lastlnum]) endfunction " Opens a specific file in github.com repository. function! s:cmd_file(args, rangegiven, firstlnum, lastlnum) abort let rangegiven = a:rangegiven || get(g:, 'openbrowser_github_select_current_line', 0) let [path, err] = s:parse_cmd_file_args(a:args, rangegiven, a:firstlnum, a:lastlnum) if err !=# '' call s:error(err) return endif if executable('hub') let url = s:hub('browse', '-u', '--', path) else let [url, err] = s:get_url_from_git({'path': path}) if err !=# '' call s:error(err) return endif endif if !s:url_exists(url) && input( \ "Maybe you are opening a URL which is not git-push'ed yet. OK?[y/n]: " \) !~? '^\%[YES]$' " TODO: Retry return endif return openbrowser#open(url) endfunction " * :OpenGithubFile [{path}] function! s:parse_cmd_file_args(args, rangegiven, firstlnum, lastlnum) abort let file = s:resolve(expand(empty(a:args) ? '%' : a:args[0])) if !filereadable(file) if a:0 is 0 return ['', 'current buffer is not a file.'] else return ['', printf('''%s'' is not readable.', file)] endif endif let relpath = s:get_repos_relpath(file) if g:openbrowser_github_always_used_branch !=# '' let branch = g:openbrowser_github_always_used_branch elseif g:openbrowser_github_always_use_commit_hash let branch = s:git('rev-parse', 'HEAD') else " When working tree is detached state, " branch becomes commit hash. let head_ref = s:git('symbolic-ref', '--short', '-q', 'HEAD') let is_detached_state = (head_ref ==# '') if is_detached_state let branch = s:git('rev-parse', 'HEAD') else let branch = head_ref endif endif if a:rangegiven let lnum = '#L'.a:firstlnum \ .(a:firstlnum is a:lastlnum ? '' : '-L'.a:lastlnum) else let lnum = '' endif " Check input values. if branch ==# '' return ['', 'Could not detect current branch name.'] endif if relpath ==# '' return ['', 'Could not detect relative path of repository.'] endif let path = 'blob/' . branch . '/' . relpath . lnum return [path, ''] endfunction function! s:get_url_from_git(repoinfo) abort let host = s:get_github_host() let user = get(a:repoinfo, 'user', '') let repos = get(a:repoinfo, 'repos', '') let path = get(a:repoinfo, 'path', '') if user ==# '' || repos ==# '' " May prompt user to choose which repos is used. try let github_repos = \ s:detect_github_repos_from_git_remote(host) catch /^INVALID INDEX$/ return ['', 'canceled or invalid GitHub URL was selected.'] endtry let user = get(github_repos, 'user', '') let repos = get(github_repos, 'repos', '') endif if user ==# '' return ['', 'Could not detect repos user.'] endif if repos ==# '' return ['', 'Could not detect current repos name on github.'] endif let url = 'https://' . host . '/' . user . '/' . repos . '/' . path return [url, ''] endfunction function! s:url_exists(url) abort if g:openbrowser_github_url_exists_check ==# 'ignore' return 1 endif if !executable('curl') call s:warn('You must have ''curl'' command to check whether the opening URL exists.') call s:warn('You can suppress this check by writing the following ' \ . 'config in your vimrc (:help g:openbrowser_github_url_exists_check).') call s:warn(' let g:openbrowser_github_url_exists_check = ''ignore''') call input('Press ENTER to continue...') return 1 endif let cmdline = 'curl -k -LI "' . a:url . '"' let headers = split(system(cmdline), '\n') if v:shell_error call s:warn(cmdline) call s:warn('curl returned error code: ' . v:shell_error) return 1 endif let re = '^status:' let status_line = get(filter(headers, 'v:val =~? re'), 0, '') if status_line ==# '' call s:warn(cmdline) call s:warn('curl received a response without ''Status'' header.') return 1 endif return status_line =~? '^status:\s*2' endfunction let s:TYPE_ISSUE = 0 function! openbrowser#github#issue(args) abort let file = expand('%') let worktree = s:lookup_git_worktree(file) call s:call_with_temp_dir(worktree, 's:cmd_open_url', [a:args, s:TYPE_ISSUE]) endfunction let s:TYPE_PULLREQ = 1 function! openbrowser#github#pullreq(args) abort let file = expand('%') let worktree = s:lookup_git_worktree(file) call s:call_with_temp_dir(worktree, 's:cmd_open_url', [a:args, s:TYPE_PULLREQ]) endfunction let s:TYPE_PROJECT = 2 function! openbrowser#github#project(args) abort let file = expand('%') let worktree = s:lookup_git_worktree(file) call s:call_with_temp_dir(worktree, 's:cmd_open_url', [a:args, s:TYPE_PROJECT]) endfunction let s:TYPE_COMMIT = 3 function! openbrowser#github#commit(args) abort let file = expand('%') let worktree = s:lookup_git_worktree(file) call s:call_with_temp_dir(worktree, 's:cmd_open_url', [a:args, s:TYPE_COMMIT]) endfunction " Opens a specific Issue/Pullreq/Project. function! s:cmd_open_url(...) abort try let repoinfo = call('s:parse_cmd_open_url_args', a:000) catch call s:error(v:exception) return endtry if executable('hub') let url = s:hub('browse', '-u', '--', repoinfo.path) else let [url, err] = s:get_url_from_git(repoinfo) if err !=# '' call s:error(err) return endif endif return openbrowser#open(url) endfunction " * :OpenGithubIssue " * :OpenGithubIssue [#]{number} [{user}/{repos}] " * :OpenGithubIssue {user}/{repos} " * :OpenGithubPullReq " * :OpenGithubPullReq [#]{number} [{user}/{repos}] " * :OpenGithubPullReq #{branch} [{user}/{repos}] " * :OpenGithubPullReq {user}/{repos} " * :OpenGithubProject [{user}/{repos}] " * :OpenGithubCommit {commit hash} [{user}/{repos}] function! s:parse_cmd_open_url_args(args, type) abort if a:type ==# s:TYPE_ISSUE " ex) '#1', '1' let nr = matchstr(get(a:args, 0, ''), '^#\?\zs\d\+\ze$') if nr !=# '' let path = 'issues/' . nr else let path = 'issues' endif " If the argument of repository was given and valid format, get user and repos. let idx = nr ==# '' ? 0 : 1 let m = matchlist(get(a:args, idx, ''), \ '^\([^/]\+\)/\([^/]\+\)$') let [user, repos] = !empty(m) ? m[1:2] : ['', ''] elseif a:type ==# s:TYPE_PULLREQ " ex) '#1', '1', '#branch_name_of_pull_request' let nr_or_branch = matchstr(get(a:args, 0, ''), '^\%(#\?\zs\d\+\ze\|#\zs.\+\ze\)$') if nr_or_branch !=# '' let path = 'pull/' . nr_or_branch else let path = 'pulls' endif " If the argument of repository was given and valid format, get user and repos. let idx = nr_or_branch ==# '' ? 0 : 1 let m = matchlist(get(a:args, idx, ''), \ '^\([^/]\+\)/\([^/]\+\)$') let [user, repos] = !empty(m) ? m[1:2] : ['', ''] elseif a:type ==# s:TYPE_PROJECT let path = '' let m = matchlist(get(a:args, 0, ''), \ '^\([^/]\+\)/\([^/]\+\)$') let [user, repos] = !empty(m) ? m[1:2] : ['', ''] else " if a:type ==# s:TYPE_COMMIT if len(a:args) > 1 let hash = a:args[0] else let hash = s:git('rev-parse', a:args[0]) if v:shell_error throw "'" . a:args[0] . "' is not valid commit hash" endif endif let path = 'commit/' . hash let m = matchlist(get(a:args, 1, ''), \ '^\([^/]\+\)/\([^/]\+\)$') let [user, repos] = !empty(m) ? m[1:2] : ['', ''] endif return { \ 'path': path, \ 'user': user, \ 'repos': repos, \} endfunction function! s:call_with_temp_dir(dir, funcname, args) abort let haslocaldir = haslocaldir() let cwd = getcwd() " a:dir could be empty string " when specifying opening repos. " e.g.) " * :OpenGithubFile path/to/file " * :OpenGithubIssue [#]{number} {user}/{repos} if a:dir !=# '' && a:dir !=# cwd execute 'lcd' a:dir endif try return call(a:funcname, a:args) finally if a:dir !=# cwd execute (haslocaldir ? 'lcd' : 'cd') cwd endif endtry endfunction function! s:parse_github_remote_url(github_host) abort let host_re = escape(a:github_host, '.') let gh_host_re = 'github\.com' " ex) ssh_re_fmt also supports 'ssh://' protocol. (#10) " - git@github.com:tyru/open-github-browser.vim " - ssh://git@github.com/tyru/open-github-browser.vim let ssh_re_fmt = 'git@%s[:/]\([^/]\+\)/\([^/]\+\)\s' let ssh2_re_fmt = '\s%s[:/]\([^/]\+\)/\([^/]\+\)\s' let ssh3_re_fmt = 'ssh://%s/\([^/]\+\)/\([^/]\+\)\s' let git_re_fmt = 'git://%s/\([^/]\+\)/\([^/]\+\)\s' let https_re_fmt = 'https\?://%s/\([^/]\+\)/\([^/]\+\)\s' let ssh_re = printf(ssh_re_fmt, host_re) let ssh2_re = printf(ssh2_re_fmt, host_re) let ssh3_re = printf(ssh3_re_fmt, host_re) let git_re = printf(git_re_fmt, host_re) let https_re = printf(https_re_fmt, host_re) let gh_ssh_re = printf(ssh_re_fmt, gh_host_re) let gh_ssh2_re = printf(ssh2_re_fmt, gh_host_re) let gh_ssh3_re = printf(ssh3_re_fmt, gh_host_re) let gh_git_re = printf(git_re_fmt, gh_host_re) let gh_https_re = printf(https_re_fmt, gh_host_re) let matched = [] for line in s:git_lines('remote', '-v') " Even if host is not 'github.com', " parse also 'github.com'. for re in [ssh_re, ssh2_re, ssh3_re, git_re, https_re] + \ (a:github_host !=# 'github.com' ? \ [gh_ssh_re, gh_ssh2_re, gh_ssh3_re, gh_git_re, gh_https_re] : []) let m = matchlist(line, re) if !empty(m) call add(matched, { \ 'user': m[1], \ 'repos': substitute(m[2], '\.git$', '', ''), \}) endif endfor endfor return matched endfunction " Detect user name and repos name from 'git remote -v' output. " * Duplicated candidates of user and repos are removed. " * Returns empty Dictionary if no valid GitHub repos are found. " * Returns an Dictionary with 'user' and 'repos' " if exact 1 repos is found. " * Prompt a user to choose which repos " if 2 or more repos are found. " * Throws "INVALID INDEX" if invalid input was given. function! s:detect_github_repos_from_git_remote(github_host) abort let github_urls = s:parse_github_remote_url(a:github_host) let github_urls = s:List.uniq_by(github_urls, 'v:val.user."/".v:val.repos') let NONE = {} if len(github_urls) ==# 0 return NONE elseif len(github_urls) ==# 1 return github_urls[0] else " Prompt which GitHub URL. let GITHUB_URL_FORMAT = 'https://%s/%s/%s' let list = ['Which GitHub repository?'] for i in range(len(github_urls)) let url = printf(GITHUB_URL_FORMAT, \ a:github_host, \ github_urls[i].user, \ github_urls[i].repos) call add(list, (i+1).'. '.url) endfor let index = inputlist(list) if 1 <=# index && index <=# len(github_urls) return github_urls[index-1] else throw 'INVALID INDEX' endif endif endfunction function! s:get_repos_relpath(file) abort let relpath = '' if s:Filepath.is_relative(a:file) let dir = s:git('rev-parse', '--show-prefix') let dir = dir !=# '' ? dir.'/' : '' let relpath = dir.a:file else let relpath = s:lookup_git_relpath(a:file) endif let relpath = substitute(relpath, '\', '/', 'g') let relpath = substitute(relpath, '/\{2,}', '/', 'g') return relpath endfunction function! s:lookup_git_relpath(path) abort return get(s:split_repos_path(a:path), 1, '') endfunction function! s:lookup_git_worktree(path) abort return get(s:split_repos_path(a:path), 0, '') endfunction " Returns [git worktree, relative path] when git dir is found. " Otherwise, returns empty List. function! s:split_repos_path(path, ...) abort let parent = s:Filepath.dirname(a:path) let basename = s:Filepath.basename(a:path) let removed_path = a:0 ? a:1 : '' if a:path ==# parent " a:path is root directory. not found. return [] elseif s:is_git_worktree(a:path) return [a:path, removed_path] else if removed_path ==# '' let removed_path = basename else let removed_path = s:Filepath.join(basename, removed_path) endif return s:split_repos_path(parent, removed_path) endif endfunction function! s:is_git_worktree(path) abort " .git may be a file when its repository is a submodule. let gitdir = s:Filepath.join(a:path, '.git') return isdirectory(gitdir) || filereadable(gitdir) endfunction " Enterprise GitHub is supported. " ('hub' command is using this config key) function! s:get_github_host() abort let url = s:git('config', '--get', 'hub.host') return url !=# '' ? url : 'github.com' endfunction " Default value is 1 (use vimproc) if get(g:, 'openbrowser_use_vimproc', 1) \ && globpath(&rtp, 'autoload/vimproc.vim') !=# '' function! s:git(...) abort return s:trim(vimproc#system(['git'] + a:000)) endfunction function! s:hub(...) abort return s:trim(vimproc#system(['hub'] + a:000)) endfunction else function! s:git(...) abort return s:trim(system(join(['git'] + a:000, ' '))) endfunction function! s:hub(...) abort return s:trim(system(join(['hub'] + a:000, ' '))) endfunction endif function! s:git_lines(...) abort let output = call('s:git', a:000) let keepempty = 1 return split(output, '\n', keepempty) endfunction function! s:trim(str) abort let str = a:str let str = substitute(str, '^[ \t\n]\+', '', 'g') let str = substitute(str, '[ \t\n]\+$', '', 'g') return str endfunction function! s:resolve(path) abort return exists('*resolve') ? resolve(a:path) : a:path endfunction function! s:echomsg(msg, hl) abort execute 'echohl' a:hl try echomsg '[openbrowser-github]' a:msg finally echohl None endtry endfunction function! s:warn(msg) abort call s:echomsg(a:msg, 'WarningMsg') endfunction function! s:error(msg) abort call s:echomsg(a:msg, 'ErrorMsg') endfunction " Restore 'cpoptions' {{{ let &cpo = s:save_cpo " }}} ================================================ FILE: autoload/vital/_open_browser_github/Data/List.vim ================================================ " Utilities for list. let s:save_cpo = &cpo set cpo&vim function! s:pop(list) return remove(a:list, -1) endfunction function! s:push(list, val) call add(a:list, a:val) return a:list endfunction function! s:shift(list) return remove(a:list, 0) endfunction function! s:unshift(list, val) return insert(a:list, a:val) endfunction function! s:cons(x, xs) return [a:x] + a:xs endfunction function! s:conj(xs, x) return a:xs + [a:x] endfunction " Removes duplicates from a list. function! s:uniq(list) return s:uniq_by(a:list, 'v:val') endfunction " Removes duplicates from a list. function! s:uniq_by(list, f) let list = map(copy(a:list), printf('[v:val, %s]', a:f)) let i = 0 let seen = {} while i < len(list) let key = string(list[i][1]) if has_key(seen, key) call remove(list, i) else let seen[key] = 1 let i += 1 endif endwhile return map(list, 'v:val[0]') endfunction function! s:clear(list) if !empty(a:list) unlet! a:list[0 : len(a:list) - 1] endif return a:list endfunction " Concatenates a list of lists. " XXX: Should we verify the input? function! s:concat(list) let memo = [] for Value in a:list let memo += Value endfor return memo endfunction " Take each elements from lists to a new list. function! s:flatten(list, ...) let limit = a:0 > 0 ? a:1 : -1 let memo = [] if limit == 0 return a:list endif let limit -= 1 for Value in a:list let memo += \ type(Value) == type([]) ? \ s:flatten(Value, limit) : \ [Value] unlet! Value endfor return memo endfunction " Sorts a list with expression to compare each two values. " a:a and a:b can be used in {expr}. function! s:sort(list, expr) if type(a:expr) == type(function('function')) return sort(a:list, a:expr) endif let s:expr = a:expr return sort(a:list, 's:_compare') endfunction function! s:_compare(a, b) return eval(s:expr) endfunction " Sorts a list using a set of keys generated by mapping the values in the list " through the given expr. " v:val is used in {expr} function! s:sort_by(list, expr) let pairs = map(a:list, printf('[v:val, %s]', a:expr)) return map(s:sort(pairs, \ 'a:a[1] ==# a:b[1] ? 0 : a:a[1] ># a:b[1] ? 1 : -1'), 'v:val[0]') endfunction " Returns a maximum value in {list} through given {expr}. " Returns 0 if {list} is empty. " v:val is used in {expr} function! s:max_by(list, expr) if empty(a:list) return 0 endif let list = map(copy(a:list), a:expr) return a:list[index(list, max(list))] endfunction " Returns a minimum value in {list} through given {expr}. " Returns 0 if {list} is empty. " v:val is used in {expr} " FIXME: -0x80000000 == 0x80000000 function! s:min_by(list, expr) return s:max_by(a:list, '-(' . a:expr . ')') endfunction " Returns List of character sequence between [a:from, a:to] " e.g.: s:char_range('a', 'c') returns ['a', 'b', 'c'] function! s:char_range(from, to) return map( \ range(char2nr(a:from), char2nr(a:to)), \ 'nr2char(v:val)' \) endfunction " Returns true if a:list has a:value. " Returns false otherwise. function! s:has(list, value) return index(a:list, a:value) isnot -1 endfunction " Returns true if a:list[a:index] exists. " Returns false otherwise. " NOTE: Returns false when a:index is negative number. function! s:has_index(list, index) " Return true when negative index? " let index = a:index >= 0 ? a:index : len(a:list) + a:index return 0 <= a:index && a:index < len(a:list) endfunction " similar to Haskell's Data.List.span function! s:span(f, xs) let border = len(a:xs) for i in range(len(a:xs)) if !eval(substitute(a:f, 'v:val', string(a:xs[i]), 'g')) let border = i break endif endfor return border == 0 ? [[], copy(a:xs)] : [a:xs[: border - 1], a:xs[border :]] endfunction " similar to Haskell's Data.List.break function! s:break(f, xs) return s:span(printf('!(%s)', a:f), a:xs) endfunction " similar to Haskell's Data.List.takeWhile function! s:take_while(f, xs) return s:span(a:f, a:xs)[0] endfunction " similar to Haskell's Data.List.partition function! s:partition(f, xs) return [filter(copy(a:xs), a:f), filter(copy(a:xs), '!(' . a:f . ')')] endfunction " similar to Haskell's Prelude.all function! s:all(f, xs) return !s:any(printf('!(%s)', a:f), a:xs) endfunction " similar to Haskell's Prelude.any function! s:any(f, xs) return !empty(filter(map(copy(a:xs), a:f), 'v:val')) endfunction " similar to Haskell's Prelude.and function! s:and(xs) return s:all('v:val', a:xs) endfunction " similar to Haskell's Prelude.or function! s:or(xs) return s:any('v:val', a:xs) endfunction function! s:map_accum(expr, xs, init) let memo = [] let init = a:init for x in a:xs let expr = substitute(a:expr, 'v:memo', init, 'g') let expr = substitute(expr, 'v:val', x, 'g') let [tmp, init] = eval(expr) call add(memo, tmp) endfor return memo endfunction " similar to Haskell's Prelude.foldl function! s:foldl(f, init, xs) let memo = a:init for x in a:xs let expr = substitute(a:f, 'v:val', string(x), 'g') let expr = substitute(expr, 'v:memo', string(memo), 'g') unlet memo let memo = eval(expr) endfor return memo endfunction " similar to Haskell's Prelude.foldl1 function! s:foldl1(f, xs) if len(a:xs) == 0 throw 'foldl1' endif return s:foldl(a:f, a:xs[0], a:xs[1:]) endfunction " similar to Haskell's Prelude.foldr function! s:foldr(f, init, xs) return s:foldl(a:f, a:init, reverse(copy(a:xs))) endfunction " similar to Haskell's Prelude.fold11 function! s:foldr1(f, xs) if len(a:xs) == 0 throw 'foldr1' endif return s:foldr(a:f, a:xs[-1], a:xs[0:-2]) endfunction " similar to python's zip() function! s:zip(...) return map(range(min(map(copy(a:000), 'len(v:val)'))), "map(copy(a:000), 'v:val['.v:val.']')") endfunction " similar to zip(), but goes until the longer one. function! s:zip_fill(xs, ys, filler) if empty(a:xs) && empty(a:ys) return [] elseif empty(a:ys) return s:cons([a:xs[0], a:filler], s:zip_fill(a:xs[1 :], [], a:filler)) elseif empty(a:xs) return s:cons([a:filler, a:ys[0]], s:zip_fill([], a:ys[1 :], a:filler)) else return s:cons([a:xs[0], a:ys[0]], s:zip_fill(a:xs[1 :], a:ys[1: ], a:filler)) endif endfunction " Inspired by Ruby's with_index method. function! s:with_index(list, ...) let base = a:0 > 0 ? a:1 : 0 return s:zip(a:list, range(base, len(a:list)+base-1)) endfunction " similar to Ruby's detect or Haskell's find. " TODO spec and doc function! s:find(list, default, f) for x in a:list if eval(substitute(a:f, 'v:val', string(x), 'g')) return x endif endfor return a:default endfunction " Return non-zero if a:list1 and a:list2 have any common item(s). " Return zero otherwise. function! s:has_common_items(list1, list2) return !empty(filter(copy(a:list1), 'index(a:list2, v:val) isnot -1')) endfunction " similar to Ruby's group_by. function! s:group_by(xs, f) let result = {} let list = map(copy(a:xs), printf('[v:val, %s]', a:f)) for x in list let Val = x[0] let key = type(x[1]) !=# type('') ? string(x[1]) : x[1] if has_key(result, key) call add(result[key], Val) else let result[key] = [Val] endif unlet Val endfor return result endfunction let &cpo = s:save_cpo unlet s:save_cpo " vim:set et ts=2 sts=2 sw=2 tw=0: ================================================ FILE: autoload/vital/_open_browser_github/System/Filepath.vim ================================================ " You should check the following related builtin functions. " fnamemodify() " resolve() " simplify() let s:save_cpo = &cpo set cpo&vim let s:path_sep_pattern = (exists('+shellslash') ? '[\\/]' : '/') . '\+' let s:is_windows = has('win16') || has('win32') || has('win64') || has('win95') let s:is_cygwin = has('win32unix') let s:is_mac = !s:is_windows && !s:is_cygwin \ && (has('mac') || has('macunix') || has('gui_macvim') || \ (!isdirectory('/proc') && executable('sw_vers'))) " Get the directory separator. function! s:separator() return fnamemodify('.', ':p')[-1 :] endfunction " Get the path separator. let s:path_separator = s:is_windows ? ';' : ':' function! s:path_separator() return s:path_separator endfunction " Get the path extensions function! s:path_extensions() if !exists('s:path_extensions') if s:is_windows if exists('$PATHEXT') let pathext = $PATHEXT else " get default PATHEXT let pathext = matchstr(system('set pathext'), '^pathext=\zs.*\ze\n', 'i') endif let s:path_extensions = map(split(pathext, s:path_separator), 'tolower(v:val)') elseif s:is_cygwin " cygwin is not use $PATHEXT let s:path_extensions = ['', '.exe'] else let s:path_extensions = [''] endif endif return s:path_extensions endfunction " Convert all directory separators to "/". function! s:unify_separator(path) return substitute(a:path, s:path_sep_pattern, '/', 'g') endfunction " Get the full path of command. if exists('*exepath') function! s:which(str) return exepath(a:str) endfunction else function! s:which(command, ...) let pathlist = a:command =~# s:path_sep_pattern ? [''] : \ !a:0 ? split($PATH, s:path_separator) : \ type(a:1) == type([]) ? copy(a:1) : \ split(a:1, s:path_separator) let pathext = s:path_extensions() if index(pathext, '.' . tolower(fnamemodify(a:command, ':e'))) != -1 let pathext = [''] endif let dirsep = s:separator() for dir in pathlist let head = dir ==# '' ? '' : dir . dirsep for ext in pathext let full = fnamemodify(head . a:command . ext, ':p') if filereadable(full) if s:is_case_tolerant() let full = glob(substitute( \ toupper(full), '\u:\@!', '[\0\L\0]', 'g'), 1) endif if full != '' return full endif endif endfor endfor return '' endfunction endif " Split the path with directory separator. " Note that this includes the drive letter of MS Windows. function! s:split(path) return split(a:path, s:path_sep_pattern) endfunction " Join the paths. " join('foo', 'bar') => 'foo/bar' " join('foo/', 'bar') => 'foo/bar' " join('/foo/', ['bar', 'buz/']) => '/foo/bar/buz/' function! s:join(...) let sep = s:separator() let path = '' for part in a:000 let path .= sep . \ (type(part) is type([]) ? call('s:join', part) : \ part) unlet part endfor return substitute(path[1 :], s:path_sep_pattern, sep, 'g') endfunction " Check if the path is absolute path. if s:is_windows function! s:is_absolute(path) return a:path =~? '^[a-z]:[/\\]' endfunction else function! s:is_absolute(path) return a:path[0] ==# '/' endfunction endif function! s:is_relative(path) return !s:is_absolute(a:path) endfunction " Return the parent directory of the path. " NOTE: fnamemodify(path, ':h') does not return the parent directory " when path[-1] is the separator. function! s:dirname(path) let path = a:path let orig = a:path let path = s:remove_last_separator(path) if path == '' return orig " root directory endif let path = fnamemodify(path, ':h') return path endfunction " Return the basename of the path. " NOTE: fnamemodify(path, ':h') does not return basename " when path[-1] is the separator. function! s:basename(path) let path = a:path let orig = a:path let path = s:remove_last_separator(path) if path == '' return orig " root directory endif let path = fnamemodify(path, ':t') return path endfunction " Remove the separator at the end of a:path. function! s:remove_last_separator(path) let sep = s:separator() let pat = (sep == '\' ? '\\' : '/') . '\+$' return substitute(a:path, pat, '', '') endfunction " Return true if filesystem ignores alphabetic case of a filename. " Return false otherwise. let s:is_case_tolerant = filereadable(expand(':r') . '.VIM') function! s:is_case_tolerant() return s:is_case_tolerant endfunction let &cpo = s:save_cpo unlet s:save_cpo " vim:set et ts=2 sts=2 sw=2 tw=0: ================================================ FILE: autoload/vital/_open_browser_github.vim ================================================ let s:self_version = expand(':t:r') " Note: The extra argument to globpath() was added in Patch 7.2.051. let s:globpath_third_arg = v:version > 702 || v:version == 702 && has('patch51') let s:loaded = {} function! s:import(name, ...) let target = {} let functions = [] for a in a:000 if type(a) == type({}) let target = a elseif type(a) == type([]) let functions = a endif unlet a endfor let module = s:_import(a:name) if empty(functions) call extend(target, module, 'keep') else for f in functions if has_key(module, f) && !has_key(target, f) let target[f] = module[f] endif endfor endif return target endfunction function! s:load(...) dict for arg in a:000 let [name; as] = type(arg) == type([]) ? arg[: 1] : [arg, arg] let target = split(join(as, ''), '\W\+') let dict = self while 1 <= len(target) let ns = remove(target, 0) if !has_key(dict, ns) let dict[ns] = {} endif if type(dict[ns]) == type({}) let dict = dict[ns] else unlet dict break endif endwhile if exists('dict') call extend(dict, s:_import(name)) endif unlet arg endfor return self endfunction function! s:unload() let s:loaded = {} endfunction function! s:exists(name) return s:_get_module_path(a:name) !=# '' endfunction function! s:search(pattern) let paths = s:_vital_files(a:pattern) let modules = sort(map(paths, 's:_file2module(v:val)')) return s:_uniq(modules) endfunction function! s:expand_modules(entry, all) if type(a:entry) == type([]) let candidates = s:_concat(map(copy(a:entry), 's:search(v:val)')) if empty(candidates) throw printf('vital: Any of module %s is not found', string(a:entry)) endif if eval(join(map(copy(candidates), 'has_key(a:all, v:val)'), '+')) let modules = [] else let modules = [candidates[0]] endif else let modules = s:search(a:entry) if empty(modules) throw printf('vital: Module %s is not found', a:entry) endif endif call filter(modules, '!has_key(a:all, v:val)') for module in modules let a:all[module] = 1 endfor return modules endfunction function! s:_import(name) if type(a:name) == type(0) return s:_build_module(a:name) endif let path = s:_get_module_path(a:name) if path ==# '' throw 'vital: module not found: ' . a:name endif let sid = s:_get_sid_by_script(path) if !sid try execute 'source' fnameescape(path) catch /^Vim\%((\a\+)\)\?:E484/ throw 'vital: module not found: ' . a:name catch /^Vim\%((\a\+)\)\?:E127/ " Ignore. endtry let sid = s:_get_sid_by_script(path) endif return s:_build_module(sid) endfunction function! s:_get_module_path(name) if s:_is_absolute_path(a:name) && filereadable(a:name) return a:name endif if a:name ==# '' let paths = [s:self_file] elseif a:name =~# '\v^\u\w*%(\.\u\w*)*$' let paths = s:_vital_files(a:name) else throw 'vital: Invalid module name: ' . a:name endif call filter(paths, 'filereadable(expand(v:val, 1))') let path = get(paths, 0, '') return path !=# '' ? path : '' endfunction function! s:_get_sid_by_script(path) let path = s:_unify_path(a:path) for line in filter(split(s:_redir('scriptnames'), "\n"), \ 'stridx(v:val, s:self_version) > 0') let list = matchlist(line, '^\s*\(\d\+\):\s\+\(.\+\)\s*$') if !empty(list) && s:_unify_path(list[2]) ==# path return list[1] - 0 endif endfor return 0 endfunction function! s:_file2module(file) let filename = fnamemodify(a:file, ':p:gs?[\\/]\+?/?') let tail = matchstr(filename, 'autoload/vital/_\w\+/\zs.*\ze\.vim$') return join(split(tail, '[\\/]\+'), '.') endfunction if filereadable(expand(':r') . '.VIM') " resolve() is slow, so we cache results. let s:_unify_path_cache = {} " Note: On windows, vim can't expand path names from 8.3 formats. " So if getting full path via and $HOME was set as 8.3 format, " vital load duplicated scripts. Below's :~ avoid this issue. function! s:_unify_path(path) if has_key(s:_unify_path_cache, a:path) return s:_unify_path_cache[a:path] endif let value = tolower(fnamemodify(resolve(fnamemodify( \ a:path, ':p')), ':~:gs?[\\/]\+?/?')) let s:_unify_path_cache[a:path] = value return value endfunction else function! s:_unify_path(path) return resolve(fnamemodify(a:path, ':p:gs?[\\/]\+?/?')) endfunction endif if s:globpath_third_arg function! s:_runtime_files(path) return split(globpath(&runtimepath, a:path, 1), "\n") endfunction else function! s:_runtime_files(path) return split(globpath(&runtimepath, a:path), "\n") endfunction endif let s:_vital_files_cache_runtimepath = '' let s:_vital_files_cache = [] function! s:_vital_files(pattern) if s:_vital_files_cache_runtimepath !=# &runtimepath let path = printf('autoload/vital/%s/**/*.vim', s:self_version) let s:_vital_files_cache = s:_runtime_files(path) let mod = ':p:gs?[\\/]\+?/?' call map(s:_vital_files_cache, 'fnamemodify(v:val, mod)') let s:_vital_files_cache_runtimepath = &runtimepath endif let target = substitute(a:pattern, '\.', '/', 'g') let target = substitute(target, '\*', '[^/]*', 'g') let regexp = printf('autoload/vital/%s/%s.vim', s:self_version, target) return filter(copy(s:_vital_files_cache), 'v:val =~# regexp') endfunction " Copy from System.Filepath if has('win16') || has('win32') || has('win64') function! s:_is_absolute_path(path) return a:path =~? '^[a-z]:[/\\]' endfunction else function! s:_is_absolute_path(path) return a:path[0] ==# '/' endfunction endif function! s:_build_module(sid) if has_key(s:loaded, a:sid) return copy(s:loaded[a:sid]) endif let functions = s:_get_functions(a:sid) let prefix = '' . a:sid . '_' let module = {} for func in functions let module[func] = function(prefix . func) endfor if has_key(module, '_vital_loaded') let V = vital#{s:self_version}#new() if has_key(module, '_vital_depends') let all = {} let modules = \ s:_concat(map(module._vital_depends(), \ 's:expand_modules(v:val, all)')) call call(V.load, modules, V) endif try call module._vital_loaded(V) catch " FIXME: Show an error message for debug. endtry endif if !get(g:, 'vital_debug', 0) call filter(module, 'v:key =~# "^\\a"') endif let s:loaded[a:sid] = module return copy(module) endfunction if exists('+regexpengine') function! s:_get_functions(sid) let funcs = s:_redir(printf("function /\\%%#=2^\%d_", a:sid)) let map_pat = '' . a:sid . '_\zs\w\+' return map(split(funcs, "\n"), 'matchstr(v:val, map_pat)') endfunction else function! s:_get_functions(sid) let prefix = '' . a:sid . '_' let funcs = s:_redir('function') let filter_pat = '^\s*function ' . prefix let map_pat = prefix . '\zs\w\+' return map(filter(split(funcs, "\n"), \ 'stridx(v:val, prefix) > 0 && v:val =~# filter_pat'), \ 'matchstr(v:val, map_pat)') endfunction endif if exists('*uniq') function! s:_uniq(list) return uniq(a:list) endfunction else function! s:_uniq(list) let i = len(a:list) - 1 while 0 < i if a:list[i] ==# a:list[i - 1] call remove(a:list, i) let i -= 2 else let i -= 1 endif endwhile return a:list endfunction endif function! s:_concat(lists) let result_list = [] for list in a:lists let result_list += list endfor return result_list endfunction function! s:_redir(cmd) let [save_verbose, save_verbosefile] = [&verbose, &verbosefile] set verbose=0 verbosefile= redir => res silent! execute a:cmd redir END let [&verbose, &verbosefile] = [save_verbose, save_verbosefile] return res endfunction function! vital#{s:self_version}#new() return s:_import('') endfunction let s:self_file = s:_unify_path(expand('')) ================================================ FILE: autoload/vital/open_browser_github.vital ================================================ open_browser_github e8ec38a System.Filepath Data.List ================================================ FILE: autoload/vital.vim ================================================ function! vital#of(name) let files = globpath(&runtimepath, 'autoload/vital/' . a:name . '.vital') let file = split(files, "\n") if empty(file) throw 'vital: version file not found: ' . a:name endif let ver = readfile(file[0], 'b') if empty(ver) throw 'vital: invalid version file: ' . a:name endif return vital#_{substitute(ver[0], '\W', '', 'g')}#new() endfunction ================================================ FILE: doc/openbrowser-github.txt ================================================ *openbrowser-github* Launch GitHub quickly from Vim Author: tyru Version: 1.0.0 License: See LICENSE file in this repository ============================================================================== CONTENTS *openbrowser-github-contents* Introduction |openbrowser-github-introduction| Requirements |openbrowser-github-requirements| Interface |openbrowser-github-interface| Variables |openbrowser-github-variables| Commands |openbrowser-github-commands| TODO |openbrowser-github-todo| Changelog |openbrowser-github-changelog| ============================================================================== INTRODUCTION *openbrowser-github-introduction* {{{ Opens GitHub URL of current file, etc. from Vim. Also supports GitHub Enterprise(|openbrowser-github-enterprise|). See |openbrowser-github-commands| for the details of available commands. }}} ============================================================================== REQUIREMENTS *openbrowser-github-requirements* {{{ You must install the following plugin/libraries. * open-browser.vim https://github.com/tyru/open-browser.vim * git command in your PATH }}} ============================================================================== GITHUB ENTERPRISE SETTING *openbrowser-github-enterprise* {{{ If you have `hub` command ----------------------- If you have `hub` command(https://github.com/github/hub) in your PATH, |openbrowser-github| executes the following command: > hub browse -u -- {path} And it will open the returned (output) URL. If you _don't_ have `hub` command ------------------------------- If you don't have `hub` command in your PATH, |openbrowser-github| tries to get each part of URL from the following gitconfig key: * hub.host You can specify GitHub Enterprise repository URL by setting above keys in gitconfig. For example, you can set `hub.host` by executing the following command in your git repository which you want to specify GitHub Enterprise repository URL. > git config --local hub.host my.git.org < }}} ============================================================================== INTERFACE *openbrowser-github-interface* {{{ ------------------------------------------------------------------------------ VARIABLES *openbrowser-github-variables* {{{ *g:openbrowser_github_always_used_branch* g:openbrowser_github_always_used_branch (Default: "") If this variable is not empty string, |openbrowser-github| always opens current file in the branch. *g:openbrowser_github_always_use_commit_hash* g:openbrowser_github_always_use_commit_hash (Default: 1) If this variable is non-zero value, |openbrowser-github| always opens a URL with a current commit hash. ex) https://github.com/tyru/open-browser.vim/blob/9f1de0e38a1e378061a8f10df6ed8e22c48aa9ae/autoload/openbrowser.vim If this variable is zero value, |openbrowser-github| opens a URL with a current commit hash if current working tree is detached state. Otherwise, |openbrowser-github| opens a URL with a current branch's latest file. ex) https://github.com/tyru/open-browser.vim/blob/master/autoload/openbrowser.vim *g:openbrowser_github_url_exists_check* g:openbrowser_github_url_exists_check (Default: "yes") This determines the behavior when you push to the non-existent/unauthorized repository. If this variable is "yes", shows prompt to ask you if you really open URL. If this variable is "ignore", any prompts and warning messages aren't showed. Otherwise, it is treated as default value ("yes"). *g:openbrowser_github_select_current_line* g:openbrowser_github_select_current_line (Default: 0) |:OpenGithubFile| opens a github page that current line is highlighted on. If this variable is non-zero, always opens URL with current line fragment (`...#Lxxx`) even the line is not selected in visual-mode. If this variable is zero (default), it opens URL with current line fragment only when the line is selected in visual-mode. }}} ------------------------------------------------------------------------------ COMMANDS *openbrowser-github-commands* {{{ :OpenGithubFile [{path}] *:OpenGithubFile* ------------------------ Opens a specific file in github.com repository(it also opens in the current branch by default). > " Opens current files URL in github.com " ex) https://{hostname}/{user}/{repos}/blob/{hash or branch}/{relpath} :OpenGithubFile " Opens current files highlighted place in github.com " ex) https://{hostname}/{user}/{repos}/blob/{hash or branch}/{relpath}#{lnum} :'<,'>OpenGithubFile " Opens a specific file in github.com " ex) https://{hostname}/{user}/{repos}/blob/{hash or branch}/PATH/TO/FILE#{lnum} :OpenGithubFile PATH/TO/FILE < Those URLs are influenced by some global variables. See: * |g:openbrowser_github_always_used_branch| * |g:openbrowser_github_always_use_commit_hash| *:OpenGithubIssue* :OpenGithubIssue :OpenGithubIssue [#]{number} [{user}/{repos}] :OpenGithubIssue {user}/{repos} ----------------------------------- Opens a specific Issue. > " Opens current repositories Issue #1 " ex) https://{hostname}/{user}/{repos}/issues/1 :OpenGithubIssue 1 " Opens a specific repositories Issue #1 " ex) https://{hostname}/tyru/open-browser.vim/issues/1 :OpenGithubIssue 1 tyru/open-browser.vim " Opens current repositories Issue List " ex) https://{hostname}/{user}/{repos}/issues :OpenGithubIssue " Opens a specific repositories Issue list " ex) https://{hostname}/tyru/open-browser.vim/issues :OpenGithubIssue tyru/open-browser.vim < *:OpenGithubPullReq* :OpenGithubPullReq :OpenGithubPullReq [#]{number} [{user}/{repos}] :OpenGithubPullReq #{branch} [{user}/{repos}] :OpenGithubPullReq {user}/{repos} ------------------------------------- Opens '/pulls' page when it has no argument. Otherwise, it does entirely the same thing as |:OpenGithubIssue| since GitHub redirects '/issues/1' to '/pull/1' if #1 is a Pull Request. Additionally, this command supports opening a pull request page by branch name. > " Opens current repository's pull request of specified branch name :OpenGithubPullReq #branch_name_of_pull_request " Opens https://github.com/tyru/open-browser-github.vim/pull/25 :OpenGithubPullReq #OpenGithubPullReq-branch-argument tyru/open-browser-github.vim < *:OpenGithubProject* :OpenGithubProject [{user}/{repos}] Opens a project page. > " Opens current opening file's repository. " ex) https://{hostname}/{user}/{name} :OpenGithubProject > " Opens current opening file's repository. " ex) https://{hostname}/tyru/open-browser.vim :OpenGithubProject tyru/open-browser.vim < *:OpenGithubCommit* :OpenGithubCommit {commit hash} [{user}/{repos}] Opens a commit page. > " Opens current repository's commit. " ex) https://{hostname}/{user}/{name}/commit/{hash} :OpenGithubCommit HEAD > " Opens speicified repository's commit. " ex) https://{hostname}/vim/vim/commit/b1c9198afb :OpenGithubCommit b1c9198afb vim/vim }}} }}} ============================================================================== TODO *openbrowser-github-todo* {{{ Any requests? ;) https://github.com/tyru/open-browser-github.vim/issues }}} ============================================================================== CHANGELOG *openbrowser-github-changelog* {{{ 1.0.0: - Initial upload }}} ============================================================================== vim:tw=78:fo=tcq2mM:ts=4:ft=help:norl:noet:fdm=marker:fen ================================================ FILE: plugin/openbrowser/github.vim ================================================ " vim:foldmethod=marker:fen: scriptencoding utf-8 " Load Once {{{ if get(g:, 'loaded_openbrowser_github', 0) || &cp finish endif let g:loaded_openbrowser_github = 1 " }}} " Saving 'cpoptions' {{{ let s:save_cpo = &cpo set cpo&vim " }}} function! s:error(msg) abort echohl ErrorMsg echomsg a:msg echohl None endfunction if !executable('git') call s:error('Please install git in your PATH.') finish endif if globpath(&rtp, 'plugin/openbrowser.vim') ==# '' call s:error('open-browser-github.vim depends on open-browser.vim. Please install open-browser.vim') finish endif if !exists('g:openbrowser_github_always_used_branch') let g:openbrowser_github_always_used_branch = '' endif if !exists('g:openbrowser_github_always_use_commit_hash') let g:openbrowser_github_always_use_commit_hash = 1 endif if !exists('g:openbrowser_github_url_exists_check') let g:openbrowser_github_url_exists_check = 'yes' endif if !exists('g:openbrowser_github_select_current_line') let g:openbrowser_github_select_current_line = 0 endif command! -range=0 -bar -nargs=* -complete=file \ OpenGithubFile \ call openbrowser#github#file([], , , ) command! -bar -nargs=* \ OpenGithubIssue \ call openbrowser#github#issue([]) command! -bar -nargs=* \ OpenGithubPullReq \ call openbrowser#github#pullreq([]) command! -bar -nargs=* \ OpenGithubProject \ call openbrowser#github#project([]) command! -bar -nargs=+ \ OpenGithubCommit \ call openbrowser#github#commit([]) " Restore 'cpoptions' {{{ let &cpo = s:save_cpo " }}}