├── LICENSE ├── README.md ├── autoload └── denite │ └── git.vim ├── doc └── denite-git.txt └── rplugin └── python3 └── denite └── source ├── gitbranch.py ├── gitchanged.py ├── gitfiles.py ├── gitlog.py └── gitstatus.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 chemzqm@gmail.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 19 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Denite-git 2 | 3 | [![](http://img.shields.io/github/issues/neoclide/denite-git.svg)](https://github.com/neoclide/denite-git/issues) 4 | [![](http://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 5 | [![](https://img.shields.io/badge/doc-%3Ah%20denite--git.txt-red.svg)](doc/denite-git.txt) 6 | 7 | Git log, git status and git changed source for [Denite.nvim](https://github.com/Shougo/denite.nvim). 8 | 9 | Video of denite gitlog: 10 | 11 | [![asciicast](https://asciinema.org/a/104395.png)](https://asciinema.org/a/104395) 12 | 13 | Video of denite gitstatus: 14 | 15 | [![asciicast](https://asciinema.org/a/104410.png)](https://asciinema.org/a/104410) 16 | 17 | ## Installation 18 | 19 | For user of [vim-plug](https://github.com/junegunn/vim-plug), add: 20 | 21 | Plug 'Shougo/denite.nvim' 22 | Plug 'chemzqm/denite-git' 23 | 24 | For user of [dein.vim](https://github.com/Shougo/dein.vim), add: 25 | 26 | call dein#add('Shougo/denite.nvim') 27 | call dein#add('chemzqm/denite-git') 28 | 29 | to your vimrc and run `PlugInstall` if needed. 30 | 31 | ## Usage 32 | 33 | For git log: 34 | 35 | ``` vim 36 | " log of current file 37 | Denite gitlog 38 | 39 | " all git log of current repository 40 | Denite gitlog:all 41 | 42 | " filter gitlog with fix as input 43 | Denite gitlog::fix 44 | ``` 45 | 46 | For git status: 47 | 48 | ``` vim 49 | Denite gitstatus 50 | ``` 51 | 52 | For git changed 53 | 54 | ``` vim 55 | Denite gitchanged 56 | ``` 57 | 58 | `gitchanged` source is just simple line source. 59 | 60 | For git branch 61 | 62 | ``` 63 | Denite gitbranch 64 | ``` 65 | 66 | For git files 67 | ```vim 68 | " view different versions of files on different branches (or commits, or tags) 69 | Denite gitfiles 70 | 71 | " all files in repo on master 72 | Denite gitfiles:master 73 | 74 | " all files in repo as of sha 31a3b3 75 | Denite gitfiles:31a3b3 76 | 77 | ``` 78 | 79 | 80 | ## Actions 81 | 82 | Actions of gitlog: 83 | 84 | * `open` default action for open seleted commit. 85 | * `preview` preview seleted commit. 86 | * `delete` run git diff with current commit for current buffer. (just named delete) 87 | * `reset` run git reset with current commit. 88 | 89 | Actions of gitstatus: 90 | 91 | * `open` open seleted file, default action 92 | * `add` run git add for seleted file(s). 93 | * `delete` run git diff for seleted file. (just named delete) 94 | * `reset` run git reset/checkout or remove for seleted file(s). 95 | * `commit` run git commit for seleted file(s). 96 | 97 | Actions of gitbranch: 98 | 99 | * `checkout` default action to checkout selected branch. 100 | * `delete` delete seleted branch. 101 | * `merge` merge seleted branch with current branch. 102 | * `rebase` rebase seleted branch with current branch. 103 | 104 | Actions of gitfiles: 105 | * `view` default action to view a file at a certain commit (read-only) 106 | 107 | ## Key Mapppings 108 | 109 | It's recommanded to add custom key mappings for improve your speed of 110 | interacting with denite source, for example: 111 | 112 | ``` viml 113 | call denite#custom#map( 114 | \ 'normal', 115 | \ 'a', 116 | \ '', 117 | \ 'noremap' 118 | \) 119 | 120 | call denite#custom#map( 121 | \ 'normal', 122 | \ 'd', 123 | \ '', 124 | \ 'noremap' 125 | \) 126 | 127 | call denite#custom#map( 128 | \ 'normal', 129 | \ 'r', 130 | \ '', 131 | \ 'noremap' 132 | \) 133 | ``` 134 | -------------------------------------------------------------------------------- /autoload/denite/git.vim: -------------------------------------------------------------------------------- 1 | 2 | function! denite#git#gitdir() abort 3 | let gitdir = get(b:, 'denite_git_git_dir', '') 4 | if !empty(gitdir) | return gitdir | endif 5 | let path = (empty(bufname('%')) || &buftype =~# '^\%(nofile\|acwrite\|quickfix\|terminal\)$') ? getcwd() : expand('%:p') 6 | let dir = finddir('.git', path.';') 7 | if empty(dir) | return '' | endif 8 | let files = findfile('.git', path.';',-1) 9 | if empty(files) | return fnamemodify(dir, ':p:h') | endif 10 | return fnamemodify(files[-1], ':p') 11 | endfunction 12 | 13 | function! denite#git#commit(prefix, files) abort 14 | if get(g:, 'loaded_fugitive', 0) 15 | execute 'Gcommit '.a:prefix .' ' . join(map(a:files, 'fnameescape(v:val)'), ' ') 16 | elseif get(g:, 'did_easygit_loaded', 0) 17 | call easygit#commit(a:prefix . ' '. join(a:files, ' ')) 18 | else 19 | execute 'terminal git commit '.a:prefix. ' '. join(map(a:files, 'fnameescape(v:val)'), ' ') 20 | endif 21 | endfunction 22 | 23 | function! denite#git#diffPreview(prefix, file, gitdir) abort 24 | let file = tempname() 25 | call system('git --no-pager --git-dir='.a:gitdir.' diff '.a:prefix. ' ' . fnameescape(a:file). ' > '.file) 26 | if v:shell_error 27 | return 28 | endif 29 | execute 'vs +setl\ previewwindow '.file 30 | setl filetype=diff 31 | setl nofoldenable 32 | endfunction 33 | 34 | function! denite#git#reset(args, gitdir) abort 35 | call system('git --git-dir='.a:gitdir.' reset '.a:args) 36 | if v:shell_error | return | endif 37 | checktime 38 | endfunction 39 | 40 | function! denite#git#diffCurrent(revision, option) abort 41 | let gitdir = get(a:option, 'gitdir', '') 42 | if empty(gitdir) | return | endif 43 | let ref = len(a:revision) ? a:revision : 'head' 44 | let edit = a:0 ? a:1 : 'vsplit' 45 | let ft = &filetype 46 | let bnr = bufnr('%') 47 | let root = fnamemodify(gitdir, ':h') 48 | let file = substitute(expand('%:p'), root . '/', '', '') 49 | let command = 'git --no-pager --git-dir='. gitdir 50 | \. ' show --no-color ' 51 | \. ref . ':' . file 52 | let edit = get(a:option, 'edit', 'edit') 53 | let output = system(command) 54 | let list = split(output, '\v\r?\n') 55 | if !len(list)| diffoff | return | endif 56 | diffthis 57 | execute 'keepalt '.edit.' +setl\ buftype=nofile [[Git '.a:revision.']]' 58 | call setline(1, list[0]) 59 | silent! call append(1, list[1:]) 60 | execute 'setf ' . ft 61 | diffthis 62 | let b:denite_git_git_dir = gitdir 63 | setl foldenable 64 | call setwinvar(winnr(), 'easygit_diff_origin', bnr) 65 | call setpos('.', [bufnr('%'), 0, 0, 0]) 66 | endfunction 67 | 68 | function! denite#git#show(args, option) 69 | let fold = get(a:option, 'fold', 1) 70 | let gitdir = get(a:option, 'gitdir', '') 71 | let showall = get(a:option, 'all', 0) 72 | if empty(gitdir) | return | endif 73 | let format = "--pretty=format:'".s:escape("commit %H%nparent %P%nauthor %an <%ae> %ad%ncommitter %cn <%ce> %cd%n %e%n%n%s%n%n%b")."' " 74 | if showall 75 | let command = 'git --no-pager --git-dir=' . gitdir 76 | \. ' show --no-color ' . format . a:args 77 | else 78 | let root = fnamemodify(gitdir, ':h') 79 | let file = get(a:option, 'file', '') 80 | let command = 'git --no-pager --git-dir=' . gitdir 81 | \. ' show --no-color ' . format . a:args . ' -- ' . file 82 | endif 83 | let edit = get(a:option, 'edit', 'edit') 84 | let output = system(command) 85 | let list = split(output, '\v\r?\n') 86 | if !len(list)| return | endif 87 | execute 'keepalt '.edit.' +setl\ buftype=nofile [[Git '.a:args.']]' 88 | call setline(1, list[0]) 89 | silent! call append(1, list[1:]) 90 | setlocal filetype=git foldmethod=syntax readonly bufhidden=wipe 91 | if !showall 92 | setl nofoldenable 93 | endif 94 | if get(a:option, 'floating_preview', 0) && exists('*nvim_win_set_config') 95 | call nvim_win_set_config(win_getid(), { 96 | \ 'relative': 'editor', 97 | \ 'row': a:option.preview_win_row, 98 | \ 'col': a:option.preview_win_col, 99 | \ 'width': a:option.preview_width, 100 | \ 'height': a:option.preview_height, 101 | \ }) 102 | doautocmd User denite-preview 103 | endif 104 | call setpos('.', [bufnr('%'), 7, 0, 0]) 105 | exe 'nnoremap u :call ShowParentCommit()' 106 | exe 'nnoremap d :call ShowNextCommit()' 107 | let b:denite_git_git_dir = gitdir 108 | endfunction 109 | 110 | function! s:ShowParentCommit() abort 111 | let commit = matchstr(getline(2), '\v\s\zs.+$') 112 | if empty(commit) | return | endif 113 | call denite#git#show(commit, { 114 | \ 'eidt': 'edit', 115 | \ 'gitdir': b:denite_git_git_dir, 116 | \ 'all': 1, 117 | \}) 118 | endfunction 119 | 120 | function! s:ShowNextCommit() abort 121 | let commit = matchstr(getline(1), '\v\s\zs.+$') 122 | let commit = s:NextCommit(commit, b:denite_git_git_dir) 123 | if empty(commit) | return | endif 124 | call denite#git#show(commit, { 125 | \ 'eidt': 'edit', 126 | \ 'gitdir': b:denite_git_git_dir, 127 | \ 'all': 1, 128 | \}) 129 | endfunction 130 | 131 | function! s:NextCommit(commit, gitdir) abort 132 | let output = system('git --git-dir=' . a:gitdir 133 | \. ' log --reverse --ancestry-path ' 134 | \. a:commit . '..master | head -n 1 | cut -d \ -f 2') 135 | if v:shell_error && output !=# "" 136 | echohl Error | echon output | echohl None 137 | return 138 | endif 139 | return substitute(output, '\n', '', '') 140 | endfunction 141 | 142 | function! s:winshell() abort 143 | return &shell =~? 'cmd' || exists('+shellslash') && !&shellslash 144 | endfunction 145 | 146 | function! s:escape(str) 147 | if s:winshell() 148 | let cmd_escape_char = &shellxquote == '(' ? '^' : '^^^' 149 | return substitute(a:str, '\v\C[<>]', cmd_escape_char, 'g') 150 | endif 151 | return a:str 152 | endfunction 153 | -------------------------------------------------------------------------------- /doc/denite-git.txt: -------------------------------------------------------------------------------- 1 | *denite-git.txt* Git log and git status sources for |denite| 2 | 3 | Version: 0.4 4 | Author: Qiming Zhao 5 | License: MIT license 6 | 7 | CONTENTS *denite-git-contents* 8 | 9 | Introduction |denite-git-introduction| 10 | Install |denite-git-install| 11 | Usage |denite-git-usage| 12 | Actions |denite-git-actions| 13 | gitlog |denite-gitlog-actions| 14 | gitstatus |denite-gitstatus-actions| 15 | gitchanged |denite-gitchanged-actions| 16 | gitbranch |denite-gitbranch-actions| 17 | Changelog |denite-git-changelog| 18 | Feedback |denite-feedback| 19 | 20 | ============================================================================== 21 | INTRODUCTION *denite-git-introduction* 22 | 23 | Denite-git is a vim/neovim plugin for user to easily manage git log, git 24 | status and git changed lines with the help of |denite| interface. 25 | 26 | ============================================================================== 27 | INSTALL *denite-git-install* 28 | 29 | Make sure you have "echo has('python3')" return 1 before useing |denite|. 30 | 31 | denite.nvim is required for provide union interface and. 32 | 33 | Take [vim-plug](https://github.com/junegunn/vim-plug) for example, add: > 34 | 35 | Plug 'Shougo/denite.nvim' 36 | Plug 'neoclide/denite-git' 37 | 38 | To your .vimrc and run "PlugInstall" after vim restarted. 39 | 40 | Note: |denite-git| requires python >= 3.5. 41 | 42 | ============================================================================== 43 | USAGE *denite-git-usage* 44 | 45 | For gitlog source: > 46 | 47 | " git log of current file 48 | Denite gitlog 49 | 50 | " all git log of current repository 51 | Denite gitlog:all 52 | 53 | " filter gitlog with fix as input 54 | Denite gitlog::fix 55 | 56 | For gitstatus source: > 57 | 58 | Denite gitstatus 59 | 60 | For gitchanged source: > 61 | 62 | Denite gitchanged 63 | 64 | For gitbranch source: > 65 | 66 | Denite gitbranch 67 | 68 | Note: denite-git find git root in the directory of vim current working 69 | directory ":echo getcwd()" 70 | 71 | ============================================================================== 72 | ACTIONS *denite-git-actions* 73 | 74 | ------------------------------------------------------------------------------ 75 | GITLOG ACTIONS *denite-gitlog-actions* 76 | 77 | open (default) 78 | Open seleted commit (with current file diff only if not all). 79 | 80 | preview 81 | Preview seleted commit in preview window. 82 | 83 | delete 84 | Run git diff with current commit(s) for current buffer. 85 | 86 | Note: three way diff is possible. 87 | 88 | reset 89 | Run git reset command with current commit. 90 | 91 | The Kind of |denite-gitlog| is inherited from openable, so all openable 92 | actions are available. 93 | 94 | ------------------------------------------------------------------------------ 95 | GITSTATUS ACTIONS *denite-gitstatus-actions* 96 | 97 | open (default) 98 | Open seleted file, default action 99 | 100 | add 101 | Run git add for seleted file(s). 102 | 103 | delete 104 | Run git diff for seleted file. (just named delete) 105 | 106 | reset 107 | Run git reset/checkout or remove for seleted file(s). 108 | A prompt would shown when needed. 109 | 110 | Commit 111 | 112 | run git commit for seleted file(s). 113 | 114 | The Kind of |denite-gitstatus| is inherited from file, so all file 115 | actions are available. 116 | 117 | ------------------------------------------------------------------------------ 118 | GITCHANGED ACTIONS *denite-gitchanged-actions* 119 | 120 | open (default) 121 | open seleted line in current buffer. 122 | 123 | ------------------------------------------------------------------------------ 124 | GITBRANCH ACTIONS *denite-gitbranch-actions* 125 | 126 | checkout (default) 127 | Checkout seleted branch in current buffer. 128 | 129 | delete 130 | Delete seleted branch. 131 | 132 | merge 133 | Merge seleted branch. 134 | 135 | rebase 136 | 137 | Rebase seleted branch with current branch. 138 | 139 | ============================================================================== 140 | CHANGELOG *denite-git-changelog* 141 | 142 | 0.4 Jul 19, 2018 143 | 144 | - reload buffers after branch action 145 | 146 | 0.3 Jun 14, 2017 147 | 148 | - use diffPreview for diff action of git status 149 | - add aptch action for git status 150 | 151 | 0.2 Feb 25, 2017 152 | 153 | - add gitchanged source 154 | 155 | 0.1 Feb 24, 2017 156 | 157 | - works 158 | 159 | ============================================================================== 160 | FEEDBACK *denite-git-feedback* 161 | 162 | |denite-git| is open sourced at https://github.com/chemzqm/denite-git. 163 | feel free to open a issue when you get any problem. 164 | 165 | ============================================================================== 166 | vim:tw=78:ts=8:ft=help:norl:noet:fen: 167 | -------------------------------------------------------------------------------- /rplugin/python3/denite/source/gitbranch.py: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # FILE: gitbranch.py 3 | # AUTHOR: Takahiro Shirasaka 4 | # License: MIT license 5 | # ============================================================================ 6 | # pylint: disable=E0401,C0411 7 | import os 8 | import re 9 | import subprocess 10 | from .base import Base as BaseSource 11 | from ..kind.base import Base as BaseKind 12 | from denite import util 13 | 14 | EMPTY_LINE = re.compile(r"^\s*$") 15 | 16 | 17 | def _parse_line(line, root): 18 | current_symbol = line[0] 19 | return { 20 | 'word': line, 21 | 'action__path': line[2:], 22 | 'source__root': root, 23 | 'source__branch': line[10:] if line[2:10] == 'remotes/' else line[2:], 24 | 'source__current': current_symbol == '*', 25 | 'source__remote': line[2:10] == 'remotes/', 26 | } 27 | 28 | 29 | def run_command(commands, cwd, encoding='utf-8'): 30 | try: 31 | p = subprocess.run(commands, 32 | cwd=cwd, 33 | stdout=subprocess.PIPE, 34 | stderr=subprocess.STDOUT) 35 | except subprocess.CalledProcessError: 36 | return [] 37 | 38 | return p.stdout.decode(encoding).split('\n') 39 | 40 | 41 | class Source(BaseSource): 42 | 43 | def __init__(self, vim): 44 | super().__init__(vim) 45 | 46 | self.name = 'gitbranch' 47 | self.kind = Kind(vim) 48 | 49 | def on_init(self, context): 50 | gitdir = self.vim.call('denite#git#gitdir') 51 | context['__root'] = os.path.dirname(gitdir) 52 | 53 | def gather_candidates(self, context): 54 | root = context['__root'] 55 | if not root: 56 | return [] 57 | args = ['git', 'branch', '--no-color', '-a'] 58 | self.print_message(context, ' '.join(args)) 59 | lines = run_command(args, root) 60 | candidates = [] 61 | 62 | for line in lines: 63 | if EMPTY_LINE.fullmatch(line): 64 | continue 65 | candidates.append(_parse_line(line, root)) 66 | 67 | return candidates 68 | 69 | 70 | class Kind(BaseKind): 71 | def __init__(self, vim): 72 | super().__init__(vim) 73 | 74 | self.persist_actions += [] # pylint: disable=E1101 75 | self.redraw_actions += [] # pylint: disable=E1101 76 | self.name = 'gitbranch' 77 | self.default_action = 'checkout' 78 | 79 | def action_checkout(self, context): 80 | target = context['targets'][0] 81 | branch = target['source__branch'] 82 | args = ['git', 'checkout', branch] 83 | root = target['source__root'] 84 | run_command(args, root) 85 | self.vim.command('bufdo e') 86 | 87 | def action_delete(self, context): 88 | target = context['targets'][0] 89 | args = [] 90 | root = target['source__root'] 91 | branch = target['source__branch'] 92 | 93 | if target['source__remote']: 94 | branchname = branch.split('/')[-1] 95 | 96 | confirm = str(self.vim.call('denite#util#input', 97 | 'Delete remote branch ' + branchname + '? [y/n] : ', 98 | 'n', 99 | '')) == 'y' 100 | if confirm: 101 | args = ['git', 'push', 'origin', '--delete', branchname] 102 | else: 103 | force = str(self.vim.call('denite#util#input', 104 | 'Force delete? [y/n] : ', 105 | 'n', 106 | '')) == 'y' 107 | args = ['git', 'branch', '-D' if force else '-d', branch] 108 | 109 | if len(args) > 0: 110 | run_command(args, root) 111 | self.vim.command('bufdo e') 112 | 113 | def action_merge(self, context): 114 | target = context['targets'][0] 115 | root = target['source__root'] 116 | branch = target['source__branch'] 117 | args = ['git', 'merge', branch] 118 | 119 | if not target['source__current']: 120 | run_command(args, root) 121 | self.vim.command('bufdo e') 122 | 123 | def action_rebase(self, context): 124 | target = context['targets'][0] 125 | branch = target['source__branch'] 126 | args = ['git', 'rebase', branch] 127 | root = target['source__root'] 128 | 129 | if not target['source__current']: 130 | run_command(args, root) 131 | self.vim.command('bufdo e') 132 | 133 | -------------------------------------------------------------------------------- /rplugin/python3/denite/source/gitchanged.py: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # FILE: gitchaned.py 3 | # AUTHOR: Qiming Zhao 4 | # License: MIT license 5 | # ============================================================================ 6 | # pylint: disable=E0401,C0411 7 | from .line import Source as Base 8 | 9 | 10 | class Source(Base): 11 | 12 | def __init__(self, vim): 13 | super().__init__(vim) 14 | 15 | self.name = 'gitchanged' 16 | 17 | def on_init(self, context): 18 | super().on_init(context) 19 | # context['__buffer'] = self.vim.current.buffer 20 | buf = self.vim.current.buffer 21 | context['__bufnr'] = buf.number 22 | context['__bufname'] = buf.name 23 | context['__gutter'] = buf.vars.get('gitgutter') 24 | 25 | def gather_candidates(self, context): 26 | if not context['__gutter']: 27 | return [] 28 | 29 | hunks = context['__gutter']['hunks'] 30 | changed = [x[2] for x in hunks] 31 | 32 | fmt = '%' + str(len(str(self.vim.call('line', '$')))) + 'd: %s' 33 | 34 | lines = [] 35 | 36 | for [i, x] in enumerate(self.vim.call( 37 | 'getbufline', context['__bufnr'], 1, '$')): 38 | # vim line number start from 1 39 | vim_line_num = i + 1 40 | if vim_line_num in changed: 41 | lines.append({ 42 | 'word': x, 43 | 'abbr': (fmt % (vim_line_num, x)), 44 | 'action__path': context['__bufname'], 45 | 'action__line': vim_line_num 46 | }) 47 | 48 | return lines 49 | -------------------------------------------------------------------------------- /rplugin/python3/denite/source/gitfiles.py: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # FILE: gitfiles.py 3 | # AUTHOR: tylerc230@gmail.com 4 | # License: MIT license 5 | # ============================================================================ 6 | # pylint: disable=E0401,C0411 7 | import os 8 | import re 9 | import subprocess 10 | from .base import Base as BaseSource 11 | from ..kind.base import Base as BaseKind 12 | from denite import util 13 | from denite.util import debug 14 | 15 | 16 | EMPTY_LINE = re.compile(r"^\s*$") 17 | def run_command(commands, cwd, encoding='utf-8'): 18 | try: 19 | p = subprocess.run(commands, 20 | cwd=cwd, 21 | stdout=subprocess.PIPE, 22 | stderr=subprocess.STDOUT) 23 | except subprocess.CalledProcessError: 24 | return [] 25 | 26 | return p.stdout.decode(encoding).split('\n') 27 | 28 | 29 | class Source(BaseSource): 30 | 31 | def __init__(self, vim): 32 | super().__init__(vim) 33 | self.name = "gitfiles" 34 | self.kind = GitObject(vim) 35 | 36 | def on_init(self, context): 37 | args = dict(enumerate(context['args'])) 38 | branch = str(args.get(0, "master")) 39 | gitdir = self.vim.call('denite#git#gitdir') 40 | context['__root'] = os.path.dirname(gitdir) 41 | context['__branch'] = branch 42 | 43 | def gather_candidates(self, context): 44 | branch = context['__branch'] 45 | args = ['git', 'ls-tree', '-r', branch] 46 | root = context['__root'] 47 | lines = run_command(args, root) 48 | return [self._parse_line(line, root, branch) for line in lines if not EMPTY_LINE.fullmatch(line)] 49 | 50 | def _parse_line(self, line, root, branch): 51 | parts = line.split("\t", 1) 52 | filename = parts[1] 53 | obj_sha = parts[0].split(" ")[2] 54 | path = os.path.join(root, filename) 55 | return { 56 | 'branch': branch, 57 | 'hash': obj_sha, 58 | 'word': path, 59 | 'abbr': path 60 | } 61 | 62 | 63 | class GitObject(BaseKind): 64 | def __init__(self, vim): 65 | super().__init__(vim) 66 | self.name = 'git_object' 67 | self.default_action = 'view' 68 | 69 | def action_view(self, context): 70 | target = context['targets'][0] 71 | obj_sha = target["hash"] 72 | branch = target['branch'] 73 | self.vim.command("new | read ! git cat-file -p " + obj_sha ) 74 | del self.vim.current.buffer[0] #need to remove the first line since 'read' insert a new line at the top 75 | self.vim.command("setl buftype=nofile nomodifiable bufhidden=wipe nobuflisted") #user a scratch buffer 76 | filename = os.path.basename(target["abbr"]) 77 | self.vim.command("file (" + branch + ") " + filename) 78 | self.vim.command("filetype detect") 79 | 80 | -------------------------------------------------------------------------------- /rplugin/python3/denite/source/gitlog.py: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # FILE: gitlog.py 3 | # AUTHOR: Qiming Zhao 4 | # License: MIT license 5 | # ============================================================================ 6 | # pylint: disable=E0401,C0411 7 | import os 8 | import re 9 | from itertools import filterfalse 10 | from ..kind.openable import Kind as Openable 11 | from denite import util, process 12 | 13 | from .base import Base 14 | 15 | 16 | def _parse_line(line, gitdir, root, filepath, winid): 17 | line = line.replace("'", '', 1) 18 | line = line.rstrip("'") 19 | pattern = re.compile(r"(\*|\|)\s+([0-9A-Za-z]{6,13})\s-\s") 20 | match = re.search(pattern, line) 21 | if not match: 22 | return None 23 | return { 24 | 'word': line, 25 | 'source__commit': match.group(2), 26 | 'source__gitdir': gitdir, 27 | 'source__root': root, 28 | 'source__file': filepath, 29 | 'source__winid': winid 30 | } 31 | 32 | 33 | class Source(Base): 34 | 35 | def __init__(self, vim): 36 | super().__init__(vim) 37 | 38 | self.name = 'gitlog' 39 | self.matchers = ['matcher_regexp'] 40 | self.vars = { 41 | 'default_opts': ['--graph', '--no-color', 42 | "--pretty=format:'%h -%d %s (%cr) <%an>'", 43 | '--abbrev-commit', '--date=relative'] 44 | } 45 | self.kind = Kind(vim) 46 | 47 | def on_init(self, context): 48 | context['__proc'] = None 49 | context['__gitdir'] = self.vim.call('denite#git#gitdir') 50 | if not context['__gitdir']: 51 | return 52 | context['__root'] = os.path.dirname(context['__gitdir']) 53 | 54 | args = dict(enumerate(context['args'])) 55 | is_all = str(args.get(0, [])) == 'all' 56 | context['pattern'] = context['input'] if context['input'] else str(args.get(1, '')) 57 | context['__winid'] = self.vim.call('win_getid') 58 | buftype = self.vim.current.buffer.options['buftype'] 59 | fullpath = os.path.normpath(self.vim.call('expand', '%:p')) 60 | if fullpath and not buftype and not is_all: 61 | context['__file'] = os.path.relpath(fullpath, context['__root']) 62 | else: 63 | context['__file'] = '' 64 | 65 | def on_close(self, context): 66 | if context['__proc']: 67 | context['__proc'].kill() 68 | context['__proc'] = None 69 | 70 | def highlight(self): 71 | self.vim.command('highlight default link deniteSource__gitlogRef Title') 72 | self.vim.command('highlight default link deniteSource__gitlogTag Type') 73 | self.vim.command('highlight default link deniteSource__gitlogTime Keyword') 74 | self.vim.command('highlight default link deniteSource__gitlogUser Constant') 75 | 76 | def define_syntax(self): 77 | self.vim.command('syntax case ignore') 78 | 79 | self.vim.command(r'syntax match deniteSource__gitlogRef /\v((\*\||)\s)@<=[0-9A-Za-z]{7,13}(\s-\s)@=/ ' 80 | r'contained containedin=deniteSource__gitlogHeader ' 81 | r'nextgroup=deniteSource__gitlogTag,deniteSource__gitlogTime') 82 | self.vim.command(r'syntax match deniteSource__gitlogTag /(.\{-}tag:\s.\{-})/ contained ' 83 | r'containedin=' + self.syntax_name + ' ' 84 | r'nextgroup=deniteSource__gitlogTime') 85 | self.vim.command(r'syntax match deniteSource__gitlogTime /([^)]\{-}\sago)/ contained ' 86 | r'containedin=' + self.syntax_name + ' ' 87 | r'nextgroup=deniteSource__gitlogUser') 88 | self.vim.command(r'syntax match deniteSource__gitlogUser /\v\<[^<]+\>$/ contained ' 89 | r'containedin=' + self.syntax_name) 90 | 91 | def gather_candidates(self, context): 92 | if context['__proc']: 93 | return self.__async_gather_candidates(context, 0.03) 94 | if not context['__root']: 95 | return [] 96 | args = [] 97 | args += ['git', '--git-dir=' + context['__gitdir']] 98 | args += ['--no-pager', 'log'] 99 | args += self.vars['default_opts'] 100 | if len(context['__file']): 101 | git_file = os.path.relpath( 102 | os.path.join(context['__root'], context['__file']), 103 | context['__root'], 104 | ) 105 | args += ['--', git_file] 106 | 107 | self.print_message(context, ' '.join(args)) 108 | 109 | context['__proc'] = process.Process(args, context, context['__root']) 110 | return self.__async_gather_candidates(context, 0.5) 111 | 112 | def __async_gather_candidates(self, context, timeout): 113 | outs, errs = context['__proc'].communicate(timeout=timeout) 114 | context['is_async'] = not context['__proc'].eof() 115 | if context['__proc'].eof(): 116 | context['__proc'] = None 117 | 118 | candidates = [] 119 | 120 | for line in errs: 121 | self.print_message(context, line) 122 | 123 | filepath = context['__file'] 124 | winid = context['__winid'] 125 | for line in outs: 126 | result = _parse_line( 127 | line, context['__gitdir'], context['__root'], filepath, winid 128 | ) 129 | if not result: 130 | continue 131 | candidates.append(result) 132 | return candidates 133 | 134 | 135 | class Kind(Openable): 136 | def __init__(self, vim): 137 | super().__init__(vim) 138 | 139 | self.persist_actions = ['reset', 'preview'] 140 | self.redraw_actions = ['reset'] 141 | self.name = 'gitlog' 142 | 143 | def action_delete(self, context): 144 | target = context['targets'][0] 145 | commit = target['source__commit'] 146 | bufname = '[Git %s]' % (commit) 147 | if self.vim.call('bufexists', bufname): 148 | bufnr = self.vim.call('bufnr', bufname) 149 | self.vim.command('bdelete ' + str(bufnr)) 150 | return 151 | 152 | winid = target['source__winid'] 153 | self.vim.call('win_gotoid', winid) 154 | option = { 155 | 'gitdir': target['source__gitdir'], 156 | 'edit': 'vsplit' 157 | } 158 | self.vim.call('denite#git#diffCurrent', commit, option) 159 | 160 | def action_reset(self, context): 161 | target = context['targets'][0] 162 | commit = target['source__commit'] 163 | gitdir = target['source__gitdir'] 164 | 165 | c = str(self.vim.call('denite#util#input', 166 | 'Reset mode mixed|soft|hard [m/s/h]: ', 167 | '', 168 | '')) 169 | opt = '' 170 | if c == 'm': 171 | opt = '--mixed' 172 | elif c == 's': 173 | opt = '--soft' 174 | elif c == 'h': 175 | opt = '--hard' 176 | else: 177 | return 178 | self.vim.call('denite#git#reset', opt + ' ' + commit, gitdir) 179 | 180 | def action_open(self, context, split=None): 181 | target = context['targets'][0] 182 | commit = target['source__commit'] 183 | gitdir = target['source__gitdir'] 184 | winid = target['source__winid'] 185 | is_all = True if not target['source__file'] else False 186 | option = { 187 | 'all': 1 if is_all else 0, 188 | 'gitdir': gitdir, 189 | 'fold': 0 190 | } 191 | if split is not None: 192 | option['edit'] = split 193 | if not is_all: 194 | option['file'] = os.path.relpath( 195 | os.path.join(target['source__root'], target['source__file']), 196 | os.path.dirname(gitdir), 197 | ) 198 | self.vim.call('win_gotoid', winid) 199 | self.vim.call('denite#git#show', commit, option) 200 | 201 | def action_split(self, context): 202 | return self.action_open(context, 'split') 203 | 204 | def action_vsplit(self, context): 205 | return self.action_open(context, 'vsplit') 206 | 207 | def __get_preview_window(self): 208 | return next(filterfalse(lambda x: 209 | not x.options['previewwindow'], 210 | self.vim.windows), None) 211 | 212 | def action_preview(self, context): 213 | target = context['targets'][0] 214 | commit = target['source__commit'] 215 | gitdir = target['source__gitdir'] 216 | suffix = commit + ']]' 217 | preview_window = self.__get_preview_window() 218 | if preview_window: 219 | same = preview_window.buffer.name.endswith(suffix) 220 | self.vim.command('pclose!') 221 | if same: 222 | return 223 | 224 | prev_id = self.vim.call('win_getid') 225 | winid = target['source__winid'] 226 | is_all = True if not target['source__file'] else False 227 | option = { 228 | 'all': 1 if is_all else 0, 229 | 'gitdir': gitdir 230 | } 231 | option['edit'] = 'vsplit' if context['vertical_preview'] else 'split' 232 | option['floating_preview'] = int(context['floating_preview']) 233 | if option['floating_preview'] == 1: 234 | pos = self.vim.call('win_screenpos', prev_id) 235 | winwidth = self.vim.call('winwidth', 0) 236 | option['preview_win_row'] = pos[0] - 1 237 | option['preview_win_col'] = (pos[1] - 1) + winwidth - context['preview_width'] 238 | option['preview_width'] = context['preview_width'] 239 | option['preview_height'] = context['preview_height'] 240 | if not is_all: 241 | option['file'] = os.path.relpath( 242 | os.path.join(target['source__root'], target['source__file']), 243 | os.path.dirname(gitdir), 244 | ) 245 | self.vim.call('denite#git#show', commit, option) 246 | self.vim.command('setl previewwindow') 247 | if not is_all: 248 | self.vim.command('set nofen') 249 | self.vim.call('win_gotoid', prev_id) 250 | 251 | -------------------------------------------------------------------------------- /rplugin/python3/denite/source/gitstatus.py: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # FILE: gitstatus.py 3 | # AUTHOR: Qiming Zhao 4 | # License: MIT license 5 | # ============================================================================ 6 | # pylint: disable=E0401,C0411 7 | import os 8 | import re 9 | import subprocess 10 | import shlex 11 | from itertools import filterfalse 12 | from .base import Base 13 | from denite import util 14 | from ..kind.file import Kind as File 15 | 16 | EMPTY_LINE = re.compile(r"^\s*$") 17 | STATUS_MAP = { 18 | ' ': ' ', 19 | 'M': '~', 20 | 'T': '~', 21 | 'A': '+', 22 | 'D': '-', 23 | 'R': '→', 24 | 'C': 'C', 25 | 'U': 'U', 26 | '?': '?'} 27 | 28 | 29 | def _parse_line(line, gitdir, root, winnr): 30 | path = os.path.join(root, line[3:]) 31 | index_symbol = STATUS_MAP[line[0]] 32 | tree_symbol = STATUS_MAP[line[1]] 33 | word = "{0}{1} {2}".format(index_symbol, tree_symbol, line[3:]) 34 | return { 35 | 'word': word, 36 | 'action__path': path, 37 | 'source__gitdir': gitdir, 38 | 'source__root': root, 39 | 'Source__winnr': winnr, 40 | 'source__staged': index_symbol not in [' ', '?'], 41 | 'source__tree': tree_symbol not in [' ', '?'] 42 | } 43 | 44 | 45 | def run_command(commands, cwd, encoding='utf-8'): 46 | try: 47 | p = subprocess.run(commands, 48 | cwd=cwd, 49 | stdout=subprocess.PIPE, 50 | stderr=subprocess.STDOUT) 51 | except subprocess.CalledProcessError: 52 | return [] 53 | 54 | return p.stdout.decode(encoding).split('\n') 55 | 56 | 57 | class Source(Base): 58 | 59 | def __init__(self, vim): 60 | super().__init__(vim) 61 | 62 | self.name = 'gitstatus' 63 | self.kind = Kind(vim) 64 | self.is_public_context = True 65 | 66 | def on_init(self, context): 67 | context['__gitdir'] = self.vim.call('denite#git#gitdir') 68 | if not context['__gitdir']: 69 | return 70 | context['__root'] = os.path.dirname(context['__gitdir']) 71 | context['__winnr'] = self.vim.call('winnr') 72 | 73 | def highlight(self): 74 | self.vim.command('highlight deniteGitStatusAdd guifg=#009900 ctermfg=2') 75 | self.vim.command('highlight deniteGitStatusChange guifg=#bbbb00 ctermfg=3') 76 | self.vim.command('highlight deniteGitStatusDelete guifg=#ff2222 ctermfg=1') 77 | self.vim.command('highlight deniteGitStatusUnknown guifg=#5f5f5f ctermfg=59') 78 | 79 | def define_syntax(self): 80 | self.vim.command(r'syntax match deniteGitStatusHeader /^.*$/ ' + 81 | r'containedin=' + self.syntax_name) 82 | self.vim.command(r'syntax match deniteGitStatusSymbol /^\s*\zs\S\+/ ' + 83 | r'contained containedin=deniteGitStatusHeader') 84 | self.vim.command(r'syntax match deniteGitStatusAdd /+/ ' + 85 | r'contained containedin=deniteGitStatusSymbol') 86 | self.vim.command(r'syntax match deniteGitStatusDelete /-/ ' + 87 | r'contained containedin=deniteGitStatusSymbol') 88 | self.vim.command(r'syntax match deniteGitStatusChange /\~/ ' + 89 | r'contained containedin=deniteGitStatusSymbol') 90 | self.vim.command(r'syntax match deniteGitStatusUnknown /?/ ' + 91 | r'contained containedin=deniteGitStatusSymbol') 92 | 93 | def gather_candidates(self, context): 94 | gitdir = context['__gitdir'] 95 | if not gitdir: 96 | return [] 97 | root = context['__root'] 98 | if not root: 99 | return [] 100 | winnr = context['__winnr'] 101 | args = ['git', 'status', '--porcelain', '-uall'] 102 | self.print_message(context, ' '.join(args)) 103 | lines = run_command(args, root) 104 | candidates = [] 105 | 106 | for line in lines: 107 | if EMPTY_LINE.fullmatch(line): 108 | continue 109 | candidates.append(_parse_line(line, gitdir, root, winnr)) 110 | 111 | return candidates 112 | 113 | 114 | class Kind(File): 115 | def __init__(self, vim): 116 | super().__init__(vim) 117 | 118 | self.persist_actions += ['reset', 'add', 'delete'] # pylint: disable=E1101 119 | self.redraw_actions += ['reset', 'add', 'commit'] # pylint: disable=E1101 120 | self.name = 'gitstatus' 121 | self._previewed_target = None 122 | 123 | val = self.vim.call('exists', ':Rm') 124 | if val == 2: 125 | self.remove = 'rm' 126 | elif self.vim.call('executable', 'rmtrash'): 127 | self.remove = 'rmtrash' 128 | else: 129 | self.remove = 'delete' 130 | 131 | def action_patch(self, context): 132 | args = [] 133 | root = context['targets'][0]['source__root'] 134 | for target in context['targets']: 135 | filepath = target['action__path'] 136 | args.append(os.path.relpath(filepath, root)) 137 | self.vim.command('terminal git add ' + ' '.join(args) + ' --patch') 138 | 139 | def action_add(self, context): 140 | args = ['git', 'add'] 141 | root = context['targets'][0]['source__root'] 142 | for target in context['targets']: 143 | filepath = target['action__path'] 144 | args.append(os.path.relpath(filepath, root)) 145 | run_command(args, root) 146 | 147 | def __get_preview_window(self): 148 | return next(filterfalse(lambda x: 149 | not x.options['previewwindow'], 150 | self.vim.windows), None) 151 | 152 | # diff action 153 | def action_delete(self, context): 154 | target = context['targets'][0] 155 | root = target['source__root'] 156 | winnr = target['Source__winnr'] 157 | gitdir = target['source__gitdir'] 158 | 159 | preview_window = self.__get_preview_window() 160 | 161 | if preview_window: 162 | self.vim.command('pclose!') 163 | if self._previewed_target == target: 164 | return 165 | 166 | relpath = os.path.relpath(target['action__path'], root) 167 | prefix = '' 168 | if target['source__staged']: 169 | if target['source__tree']: 170 | confirmed = str(self.vim.call('denite#util#input', 171 | 'Diff cached?[y/n]', 172 | 'y', 173 | '')) == 'y' 174 | if confirmed == 'y': 175 | prefix = '--cached ' 176 | else: 177 | prefix = '--cached ' 178 | prev_id = self.vim.call('win_getid') 179 | self.vim.command(str(winnr) + 'wincmd w') 180 | self.vim.call('denite#git#diffPreview', prefix, relpath, gitdir) 181 | 182 | self.vim.call('win_gotoid', prev_id) 183 | self._previewed_target = target 184 | 185 | def action_reset(self, context): 186 | cwd = os.path.normpath(self.vim.eval('expand("%:p:h")')) 187 | for target in context['targets']: 188 | filepath = target['action__path'] 189 | root = target['source__root'] 190 | path = os.path.relpath(filepath, root) 191 | if target['source__tree'] and target['source__staged']: 192 | res = str(self.vim.call('denite#util#input', 193 | 'Select action reset or checkout [r/c]', 194 | '', 195 | '')) 196 | if res == 'c': 197 | args = 'git checkout -- ' + path 198 | run_command(shlex.split(args), root) 199 | elif res == 'r': 200 | args = 'git reset HEAD -- ' + path 201 | run_command(shlex.split(args), root) 202 | elif target['source__tree']: 203 | args = 'git checkout -- ' + path 204 | run_command(shlex.split(args), root) 205 | elif target['source__staged']: 206 | args = 'git reset HEAD -- ' + path 207 | run_command(shlex.split(args), root) 208 | else: 209 | if self.remove == 'rm': 210 | self.vim.command('Rm ' + os.path.relpath(filepath, cwd)) 211 | elif self.remove == 'rmtrash': 212 | run_command(['rmtrash', filepath], root) 213 | else: 214 | self.vim.call('delete', filepath) 215 | self.vim.command('checktime') 216 | 217 | def action_commit(self, context): 218 | root = context['targets'][0]['source__root'] 219 | files = [] 220 | for target in context['targets']: 221 | filepath = target['action__path'] 222 | files.append(os.path.relpath(filepath, root)) 223 | self.vim.call('denite#git#commit', '-v', files) 224 | 225 | --------------------------------------------------------------------------------