diff --git a/autoload/SpaceVim/layers/github.vim b/autoload/SpaceVim/layers/github.vim index 197b9c8b5..9437e35a5 100644 --- a/autoload/SpaceVim/layers/github.vim +++ b/autoload/SpaceVim/layers/github.vim @@ -25,7 +25,7 @@ function! SpaceVim#layers#github#plugins() abort return [ - \ ['jaxbot/github-issues.vim', { 'on_cmd' : 'Gissues' }], + \ [g:_spacevim_root_dir . 'bundle/github-issues.vim', {'merged': 0}], \ ['junegunn/vim-github-dashboard', { \ 'on_cmd': ['GHA', 'GHD', 'GHActivity', 'GHDashboard'], \ }], diff --git a/bundle/README.md b/bundle/README.md index 9b76b9a71..d549a90f4 100644 --- a/bundle/README.md +++ b/bundle/README.md @@ -8,6 +8,7 @@ In `bundle/` directory, there are two kinds of plugins: forked plugins without c - `delimitMate`: based on [`Raimondi/delimitMate@537a1da`](https://github.com/Raimondi/delimitMate/tree/537a1da0fa5eeb88640425c37e545af933c56e1b) - `vim-toml`: based on [cespare/vim-toml@717bd87ef9](https://github.com/cespare/vim-toml/tree/717bd87ef928293e0cc6cfc12ebf2e007cb25311) - `neoformat`: based on [neoformat](https://github.com/sbdchd/neoformat/tree/f1b6cd506b72be0a2aaf529105320ec929683920) +- `github-issues.vim`: based on [github-issues.vim](https://github.com/jaxbot/github-issues.vim/tree/46f1922d3d225ed659f3dda1c95e35001c9f41f4) ### No changed plugins diff --git a/bundle/github-issues.vim/.gitignore b/bundle/github-issues.vim/.gitignore new file mode 100644 index 000000000..6e92f57d4 --- /dev/null +++ b/bundle/github-issues.vim/.gitignore @@ -0,0 +1 @@ +tags diff --git a/bundle/github-issues.vim/LICENSE b/bundle/github-issues.vim/LICENSE new file mode 100644 index 000000000..392fae762 --- /dev/null +++ b/bundle/github-issues.vim/LICENSE @@ -0,0 +1,24 @@ +The MIT License (MIT) + +Copyright (C) 2014 Jonathan Warner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +I am providing code in this repository to you under an open source license. Because this is my personal repository, the license you receive to my code is from me and not my employer (Facebook). + diff --git a/bundle/github-issues.vim/README.md b/bundle/github-issues.vim/README.md new file mode 100644 index 000000000..f46ecc41c --- /dev/null +++ b/bundle/github-issues.vim/README.md @@ -0,0 +1,147 @@ +github-issues.vim +================= + +Github issue integration in Vim. It's kind of awesome. + +# Looking for a new owner + +If you're interested in taking over this project and giving it the love it deserves, send me an email or file a bug. + +### Omnicomplete + +If you use Fugitive or edit gitcommit files in Vim, github-issues will automatically populate the omnicomplete menu with issues on Github. This is useful when you want to reference commits, close issues, etc., through Github's commit message parsing. + +Here's how it works with Neocomplete: + + + +If you use pure omnicomplete, use `C-x C-o` to pull up the menu. + +No need to run commands, no need to configure. It just works. ;) (And if it doesn't, it should, so submit an issue) Not bad, huh? + +### Lookup menu + +To show Github issues for the current repository: +``` +:Gissues +``` + +Press enter to view more details. + + + +### Handling issues + +You can open and close issues using `co` and `cc` in the issue view. + + + +They're also totally editable buffers, and saving the file will sync with Github's servers. You can use this to write comments, too: + + + +You can also open the current issue in your browser using `cb`. + +### Creating issues + +You can even use `:Giadd` to create a blank issue. Saving the buffer will generate a new issue and update the buffer with an issue number and the ability to add comments. + + + +How awesome is that!? + +### Milestones + +New feature: Use `:Gmiles` to open a menu of milestones. Press return on one to select it and filter `:Gissues` by that milestone from then on. + +### Custom Issue Search + +You have the ability to define your own issue search using [Githubs search API](https://developer.github.com/v3/search/). This allows you to view issues and pull requests across repos. + +To use this feature, you can add a line like the following, with the search parameters that you wish to use. + +```vim +let g:gh_issues_query = "state:open user:github label:\"feature\" sort:created-asc" +``` + +You can than get this list of issues using the command `:Gisearch` + +### Fugitive integration + +Github will show any commits that reference the issue. That's what the omnicomplete helps with. But to make things even more awesome, github-issues.vim integrates with Fugitive.vim to make commit hashes clickable with the return key. + + + +### Requirements and Installation + +Vim with Python 2.7, Python 2.7 installed and working with Vim. + +I recommend using [Pathogen](https://github.com/tpope/vim-pathogen) and Git cloning into ~/.vim/bundle. You can also just download the plugin and paste it into your plugin directory. + +Then **read below about adding an access token**. + +### Configuration + +The omnicomplete and lookup features will work out of the box for public repos. + +If you have private repos, or would like the ability to comment, open, close, and add issues, you will need to set an access token. Don't worry, this is super easy. + +``` +g:github_access_token +``` + +Grab an access token [from here]( +https://github.com/settings/tokens/new), then set this variable, preferably in a local vim file not included in any public repositories: + +`let g:github_access_token = "9jb19c1189f083d7013i24367lol"` + +**Remember**, you should treat your access token like a password! + + +Other options include: + +``` +g:github_issues_no_omni +``` + +When this is set to any value, github-issues will not set Neocomplete and Omnicomplete hooks. + + +``` +g:github_upstream_issues +``` + +When this is set to 1, github-issues will use upstream issues (if repo is fork). This will require extra requests for the Github API, however. + +``` +g:github_api_url = "https://api.github.com/" +``` + +If you use Github Enterprise, where the Github server is hosted somewhere other than Github.com, set this parameter to your API path. This is specifically for Github Enterprise and will not work for Bitbucket, Gitlab, etc. + +``` +g:github_same_window = 1 +``` + +When this is set to 1, github-issues will use the current window instead of splitting the screen via the `:new` command. + +``` +g:gissues_lazy_load = 0 +``` + +When this is set to 1, omnicomplete will not be populated until it is triggered. This eliminates potential lag when opening `gitcommit` files. + +``` +g:gissues_async_omni = 0 +``` + +**Experimental**: When set to 1, omnicomplete will be populated asynchronously, on another thread. This removes almost all lag from the UI when using Gissues, and can be combined with g:gissues_lazy_load to reduce network traffic while still receiving the same speed boost. However, this uses threads and needs a lot more testing to ensure it is stable. + +### Contributing + +Pull requests, feature requests, and issues are always welcome! + +## Shameless plug + +I hack around with Vim plugins, so [follow me](https://github.com/jaxbot) if you're into that kind of stuff (or just want to make my day) ;) + diff --git a/bundle/github-issues.vim/autoload/gh_colors.vim b/bundle/github-issues.vim/autoload/gh_colors.vim new file mode 100644 index 000000000..dc3ec14ad --- /dev/null +++ b/bundle/github-issues.vim/autoload/gh_colors.vim @@ -0,0 +1,268 @@ +Python <:p')), ':h') . '/../rplugin/python/ghissues.py' + +if has("python3") + command! -nargs=1 Python python3 + execute 'py3file ' . s:path +else + command! -nargs=1 Python python + execute 'pyfile ' . s:path +endif + + +" Default to not having loaded xterm +let g:gissues_xterm_colors = 0 + +function! ghissues#init() + let g:github_issues_pyloaded = 1 + + " Load in colors if we're in terminal mode + if &term == "xterm-256color" + call gh_colors#init() + endif +endfunction + +" vim: softtabstop=2 expandtab shiftwidth=2 tabstop=2 diff --git a/bundle/github-issues.vim/doc/Gissues.txt b/bundle/github-issues.vim/doc/Gissues.txt new file mode 100644 index 000000000..256d3166c --- /dev/null +++ b/bundle/github-issues.vim/doc/Gissues.txt @@ -0,0 +1,216 @@ +*gissues.txt* github-issues.vim + +=============================================================================== +Contents ~ + + 1. Introduction |gissues-introduction| + 1. Omnicomplete |gissues-omnicomplete| + 2. Lookup menu |gissues-lookup-menu| + 3. Handling issues |gissues-handling-issues| + 4. Creating issues |gissues-creating-issues| + 5. Milestones |gissues-milestones| + 6. Fugitive integration |gissues-fugitive-integration| + 7. Requirements and Installation |gissues-requirements-installation| + 8. Configuration |gissues-configuration| + 9. Contributing |gissues-contributing| + 2. Shameless plug |gissues-shameless-plug| + 3. References |gissues-references| + +=============================================================================== + *gissues-introduction* +Introduction ~ + +Github issue integration in Vim. It's kind of awesome. + +------------------------------------------------------------------------------- + *gissues-omnicomplete* +Omnicomplete ~ + +If you use Fugitive or edit gitcommit files in Vim, github-issues will +automatically populate the omnicomplete menu with issues on Github. This is +useful when you want to reference commits, close issues, etc., through Github's +commit message parsing. + +Here's how it works with Neocomplete: + + Image: (see reference [1]) + +If you use pure omnicomplete, use 'C-x C-o' to pull up the menu. + +No need to run commands, no need to configure. It just works. ;) (And if it +doesn't, it should, so submit an issue) Not bad, huh? + +------------------------------------------------------------------------------- + *gissues-lookup-menu* +Lookup menu ~ + +To show Github issues for the current repository: +> + :Gissues +< +Press enter to view more details. + + Image: (see reference [2]) + +------------------------------------------------------------------------------- + *gissues-handling-issues* +Handling issues ~ + +You can open and close issues using 'co' and 'cc' in the issue view. + + Image: (see reference [3]) + +They're also totally editable buffers, and saving the file will sync with +Github's servers. You can use this to write comments, too: + + Image: (see reference [4]) + +------------------------------------------------------------------------------- + *gissues-creating-issues* +Creating issues ~ + +You can even use ':Giadd' to create a blank issue. Saving the buffer will +generate a new issue and update the buffer with an issue number and the ability +to add comments. + + Image: (see reference [5]) + +While creating or editing issues it is possible to add multiple space separated +assignees. + +How awesome is that!? + +------------------------------------------------------------------------------- + *gissues-milestones* +Milestones ~ + +New feature: Use ':Gmiles' to open a menu of milestones. Press return on one to +select it and filter ':Gissues' by that milestone from then on. + +------------------------------------------------------------------------------- + *gissues-fugitive-integration* +Fugitive integration ~ + +Github will show any commits that reference the issue. That's what the +omnicomplete helps with. But to make things even more awesome, github- +issues.vim integrates with Fugitive.vim to make commit hashes clickable with +the return key. + + Image: (see reference [6]) + +------------------------------------------------------------------------------- + *gissues-requirements-installation* +Requirements and Installation ~ + +Vim with Python 2.7, Python 2.7 installed and working with Vim. + +I recommend using Pathogen [7] and Git cloning into ~/.vim/bundle. You can also +just download the plugin and paste it into your plugin directory. + +Then **read below about adding an access token**. + +------------------------------------------------------------------------------- + *gissues-configuration* +Configuration ~ + +The omnicomplete and lookup features will work out of the box for public repos. + +If you have private repos, or would like the ability to comment, open, close, +and add issues, you will need to set an access token. Don't worry, this is +super easy. +> + g:github_access_token +< +Grab an access token from here [8], then set this variable, preferably in a +local vim file not included in any public repositories: + +'let g:github_access_token = "9jb19c1189f083d7013i24367lol"' + +**Remember**, you should treat your access token like a password! + +Other options include: +> + g:github_issues_no_omni +< +When this is set to any value, github-issues will not set Neocomplete and +Omnicomplete hooks. +> + g:github_upstream_issues +< +When this is set to 1, github-issues will use upstream issues (if repo is +fork). This will require extra requests for the Github API, however. +> + g:github_api_url = "https://api.github.com/" +< +If you use Github Enterprise, where the Github server is hosted somewhere other +than Github.com, set this parameter to your API path. This is specifically for +Github Enterprise and will not work for Bitbucket, Gitlab, etc. +> + g:github_same_window = 1 +< +When this is set to 1, github-issues will use the current window instead of +splitting the screen via the ':new' command. +> + g:gissues_issue_vsplit = 0 + +When this set to one the issue details buffer will open in vertical buffer +instead of horizontal. Works only if `g:github_same_window` is not set +> + g:gissues_list_vsplit = 0 + +Same as above but for the list of issues. +> + g:gissues_split_expand = 0 + +Setting to 1 will expand the buffer to full width for horizontal split and +full height for vertical split instead of nesting inside parent split. +> + g:gissues_split_height = 0 + +Set the height of horizontal split, default 50% of parent. +> + g:issues_vsplit_width = 0 + +Same as above but for vertical split. +> + g:gissues_lazy_load = 0 +< +When this is set to 1, omnicomplete will not be populated until it is +triggered. This eliminates potential lag when opening 'gitcommit' files. +> + g:gissues_async_omni = 0 +< +**Experimental**: When set to 1, omnicomplete will be populated asynchronously, +on another thread. This removes almost all lag from the UI when using Gissues, +and can be combined with g:gissues_lazy_load to reduce network traffic while +still receiving the same speed boost. However, this uses threads and needs a +lot more testing to ensure it is stable. + +------------------------------------------------------------------------------- + *gissues-contributing* +Contributing ~ + +Pull requests, feature requests, and issues are always welcome! + +=============================================================================== + *gissues-shameless-plug* +Shameless plug ~ + +I hack around with Vim plugins, so follow me [9] if you're into that kind of +stuff (or just want to make my day) ;) + +=============================================================================== + *gissues-references* +References ~ + +[1] https://jaxbot.me/pics/vim/vim_gissues2.gif +[2] https://jaxbot.me/pics/vim/vim-github-issues-1.gif +[3] https://jaxbot.me/pics/vim/vim-github-issues-2.gif +[4] https://jaxbot.me/pics/vim/vim-github-issues-4.gif +[5] https://jaxbot.me/pics/vim/vim-github-issues-6.gif +[6] https://jaxbot.me/pics/vim/vim-github-issues-3.gif +[7] https://github.com/tpope/vim-pathogen +[8] https://github.com/settings/tokens/new +[9] https://github.com/jaxbot + +vim: ft=help diff --git a/bundle/github-issues.vim/plugin/githubissues.vim b/bundle/github-issues.vim/plugin/githubissues.vim new file mode 100644 index 000000000..6eb3d323c --- /dev/null +++ b/bundle/github-issues.vim/plugin/githubissues.vim @@ -0,0 +1,363 @@ +" File: github-issues.vim +" Version: 3.1.0 +" Description: Pulls github issues into Vim +" Maintainer: Jonathan Warner +" Homepage: http://jaxbot.me/ +" Repository: https://github.com/jaxbot/github-issues.vim +" License: Copyright (C) 2014 Jonathan Warner +" Released under the MIT license +" ====================================================================== + +" do not load twice +if exists("g:github_issues_loaded") || &cp + finish +endif + +let g:github_issues_loaded = 1 + +" do not continue if Vim is not compiled with Python support +if !has("python") && !has("python3") + echo "github-issues.vim requires Python support, sorry :c" + finish +endif + +function! s:showGithubMilestones(...) + call ghissues#init() + + if a:0 <1 + Python showMilestoneList(0, "True") + else + Python showMilestoneList(vim.eval("a:1"), "True") + endif + + set buftype=nofile + nnoremap q :q + nnoremap :normal! 0:call setMilestone(getline(".")) + +endfunction + +function! s:showGithubIssues(...) + call ghissues#init() + + let github_failed = 0 + if a:0 < 1 + Python showIssueList(0, "True") + else + Python showIssueList(vim.eval("a:1"), "True") + endif + + if github_failed == "1" + return + endif + + " its not a real file + set buftype=nofile +" map the enter key to show issue + nnoremap :call showIssue(expand(""),"") + nnoremap i :Giadd + nnoremap q :q + +endfunction + +function! s:showSearchList() + call ghissues#init() + Python showSearchList() + + " its not a real file + set buftype=nofile + " map the enter key to show issue + nnoremap :call showIssue(expand(""),"") + nnoremap i :Giadd + nnoremap q :q +endfunction + +function! s:showIssueLink(...) + call ghissues#init() + if a:0 > 2 + Python showIssueLink(vim.eval("a:1"), vim.eval("a:2"), vim.eval("a:3")) + elseif a:0 > 1 + Python showIssueLink(vim.eval("a:1"), vim.eval("a:2")) + else + Python showIssueLink(vim.eval("a:1")) + endif +endfunction + +function! s:showIssue(...) + call ghissues#init() + + if a:0 > 2 + Python showIssueBuffer(vim.eval("a:1"), vim.eval("a:2"), vim.eval("a:3")) + elseif a:0 > 1 + Python showIssueBuffer(vim.eval("a:1"), vim.eval("a:2")) + else + Python showIssueBuffer(vim.eval("a:1")) + endif + + + call s:setupOmni() + + if a:1 == "new" + normal 0llllllllll + startinsert + endif + + setlocal nomodified +endfunction + +function! s:showThisIssue(...) + call ghissues#init() + + Python curUri = getRepoURI() + + silent tabe + set buftype=nofile + normal ggdG + + if a:0 > 1 + let name = "gissues/" . a:2 . "/" . a:1 + execute 'edit' name + Python showIssue(vim.eval("a:1"),vim.eval("a:2")) + else + Python < :call showIssueLink("","","False") + nnoremap s :call showIssueLink("","","True") + nnoremap q :q + +endfunction + +function! s:setIssueState(state) + Python setIssueData({ 'state': 'open' if vim.eval("a:state") == '1' else 'closed' }) +endfunction + +function! s:browse() + if has("patch-7.4.567") + call netrw#BrowseX(b:ghissue_url,0) + else + call netrw#NetrwBrowseX(b:ghissue_url,0) + endif +endfunction + +function! s:updateIssue() + call ghissues#init() + Python showIssue() + silent execute 'doautocmd BufReadPost '.expand('%:p') +endfunction + +function! s:saveIssue() + call ghissues#init() + Python saveGissue() + silent execute 'doautocmd BufWritePost '.expand('%:p') +endfunction + +" omnicomplete function, also used by neocomplete +function! githubissues#CompleteIssues(findstart, base) + if g:gissues_lazy_load + if !b:did_init_omnicomplete + Python populateOmniComplete() + let b:did_init_omnicomplete = 1 + endif + endif + + if g:gissues_async_omni && len(b:omni_options) < 1 + if !b:did_init_omnicomplete_async + let b:did_init_omnicomplete_async = 1 + Python doPopulateOmniComplete() + endif + endif + + if a:findstart + " locate the start of the word + let line = getline('.') + let start = col('.') - 1 + + while start > 0 && line[start - 1] =~ '\w' + let start -= 1 + endwhile + let b:compl_context = getline('.')[start - 1: col('.')] + return start + else + let res = [] + if b:compl_context == '.' + return res + endif + + for m in b:omni_options + if m['menu'] == '[Issue]' + if '#' . m['word'] =~ '^' . b:compl_context + call add(res, m) + endif + elseif m['menu'] == '[User]' + if '@' . m['word'] =~ '^' . b:compl_context + call add(res, m) + endif + else + if m['word'] =~ '^' . b:compl_context || ' ' . m['word'] =~ '^' . b:compl_context + call add(res, m) + endif + endif + endfor + return res + endif +endfunction + +" set omnifunc for the buffer +function! s:setupOmni() + call ghissues#init() + + setlocal omnifunc=githubissues#CompleteIssues + + " empty array will store the menu items + let b:omni_options = [] + + if !g:gissues_lazy_load + Python populateOmniComplete() + if !g:gissues_async_omni + Python doPopulateOmniComplete() + endif + else + let b:did_init_omnicomplete = 0 + endif + + let b:did_init_omnicomplete_async = 0 +endfunction + +function! s:setMilestone(title) + let title = "" + if a:title != "[None]" + let title = a:title + echo "Switched current milestone to " . title + else + echo "No longer filtering by milestone" + endif + + let g:github_current_milestone = title + +endfunction + +function! s:commitHighlighting() + hi shaHighlight term=bold cterm=bold gui=bold ctermfg=red guifg=red + syn match shaHighlight "\v^[a-zA-Z0-9]{40}" +endfunction + +" define the :Gissues command +command! -nargs=* Gissues call s:showGithubIssues() +command! -nargs=* Gisearch call s:showSearchList() +command! -nargs=* Giadd call s:showIssue("new", ) +command! -nargs=* Giedit call s:showIssue() +command! -nargs=0 Giupdate call s:updateIssue() + +command! -nargs=* Gmiles call s:showGithubMilestones() + +command! -nargs=* Gishow call s:showThisIssue() + +autocmd BufReadCmd gissues/*/\([0-9]*\|new\) call s:updateIssue() +autocmd BufReadCmd gissues/*/\([0-9]*\|new\) nnoremap cc :call setIssueState(0) +autocmd BufReadCmd gissues/*/\([0-9]*\|new\) nnoremap co :call setIssueState(1) +autocmd BufReadCmd gissues/*/\([0-9]*\|new\) nnoremap cb :call browse() + +" map the enter key to show issue or click link +autocmd BufReadCmd gissues/*/\([0-9]*\|new\) nnoremap :call showIssueLink("","","False") +autocmd BufReadCmd gissues/*/\([0-9]*\|new\) nnoremap s :call showIssueLink("","","True") +autocmd BufReadCmd gissues/*/\([0-9]*\|new\) nnoremap q :q + +autocmd BufWriteCmd gissues/*/[0-9a-z]* call s:saveIssue() + +if !exists("g:github_issues_no_omni") + " Neocomplete support + if !exists('g:neocomplete#sources#omni#input_patterns') + let g:neocomplete#sources#omni#input_patterns = {} + endif + let g:neocomplete#sources#omni#input_patterns.gitcommit = '.' + let g:neocomplete#sources#omni#input_patterns.gfimarkdown = '.' + + " Install omnifunc on gitcommit files + autocmd FileType gitcommit call s:setupOmni() +endif + +if !exists("g:github_access_token") + let g:github_access_token = "" +elseif filereadable(expand(g:github_access_token)) + let lines = readfile(expand(g:github_access_token)) + if !empty(lines) + let g:github_access_token=lines[0] + endif +endif + +if !exists("g:github_upstream_issues") + let g:github_upstream_issues = 0 +endif + +if !exists("g:github_issues_urls") + let g:github_issues_urls = ["github.com:", "github.com/"] +endif + +if !exists("g:github_api_url") + let g:github_api_url = "https://api.github.com/" +endif + +if !exists("g:github_issues_max_pages") + let g:github_issues_max_pages = 1 +endif + +" force issues and what not to stay in the same window +if !exists("g:github_same_window") + let g:github_same_window = 0 +endif + +" allow milestone filtering +if !exists("g:github_current_milestone") + let g:github_current_milestone = "" +endif + +" lazy load issues +if !exists("g:gissues_lazy_load") + let g:gissues_lazy_load = 0 +endif + +" asynchronously load autocomplete +if !exists("g:gissues_async_omni") + let g:gissues_async_omni = 0 +endif + +if !exists("g:gissues_default_remote") + let g:gissues_default_remote = "origin" +endif + +if !exists("g:gissues_show_errors") + let g:gissues_show_errors = 0 +endif + +if !exists("g:gissues_offline_cache") + let g:gissues_offline_cache = 0 +endif + +if !exists("g:gissues_issue_vsplit") + let g:gissues_issue_vsplit = 0 +endif + +if !exists("g:gissues_list_vsplit") + let g:gissues_list_vsplit = 0 +endif + +if !exists("g:gissues_split_expand") + let g:gissues_split_expand = 0 +endif + +if !exists("g:gissues_split_height") + let g:gissues_split_height = 0 +endif + +if !exists("g:gissues_vsplit_width") + let g:gissues_vsplit_width = 0 +endif + + + + diff --git a/bundle/github-issues.vim/rplugin/python/ghissues.py b/bundle/github-issues.vim/rplugin/python/ghissues.py new file mode 100644 index 000000000..5bbf3f0ef --- /dev/null +++ b/bundle/github-issues.vim/rplugin/python/ghissues.py @@ -0,0 +1,1113 @@ +import hashlib +import json +import os +import shlex +import string +import subprocess +import threading +import time +import urllib +from past.builtins import basestring # Works in python 2 and 3 +from past.builtins import map, filter # Python 2 map compatibility + +try: + # Python 2 + # import basestring + import urllib2 +except ImportError: + print("Using Python3") + + # Python 3 re-organizes urllib + from urllib.parse import urlencode + import urllib.request as urllib2 + urllib.urlencode = urlencode + +import vim + +SHOW_ALL = "[Show all issues]" +SHOW_ASSIGNED_ME = "[Only show assigned to me]" +SHOW_COMMITS = "## [Commits]" +SHOW_FILES_CHANGED = "## [Files Changed]" + +# dictionaries for caching +# repo urls on github by filepath +github_repos = {} +# issues by repourl +github_datacache = {} +# api data by repourl + / + endpoint +api_cache = {} +# process handles by file hash +proc_handle = {} +# whether or not a async request is pending by file hash +proc_pending = {} + +# reset web cache after this value grows too large +cache_count = 0 + +# whether or not SSL is known to be enabled +ssl_enabled = False + +# keep a list of issues +globissues = {} + +# returns the Github url (i.e. jaxbot/vimfiles) for the current file + + +def getRepoURI(): + global github_repos + + if "gissues" in vim.current.buffer.name: + parens = getFilenameParens() + return parens[0] + "/" + parens[1] + + # get the directory the current file is in + filepath = vim.eval("expand('%:p:h')") + + # Remove trailing ".git" segment from path. + # While `git remote -v` appears to work from here in general, it fails when + # invoked for COMMIT_EDITMSG: `fatal: Not a git repository: '.git'`. + filepath = filepath.split(os.path.sep + ".git")[0] + + # cache the github repo for performance + if github_repos.get(filepath, None) is not None: + return github_repos[filepath] + + # Get info for all remotes. + # Do this first: if it fails, we're not in a Git repo. + try: + all_remotes = subprocess.check_output( + ['git', 'remote', '-v'], cwd=filepath) + except subprocess.CalledProcessError: + github_repos[filepath] = "" + return github_repos[filepath] + except OSError: + all_remotes = subprocess.check_output(['git', 'remote', '-v']) + + # Try to get the remote for the current branch/HEAD. + try: + remote_ref = subprocess.check_output( + 'git rev-parse --abbrev-ref --verify --symbolic-full-name @{upstream}'.split(" "), + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError: + # Use the first one we find instead + remote = None + #print("github-issues: using default remote: %s" % remote) + else: + try: + branch = subprocess.check_output( + ["git", "symbolic-ref", "--short", "HEAD"]) + except subprocess.CalledProcessError: + # Branch could not be determined, do not filter by remote. + remote = None + else: + # Remove "/branch" from the end of remote_ref to get the remote. + remote = remote_ref[:-(len(branch) + 1)] + + # possible URLs + possible_urls = vim.eval("g:github_issues_urls") + + for line in all_remotes.decode().split("\n"): + try: + cur_remote, url = line.split("\t") + except ValueError: + continue + + # Filter out non-matching remotes. + if remote and remote.decode() != cur_remote: + continue + + # Remove " (fetch)"/" (pull)" and ".git" suffixes. + url = url.split(" ", 1)[0] + if url.endswith(".git"): + url = url[:-4] + + # Skip any unwanted urls. + for possible_url in possible_urls: + split_url = url.split(possible_url) + if len(split_url) > 1: + github_repos[filepath] = split_url[1] + #print("github-issues: using repo: %s" % s[1]) + break + else: + continue + break + else: + github_repos[filepath] = "" + + return github_repos[filepath] + +# returns the repo uri, taking into account forks + + +def getUpstreamRepoURI(): + repourl = getRepoURI() + if repourl == "": + return "" + + upstream_issues = int(vim.eval("g:github_upstream_issues")) + if upstream_issues == 1: + # try to get from what repo forked + repoinfo = ghApi("", repourl) + if repoinfo and repoinfo["fork"]: + repourl = repoinfo["source"]["full_name"] + + return repourl + + +def showCommits(split=False): + number = vim.eval("b:ghissue_number") + repourl = vim.eval("b:ghissue_repourl") + url = ghUrl("/pulls/" + number + "/commits", repourl) + response = urllib2.urlopen(url, timeout=2) + commits = json.loads(response.read()) + buffer_name = "commits/" + repourl + "/" + number + if split: + newSplit(buffer_name) + else: + newTab(buffer_name) + vim.command("setlocal modifiable") + current_buffer = vim.current.buffer + for commit in commits: + current_buffer.append( + (commit['sha'] + " " + commit['commit']['message']).split("\n")) + vim.command("normal ggdd") + vim.command( + "nnoremap :call showIssueLink('','','False')") + vim.command("nnoremap s :call showIssueLink('','','True')") + vim.command("call commitHighlighting()") + vim.command("let b:ghissue_repourl=\"" + repourl + "\"") + vim.command("setlocal nomodifiable") + + +def showCommit(sha, split=False): + repourl = vim.eval("b:ghissue_repourl") + url = ghUrl("/commits/" + sha, repourl) + headers = {"Accept": "application/vnd.github.patch"} + req = urllib2.Request(url, None, headers) + diff = urllib2.urlopen(req, timeout=2) + buffer_name = "commit/" + repourl + "/" + sha + if split: + newSplit(buffer_name) + else: + newTab(buffer_name) + vim.command("set syn=diff") + vim.command("setlocal modifiable") + current_buffer = vim.current.buffer + current_buffer.append("".join(diff).split("\n")) + vim.command("normal ggdd") + vim.command("setlocal nomodifiable") + + +def showFilesChanged(split=False): + number = vim.eval("b:ghissue_number") + repourl = vim.eval("b:ghissue_repourl") + url = ghUrl("/pulls/" + number, repourl) + headers = {"Accept": "application/vnd.github.diff"} + req = urllib2.Request(url, None, headers) + diff = urllib2.urlopen(req, timeout=2) + buffer_name = "files_Changed/" + repourl + "/" + number + if split: + newSplit(buffer_name) + else: + newTab(buffer_name) + vim.command("set syn=diff") + vim.command("setlocal modifiable") + current_buffer = vim.current.buffer + current_buffer.append("".join(diff).split("\n")) + vim.command("normal ggdd") + vim.command("setlocal nomodifiable") + + +def newTab(name): + vim.command("noswapfile silent tabe +set\\ buftype=nofile %s" % name) + mapQuit() + + +def newSplit(name): + vim.command( + "noswapfile silent belowright vsplit +set\\ buftype=nofile %s" % + name) + mapQuit() + + +def mapQuit(): + vim.command("nnoremap q :bdelete") + +# displays the issues in a vim buffer + + +def showIssueList(labels, ignore_cache=False, only_me=False): + repourl = getUpstreamRepoURI() + + if repourl == "": + print("github-issues.vim: Failed to find a suitable Github repository URL, sorry!") + vim.command("let github_failed = 1") + return + + issues = getIssueList(repourl, '/issues', labels, ignore_cache, only_me) + + printIssueList(issues, repourl, labels, only_me) + + +def printIssueList(issues, repourl='search', labels=False, only_me=False): + global globissues + globissues = issues + + # Setup split + if not vim.eval("g:github_same_window") == "1": + cmd = 'silent ' + if not vim.eval("g:gissues_list_vsplit") == "1": + spl = 'new +set\ buftype=nofile' + if vim.eval("g:gissues_split_height") != "0": + spl = ' ' + vim.eval("g:gissues_split_height") + spl + cmd = cmd + spl + else: + spl = 'vnew +set\ buftype=nofile' + if vim.eval("g:gissues_vsplit_width") != "0": + spl = ' ' + vim.eval("g:gissues_vsplit_width") + spl + cmd = cmd + spl + if vim.eval("g:gissues_split_expand") == "1": + cmd = 'botright ' + cmd + vim.command(cmd) + + if 'labels' not in locals(): + labels = "" + + # Some Vim versions don't allow noswapfile as a verb + try: + vim.command("noswapfile edit " + "gissues/" + repourl + "/issues") + except BaseException: + vim.command("edit " + "gissues/" + repourl + "/issues") + + vim.command("normal ggdG") + + current_buffer = vim.current.buffer + + cur_milestone = str(vim.eval("g:github_current_milestone")) + # its an array, so dump these into the current (issues) buffer + for issue in issues: + if cur_milestone != "" and ( + not issue["milestone"] or issue["milestone"]["title"] != cur_milestone): + continue + + issuestr = str(issue["number"]) + " " + issue["title"] + if 'labels' in issue: + for label in issue["labels"]: + issuestr += " [" + label["name"] + "]" + + current_buffer.append(issuestr.encode(vim.eval("&encoding"))) + + if len(current_buffer) < 2: + current_buffer.append("No results found in " + repourl) + if cur_milestone: + current_buffer.append("Filtering by milestone: " + cur_milestone) + if labels: + current_buffer.append("Filtering by labels: " + labels) + if cur_milestone or labels or only_me: + current_buffer.append(SHOW_ALL) + if not only_me: + current_buffer.append(SHOW_ASSIGNED_ME) + + highlightColoredLabels(getLabels(), True) + + # append leaves an unwanted beginning line. delete it. + vim.command("1delete _") + + +def showSearchList(): + if not vim.eval("g:github_same_window") == "1": + vim.command("silent new") + + # Some Vim versions don't allow noswapfile as a verb + try: + vim.command("noswapfile edit " + "gissues") + except BaseException: + vim.command("edit " + "gissues") + + vim.command("normal ggdG") + params = {"q": vim.eval("g:gh_issues_query")} + + issues = getGHList(1, "custom", 'search/issues', params) + printIssueList(issues) + + +def showMilestoneList(labels, ignore_cache=False): + repourl = getUpstreamRepoURI() + + vim.command("silent new") + + # Some Vim versions don't allow noswapfile as a verb + try: + vim.command("noswapfile edit " + "gissues/" + repourl + "/milestones") + except BaseException: + vim.command("edit " + "gissues/" + repourl + "/milestones") + vim.command("normal ggdG") + + current_buffer = vim.current.buffer + current_buffer.append("[None]") + + milestones = getMilestoneList(repourl, labels, ignore_cache) + + for mstone in milestones: + mstonestr = mstone["title"] + + current_buffer.append(mstonestr.encode(vim.eval("&encoding"))) + + vim.command("1delete _") + +# pulls the issue array from the server + + +def getIssueList(repourl, endpoint, query, ignore_cache=False, only_me=False): + global github_datacache + + # non-string args correspond to vanilla issues request + # strings default to label unless they correspond to a state + params = {} + if isinstance(query, basestring): + params = {"labels": query} + if query in ["open", "closed", "all"]: + params = {"state": query} + if only_me: + params["assignees"] = getCurrentUser() + + return getGHList(ignore_cache, repourl, endpoint, params) + + +def getCurrentUser(): + return ghApi("", "user", True, False)["login"] + +# pulls the milestone list from the server + + +def getMilestoneList(repourl, query="", ignore_cache=False): + global github_datacache + + # TODO Add support for 'state', 'sort', 'direction' + params = {} + + return getGHList(ignore_cache, repourl, "/milestones", params) + + +def getGHList(ignore_cache, repourl, endpoint, params): + global cache_count, github_datacache + + # Maybe initialise + if github_datacache.get( + repourl, '') == '' or len( + github_datacache[repourl]) < 1: + github_datacache[repourl] = {} + + if (ignore_cache or + github_datacache[repourl].get(endpoint, '') == '' or + len(github_datacache[repourl].get(endpoint, '')) < 1 or + time.time() - github_datacache[repourl][endpoint][0]["cachetime"] > 60): + + # load the github API. github_repo looks like + # "jaxbot/github-issues.vim", for ex. + try: + github_datacache[repourl][endpoint] = [] + more_to_load = True + + page = 1 + + while more_to_load and page <= int( + vim.eval("g:github_issues_max_pages")): + params['page'] = str(page) + + # TODO This should be in ghUrl() I think + qs = urllib.urlencode(params) + if repourl != "custom": + url = ghUrl(endpoint + '?' + qs, repourl) + else: + url = ghUrl(endpoint + '?' + qs, repourl, False) + + response = urllib2.urlopen(url, timeout=2) + issuearray = json.loads(response.read()) + + if 'items' in issuearray: + issuearray = issuearray["items"] + + # JSON parse the API response, add page to previous pages if + # any + github_datacache[repourl][endpoint] += issuearray + + more_to_load = len(issuearray) == 30 + + page += 1 + + except urllib2.URLError as e: + github_datacache[repourl][endpoint] = [] + if "code" in e and e.code == 404: + print( + "github-issues.vim: Error: Do you have a github_access_token defined?") + + if len(github_datacache[repourl][endpoint]) > 0: + github_datacache[repourl][endpoint][0]["cachetime"] = time.time() + + return github_datacache[repourl][endpoint] + +# populate the omnicomplete synchronously or asynchronously, depending on mode + + +def populateOmniComplete(): + if vim.eval("g:gissues_async_omni") == "1": + if vim.eval("g:gissues_offline_cache") == "1": + populateOmniCompleteFromDisk() + else: + populateOmniCompleteAsync() + else: + doPopulateOmniComplete() + + +def populateOmniCompleteFromDisk(): + url = getUpstreamRepoURI() + + if url == "": + return + + issues = grabCacheData("/issues") + for issue in issues: + addToOmni(str(issue["number"]) + " " + issue["title"], 'Issue') + + labels = grabCacheData("/labels") + if labels is not None: + for label in labels: + addToOmni(unicode(label["name"]), 'Label') + + contributors = grabCacheData("/stats/contributors") + if contributors is not None: + for contributor in contributors: + addToOmni(str(contributor["author"]["login"]), 'user') + + milestones = getMilestoneList(url) + if milestones is not None: + for milestone in milestones: + addToOmni(str(milestone["title"].encode('utf-8')), 'Milestone') + + +def apiToDiskAsync(endpoint): + repourl = getUpstreamRepoURI() + url = ghUrl(endpoint, repourl) + filepath = getFilePathForURL(url) + proc_handle[filehash] = subprocess.Popen( + shlex.split("curl " + url), + stdout=open(filepath, "w"), + stderr=open(os.devnull, 'wb') + ) + proc_pending[filehash] = True + + +def cacheFromDisk(endpoint): + repourl = getUpstreamRepoURI() + url = ghUrl(endpoint, repourl) + filepath = getFilePathForURL(url) + try: + jsonfile = open(filepath) + data = json.load(jsonfile) + api_cache[repourl + "/" + endpoint] = data + return data + except Exception as e: + return None + + +def grabCacheData(endpoint): + repourl = getUpstreamRepoURI() + url = ghUrl(endpoint, repourl) + filepath = getFilePathForURL(url) + + # If something was pending and we have fresh data, + # replace what is in memory with the disk data + if proc_pending.get(filepath): + if proc_handle[filepath].poll() is not None: + proc_pending[filepath] = False + return cacheFromDisk(filepath) + + if api_cache.get(repourl + "/" + endpoint): + return api_cache[repourl + "/" + endpoint] + + apiToDiskAsync(endpoint) + return cacheFromDisk(endpoint) + + +def getContributors(): + return ghApi("/stats/contributors") + +# adds issues, labels, and contributors to omni dictionary + + +def doPopulateOmniComplete(): + url = getUpstreamRepoURI() + + if url == "": + return + + issues = getIssueList(url, "/issues", 0) + for issue in issues: + addToOmni(str(issue["number"]) + " " + issue["title"], 'Issue') + + labels = getLabels() + if labels is not None: + for label in labels: + addToOmni(unicode(label["name"]), 'Label') + + contributors = getContributors() + if contributors is not None: + for contributor in contributors: + addToOmni(str(contributor["author"]["login"]), 'user') + + milestones = getMilestoneList(url) + if milestones is not None: + for milestone in milestones: + addToOmni(str(milestone["title"].encode('utf-8')), 'Milestone') + +# calls populateOmniComplete asynchronously + + +def populateOmniCompleteAsync(): + thread = AsyncOmni() + thread.start() + + +class AsyncOmni(threading.Thread): + def run(self): + # Download and cache the omnicomplete data + url = getUpstreamRepoURI() + + if url == "": + return + + issues = getIssueList(url, '/issues', 0) + labels = getLabels() + contributors = getContributors() + milestones = getMilestoneList(url) + +# adds to omni dictionary. used by populateOmniComplete + + +def addToOmni(keyword, typ): + vim.command("call add(b:omni_options, " + + json.dumps({'word': keyword, 'menu': '[' + typ + ']'}) + ")") + + +def showIssueLink(number, url="", split="False"): + if url != "": + repourl = url + else: + repourl = getUpstreamRepoURI() + + # convert string to boolean + split = split == "True" + word = vim.eval("expand('')") + + line = vim.eval("getline(\".\")") + if line == SHOW_COMMITS: + showCommits(split) + elif line == SHOW_FILES_CHANGED: + showFilesChanged(split) + elif len(word) == 40: + showCommit(word, split) + + return + +# handle user pressing enter on the gissue list +# possible actions: view issue, filter by label, filter by assignees, +# remove filters + + +def showIssueBuffer(number, url=False): + if url: + repourl = url + elif 'search/issues' in vim.current.buffer.name: + line_number = vim.eval("line('.')-1") + html_url = globissues[int(line_number)]['html_url'] + html_url_split = html_url.split("/") + repourl = html_url_split[3] + "/" + html_url_split[4] + else: + repourl = getUpstreamRepoURI() + + line = vim.eval("getline(\".\")") + if line == SHOW_ALL: + showIssueList(0, "True") + return + if line == SHOW_ASSIGNED_ME: + showIssueList(0, "True", "True") + return + + labels = getLabels() + if labels is not None: + for label in labels: + if str(label["name"]) == number: + showIssueList(number, "True") + return + + if number != "new": + vim.command("normal! 0") + number = vim.eval("expand('')") + + if not vim.eval("g:github_same_window") == "1": + cmd = 'silent ' + if not vim.eval("g:gissues_issue_vsplit") == "1": + spl = 'new +set\ buftype=nofile' + if vim.eval("g:gissues_split_height") != "0": + spl = ' ' + vim.eval("g:gissues_split_height") + spl + cmd = cmd + spl + else: + spl = 'vnew +set\ buftype=nofile' + if vim.eval("g:gissues_vsplit_width") != "0": + spl = ' ' + vim.eval("g:gissues_vsplit_width") + spl + cmd = cmd + spl + if vim.eval("g:gissues_split_expand") == "1": + cmd = 'botright ' + cmd + vim.command(cmd) + + vim.command("edit gissues/" + repourl + "/" + number) + +# show an issue buffer in detail + + +def showIssue(number=False, repourl=False): + if repourl is False: + repourl = getUpstreamRepoURI() + + if number is False: + parens = getFilenameParens() + number = parens[2] + + b = vim.current.buffer + vim.command("normal ggdG") + + if number == "new": + # new issue + issue = {'title': '', + 'body': '', + 'number': 'new', + 'user': { + 'login': '' + }, + 'assignees': '', + 'state': 'open', + 'labels': [] + } + else: + url = ghUrl("/issues/" + number, repourl) + issue = json.loads(urllib2.urlopen(url, timeout=2).read()) + if "pull_request" in issue: + pull_request_url = ghUrl("/pulls/" + number, repourl) + pull_request = json.loads( + urllib2.urlopen( + pull_request_url, + timeout=2).read()) + vim.command("let b:ghissue_url=\"" + issue["html_url"] + "\"") + vim.command("let b:ghissue_number=" + number) + vim.command("let b:ghissue_repourl=\"" + repourl + "\"") + + encoding = vim.eval("&encoding") + + b.append("## Title: " + + issue["title"].encode(encoding).decode(encoding) + + " (" + + str(issue["number"]) + + ")") + if issue["user"]["login"]: + b.append( + "## Reported By: " + + issue["user"]["login"].encode(encoding).decode(encoding)) + + b.append("## State: " + issue["state"]) + if issue['assignees']: + b.append( + "## Assignees: " + + ' '.join( + i["login"].encode(encoding).decode(encoding) for i in issue["assignees"])) + else: + b.append("## Assignees: ") + + if number == "new": + b.append("## Milestone: ") + elif issue['milestone']: + b.append("## Milestone: " + str(issue["milestone"]["title"])) + + labelstr = "" + if issue["labels"]: + for label in issue["labels"]: + labelstr += label["name"] + ", " + b.append("## Labels: " + labelstr[:-2]) + + if number != "new" and "pull_request" in issue: + b.append("## Branch Name: " + str(pull_request["head"]["ref"])) + b.append(SHOW_COMMITS) + b.append(SHOW_FILES_CHANGED) + + # This part breaks Python 3 + if issue["body"]: + lines = issue["body"].encode(encoding).decode(encoding).split("\n") + b.append(map(lambda line: line.rstrip(), lines)) + + if number != "new": + b.append("## Comments") + + url = ghUrl("/issues/" + number + "/comments", repourl) + data = urllib2.urlopen(url, timeout=2).read() + comments = json.loads(data) + + url = ghUrl("/issues/" + number + "/events", repourl) + data = urllib2.urlopen(url, timeout=2).read() + events = json.loads(data) + + events = comments + events + + if len(events) > 0: + for event in events: + b.append("") + if "user" in event: + user = event["user"]["login"] + else: + user = event["actor"]["login"] + + b.append(user.encode(encoding).decode( + encoding) + "(" + event["created_at"] + ")") + + if "body" in event: + lines = event["body"].encode( + encoding).decode(encoding).split("\n") + b.append(map(lambda line: line.rstrip(), lines)) + else: + eventstr = event["event"].encode(encoding).decode(encoding) + if "commit_id" in event and event["commit_id"]: + eventstr += " from " + event["commit_id"] + b.append(eventstr) + + else: + b.append("") + b.append("No comments.") + b.append("") + + b.append("## Add a comment") + b.append("") + + vim.command("nnoremap q :q") + vim.command("set ft=gfimarkdown") + vim.command("normal ggdd") + + highlightColoredLabels(getLabels()) + + # mark it as "saved" + vim.command("setlocal nomodified") + +# saves an issue and pushes it to the server + + +def saveGissue(): + token = vim.eval("g:github_access_token") + if not token: + print("github-issues.vim: In order to save an issue or add a comment, you need to define a GitHub token. See: https://github.com/jaxbot/github-issues.vim#configuration") + return + + parens = getFilenameParens() + number = parens[2] + encoding = "utf-8" # TODO: Get this from vim + + issue = { + 'title': '', + 'body': '', + 'assignees': '', + 'labels': '', + 'milestone': '' + } + + commentmode = 0 + + comment = "" + + for line in vim.current.buffer: + if commentmode == 1: + if line == "## Add a comment": + commentmode = 2 + continue + if commentmode == 2: + if line != "": + commentmode = 3 + comment += line + "\n" + continue + if commentmode == 3: + comment += line + "\n" + continue + + if line == "## Comments": + commentmode = 1 + continue + if len(line.split("## Reported By:")) > 1: + continue + + title = line.split("## Title:") + if len(title) > 1: + issue['title'] = title[1].strip().split(" (" + number + ")")[0] + continue + + state = line.split("## State:") + if len(state) > 1: + if state[1].strip().lower() == "closed": + issue['state'] = "closed" + else: + issue['state'] = "open" + continue + + milestone = line.split("## Milestone:") + if len(milestone) > 1: + milestones = getMilestoneList(parens[0] + "/" + parens[1], "") + + milestone = milestone[1].strip() + + for mstone in milestones: + if mstone["title"] == milestone: + issue['milestone'] = str(mstone["number"]) + break + continue + + labels = line.split("## Labels:") + if len(labels) > 1: + issue['labels'] = labels[1].lstrip().split(', ') + continue + + assignees = line.split("## Assignees:") + if len(assignees) > 1: + issue['assignees'] = assignees[1].lstrip().split(' ') + continue + + if line == SHOW_COMMITS or line == SHOW_FILES_CHANGED or "## Branch Name:" in line: + continue + + if issue['body'] != '': + issue['body'] += '\n' + issue['body'] += line + + # remove blank entries + issue['labels'] = filter(bool, issue['labels']) + + if number == "new": + + if issue['assignees'] == '': + del issue['assignees'] + if issue['milestone'] == '': + del issue['milestone'] + if issue['body'] == '': + del issue['body'] + + data = "" + try: + repourl = getUpstreamRepoURI() + url = ghUrl("/issues", repourl) + data = json.dumps(issue).encode(encoding) + request = urllib2.Request(url, data) + data = json.loads(urllib2.urlopen(request, timeout=2).read()) + parens[2] = str(data['number']) + vim.current.buffer.name = "gissues/" + \ + parens[0] + "/" + parens[1] + "/" + parens[2] + except urllib2.HTTPError as e: + if "code" in e and e.code == 410 or e.code == 404: + print( + "github-issues.vim: Error creating issue. Do you have a github_access_token defined?") + else: + print("github-issues.vim: Unknown HTTP error:") + print(e) + print(data) + print(url) + print(issue) + else: + repourl = vim.eval("b:ghissue_repourl") + url = ghUrl("/issues/" + number, repourl) + data = json.dumps(issue).encode(encoding) + # TODO: Fix POST data should be bytes. + request = urllib2.Request(url, data) + request.get_method = lambda: 'PATCH' + try: + urllib2.urlopen(request, timeout=2) + except urllib2.HTTPError as e: + if "code" in e and e.code == 410 or e.code == 404: + print("Could not update the issue as it does not belong to you!") + + if commentmode == 3: + try: + url = ghUrl("/issues/" + parens[2] + "/comments", repourl) + data = json.dumps({'body': comment}).encode(encoding) + request = urllib2.Request(url, data) + urllib2.urlopen(request, timeout=2) + except urllib2.HTTPError as e: + if "code" in e and e.code == 410 or e.code == 404: + print( + "Could not post comment. Do you have a github_access_token defined?") + + if commentmode == 3 or number == "new": + showIssue() + + # mark it as "saved" + vim.command("setlocal nomodified") + +# updates an issues data, such as opening/closing + + +def setIssueData(issue): + parens = getFilenameParens() + repourl = getUpstreamRepoURI() + url = ghUrl("/issues/" + parens[2], repourl) + request = urllib2.Request(url, json.dumps(issue)) + request.get_method = lambda: 'PATCH' + urllib2.urlopen(request, timeout=2) + + showIssue() + + +def getLabels(): + return ghApi("/labels") + + +def getContributors(): + return ghApi("/stats/contributors") + +# adds labels to the match system + + +def highlightColoredLabels(labels, decorate=False): + if not labels: + labels = [] + + labels.append({'name': 'closed', 'color': 'ff0000'}) + labels.append({'name': 'open', 'color': '00aa00'}) + + for label in labels: + # Dummy failsafe value + xtermcolor = "242" + + # If we've loaded xterm colors, calculate one + if vim.eval("g:gissues_xterm_colors") != "0": + xtermcolor = rgb_to_xterm(label["color"])[1:] + + vim.command( + "hi issueColor" + + label["color"] + + " ctermbg=" + + xtermcolor + + " guifg=#ffffff guibg=#" + + label["color"]) + name = label["name"] + if decorate: + name = "\\\\[" + name + "\\\\]" + else: + name = "\\\\<" + name + "\\\\>" + vim.command( + "let m = matchadd(\"issueColor" + + label["color"] + + "\", \"" + + name + + "\")") + vim.command("hi issueButton guifg=#ffffff guibg=#333333 ctermbg=DarkGray") + vim.command("let m = matchadd(\"issueButton\", \"\\\\[.*how.*\\\\]\")") + +# queries the ghApi for + + +def ghApi(endpoint, repourl=False, cache=True, repo=True): + global ssl_enabled + data = None + + if not repourl: + repourl = getUpstreamRepoURI() + + if cache and api_cache.get(repourl + "/" + endpoint): + return api_cache[repourl + "/" + endpoint] + + if not ssl_enabled: + try: + import ssl + ssl_enabled = True + except BaseException: + print("SSL appears to be disabled or not installed on this machine. Please reinstall Python and/or Vim.") + + url = ghUrl(endpoint, repourl, repo) + filepath = getFilePathForURL(url) + if vim.eval("g:gissues_offline_cache") and cache: + try: + jsonfile = open(filepath) + data = json.load(jsonfile) + return data + except Exception as e: + # fallback to downloading + pass + + try: + req = urllib2.urlopen(url, timeout=5) + request_data = req.read() + data = json.loads(request_data) + + cache_file = open(filepath, "w") + cache_file.write(request_data) + cache_file.close() + + api_cache[repourl + "/" + endpoint] = data + + return data + except Exception as e: + if vim.eval("g:gissues_offline_cache") == "1": + try: + jsonfile = open(filepath) + data = json.load(jsonfile) + print("Github-issues.vim is in OFFLINE mode") + return data + except Exception as e: + pass + if vim.eval("g:gissues_show_errors") != "0": + print( + "github-issues.vim: An error occurred. If this is a private repo, make sure you have a github_access_token defined. Call: " + + endpoint + + " on " + + repourl) + print(e) + return None + + +def getFilePathForURL(url): + return os.path.expanduser("~/.vim/.gissue-cache/") + \ + hashlib.sha224(url.encode('utf-8')).hexdigest() + +# generates a github URL, including access token + + +def ghUrl(endpoint, repourl=False, repo=True): + params = "" + token = vim.eval("g:github_access_token") + if token: + if "?" in endpoint: + params = "&" + else: + params = "?" + params += "access_token=" + token + + if repo: + repourl = "repos/" + repourl + + if repourl == "custom": + return vim.eval("g:github_api_url") + endpoint + params + else: + return vim.eval("g:github_api_url") + \ + urllib2.quote(repourl) + endpoint + params + +# returns an array of parens after gissues in filename + + +def getFilenameParens(): + return vim.current.buffer.name.replace( + "\\", "/").split("gissues/")[1].split("/") + + +def createDirectory(path): + try: + os.makedirs(path) + except OSError: + if not os.path.isdir(path): + raise + + +if vim.eval("g:gissues_offline_cache") == "1": + createDirectory(os.path.expanduser("~/.vim")) + createDirectory(os.path.expanduser("~/.vim/.gissue-cache")) diff --git a/bundle/github-issues.vim/syntax/gfimarkdown.vim b/bundle/github-issues.vim/syntax/gfimarkdown.vim new file mode 100644 index 000000000..b7768ed9d --- /dev/null +++ b/bundle/github-issues.vim/syntax/gfimarkdown.vim @@ -0,0 +1,125 @@ +" Vim syntax file +" Language: Github Flavored Markdown plus Issues +" Markdown.vim Maintainer: Tim Pope +" GFM Maintainer: Jeff Tratner +" GFM plus issues Maintainer: Jonathan Warner +" Same as GFM, but with issue highlighting and such + +if exists("b:current_syntax") + finish +endif + +if !exists('main_syntax') + let main_syntax = 'ghmarkdown' +endif + +runtime! syntax/html.vim +unlet! b:current_syntax + +syn sync minlines=10 +syn case ignore + +syn match markdownValid '[<>]\c[a-z/$!]\@!' +syn match markdownValid '&\%(#\=\w*;\)\@!' + +syn match markdownLineStart "^[<@]\@!" nextgroup=@markdownBlock,htmlSpecialChar + +syn cluster markdownBlock contains=markdownH1,markdownH2,markdownH3,markdownH4,markdownH5,markdownH6,markdownBlockquote,markdownListMarker,markdownOrderedListMarker,markdownCodeBlock,markdownRule,markdownGHCodeBlock +syn cluster markdownInline contains=markdownLineBreak,markdownLinkText,markdownItalic,markdownBold,markdownCode,markdownEscape,@htmlTop,markdownError + +syn match markdownH1 "^.\+\n=\+$" contained contains=@markdownInline,markdownHeadingRule,markdownAutomaticLink +syn match markdownH2 "^.\+\n-\+$" contained contains=@markdownInline,markdownHeadingRule,markdownAutomaticLink + +syn match markdownHeadingRule "^[=-]\+$" contained + +syn region markdownH1 matchgroup=markdownHeadingDelimiter start="##\@!" end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained +syn region markdownH2 matchgroup=markdownHeadingDelimiter start="###\@!" end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained +syn region markdownH3 matchgroup=markdownHeadingDelimiter start="####\@!" end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained +syn region markdownH4 matchgroup=markdownHeadingDelimiter start="#####\@!" end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained +syn region markdownH5 matchgroup=markdownHeadingDelimiter start="######\@!" end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained +syn region markdownH6 matchgroup=markdownHeadingDelimiter start="#######\@!" end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained + +syn match markdownBlockquote ">\%(\s\|$\)" contained nextgroup=@markdownBlock + +syn region markdownCodeBlock start=" \|\t" end="$" contained + +" TODO: real nesting +syn match markdownListMarker "\%(\t\| \{0,4\}\)[-*+]\%(\s\+\S\)\@=" contained +syn match markdownOrderedListMarker "\%(\t\| \{0,4}\)\<\d\+\.\%(\s\+\S\)\@=" contained + +syn match markdownRule "\* *\* *\*[ *]*$" contained +syn match markdownRule "- *- *-[ -]*$" contained + +syn match markdownLineBreak " \{2,\}$" + +syn region markdownIdDeclaration matchgroup=markdownLinkDelimiter start="^ \{0,3\}!\=\[" end="\]:" oneline keepend nextgroup=markdownUrl skipwhite +syn match markdownUrl "\S\+" nextgroup=markdownUrlTitle skipwhite contained +syn region markdownUrl matchgroup=markdownUrlDelimiter start="<" end=">" oneline keepend nextgroup=markdownUrlTitle skipwhite contained +syn region markdownUrlTitle matchgroup=markdownUrlTitleDelimiter start=+"+ end=+"+ keepend contained +syn region markdownUrlTitle matchgroup=markdownUrlTitleDelimiter start=+'+ end=+'+ keepend contained +syn region markdownUrlTitle matchgroup=markdownUrlTitleDelimiter start=+(+ end=+)+ keepend contained + +syn region markdownLinkText matchgroup=markdownLinkTextDelimiter start="!\=\[\%(\_[^]]*]\%( \=[[(]\)\)\@=" end="\]\%( \=[[(]\)\@=" keepend nextgroup=markdownLink,markdownId skipwhite contains=@markdownInline,markdownLineStart +syn region markdownLink matchgroup=markdownLinkDelimiter start="(" end=")" contains=markdownUrl keepend contained +syn region markdownId matchgroup=markdownIdDelimiter start="\[" end="\]" keepend contained +syn region markdownAutomaticLink matchgroup=markdownUrlDelimiter start="<\%(\w\+:\|[[:alnum:]_+-]\+@\)\@=" end=">" keepend oneline + +syn region markdownItalic start="\<\*\|\*\>" end="\<\*\|\*\>" keepend contains=markdownLineStart +syn region markdownItalic start="\<_\|_\>" end="\<_\|_\>" keepend contains=markdownLineStart +syn region markdownBold start="\<\*\*\|\*\*\>" end="\<\*\*\|\*\*\>" keepend contains=markdownLineStart,markdownItalic +syn region markdownBold start="\<__\|__\>" end="\<__\|__\>" keepend contains=markdownLineStart,markdownItalic +syn region markdownBoldItalic start="\<\*\*\*\|\*\*\*\>" end="\<\*\*\*\|\*\*\*\>" keepend contains=markdownLineStart +syn region markdownBoldItalic start="\<___\|___\>" end="\<___\|___\>" keepend contains=markdownLineStart +syn region markdownCode matchgroup=markdownCodeDelimiter start="`" end="`" keepend contains=markdownLineStart +syn region markdownCode matchgroup=markdownCodeDelimiter start="`` \=" end=" \=``" keepend contains=markdownLineStart +syn region markdownGHCodeBlock matchgroup=markdownCodeDelimiter start="^\s*$\n\s*```\s\?\S*\s*$" end="\s*```$\n\s*\n" contained keepend +syn region markdownGHCodeBlock matchgroup=markdownCodeDelimiter start="^\s*```.*$" end="^\s*```\ze\s*$" keepend + +syn match markdownEscape "\\[][\\`*_{}()#+.!-]" + +" github-issues.vim stuff: +" commit hash +syn match Constant "[A-Za-z0-9]\{40}" +" issue number +syn match Constant "\#[0-9]\+" + +" Copying rst's method of using literal strings +hi def link markdownGHCodeBlock String +hi def link markdownCodeBlock String +hi def link markdownCode String +hi def link markdownH1 htmlH1 +hi def link markdownH2 htmlH2 +hi def link markdownH3 htmlH3 +hi def link markdownH4 htmlH4 +hi def link markdownH5 htmlH5 +hi def link markdownH6 htmlH6 +hi def link markdownHeadingRule markdownRule +hi def link markdownHeadingDelimiter Delimiter +hi def link markdownOrderedListMarker markdownListMarker +hi def link markdownListMarker htmlTagName +hi def link markdownBlockquote Comment +hi def link markdownRule PreProc + +hi def link markdownLinkText htmlLink +hi def link markdownIdDeclaration Typedef +hi def link markdownId Type +hi def link markdownAutomaticLink markdownUrl +hi def link markdownUrl Float +hi def link markdownUrlTitle String +hi def link markdownIdDelimiter markdownLinkDelimiter +hi def link markdownUrlDelimiter htmlTag +hi def link markdownUrlTitleDelimiter Delimiter + +hi def link markdownItalic htmlItalic +hi def link markdownBold htmlBold +hi def link markdownBoldItalic htmlBoldItalic +hi def link markdownCodeDelimiter Delimiter + +hi def link markdownEscape Special + +let b:current_syntax = "gfimarkdown" + +if main_syntax ==# 'gfimarkdown' + unlet main_syntax +endif +" vim:set sw=2: diff --git a/bundle/github-issues.vim/test/test.vim b/bundle/github-issues.vim/test/test.vim new file mode 100644 index 000000000..89d342526 --- /dev/null +++ b/bundle/github-issues.vim/test/test.vim @@ -0,0 +1,47 @@ +function! s:run_tests() + exec ":Gissues" + let line = getline(".") + + call s:expectEqual(getline("."), "No results found in jaxbot/test") + + exec ":Giadd" + + call s:expectEqual(getline("."), "# (new)") + + exec "normal iTest Issue for Gissues" + exec ":w" + + call s:expectEqual(getline("7"), "No comments.") + + exec ":4" + normal $a enhancement + normal GoTest comment + + exec ":w" + call s:expectEqual(getline("8"), "Test comment") + call s:expectEqual(getline("4"), "## Labels: enhancement") + + exec ":Gissues" + normal ee + call s:expectEqual(expand(""), "Test") + + exec "normal \" + + call s:expectEqual(getline("3"), "## State: open") + normal cc + call s:expectEqual(getline("3"), "## State: closed") + + echom "Finished!" + exec ":messages" +endfunction + +function! s:expectEqual(actual, expected) + if a:actual == a:expected + else + throw "Assertion failed: Expected " . a:expected . ", got " . a:actual + endif +endfunction + +edit ~/www/test/README.md +call s:run_tests() +