├── .gitignore ├── CHANGELOG.md ├── README.md ├── autoload ├── merginal.vim └── merginal │ ├── buffers │ ├── base.vim │ ├── branchList.vim │ ├── cherryPickConflicts.vim │ ├── conflictsBase.vim │ ├── diffFiles.vim │ ├── fileListBase.vim │ ├── historyLog.vim │ ├── immutableBranchList.vim │ ├── mergeConflicts.vim │ ├── rebaseAmend.vim │ └── rebaseConflicts.vim │ ├── modulelib.vim │ └── util.vim ├── doc └── merginal.txt └── plugin └── merginal.vim /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | ### Added 9 | - `g:merginal_showCommands` to control whether Git commands will be `echo`ed. 10 | - `g:merginal_resizeWindowToBranchLen` and `g:merginal_resizePadding` to resize the branch list to fit the branches lengths. 11 | - `ms` keymap for merging with `--squash`. 12 | - `g:merginal_branchListFlags` to control the branch list displayed in the Merginal buffer. 13 | 14 | ### Fixed 15 | - Use `Git` instead of `Gstatus` (which was deprecated and removed from Fugitive) 16 | 17 | ## [2.2.4] - 2023-08-27 18 | ### Fixed 19 | - `ds` now calls `:Ghdiffsplit`, since that's the new name of `:Gsdiffsplit`. 20 | 21 | ## [2.2.3] - 2022-10-29 22 | ### Fixed 23 | - Remove a stray echo when generating help for the various Merginal buffers. 24 | 25 | ## [2.2.2] - 2022-10-05 26 | ### Fixed 27 | - Replace `fugitive#repo()` usage with Fugitives standalone functions. 28 | 29 | ## [2.2.1] - 2021-11-26 30 | ### Fixed 31 | - Use `FugitiveShellCommand` instead of `repo.git_command` (which is deprecated by Fugitive) 32 | - Use `Git merge` instead of `Gmerge` (which is deprecated by Fugitive) 33 | 34 | ## [2.2.0] - 2020-12-06 35 | ### Added 36 | - Support separate Meginal buffers on multiple tabs. 37 | - Added merginal_remoteVisible variable to set remote branches to be viewed by default. 38 | - Added a hotkey 'tr' to toggle remote branches in the view. 39 | 40 | ### Changed 41 | - Calling `Merginal` or `MerginalToggle` when the merginal buffer is open but not in the main mode (branch list by default, or changes if in special Git status) will: 42 | - Jump to it, if the user is in another window. 43 | - Change it to the main mode, if the user is in it. 44 | 45 | ## [2.1.2] - 2020-01-29 46 | ### Fixed 47 | - Made merginal commands global, not buffer-local, because Fugutive no longer fires the signals Merginal depended on. 48 | - Changed `fugitive#detect` to `FugitiveDetect`. 49 | - Made `pS` push with `--force-with-lease` instead of just `--force`. Yes, this is a fix. No this is not a breaking change. If this breaks your workflow then your workflow was broken to begin with. 50 | - Made all the mappings ``. 51 | 52 | ## [2.1.1] - 2019-09-06 53 | ### Added 54 | - `g:merginal_logCommitCount` to allow limiting amount of commits displayed in history log. 55 | 56 | ### Fixed 57 | - Fix `fileFullPath` bug. 58 | - Correct argument order for merging. 59 | 60 | ## [2.1.0] - 2017-10-29 61 | ### Added 62 | - NERDTree style keymaps for opening files. 63 | - `g:merginal_windowWidth`/`g:merginal_windowSize` to control window size. 64 | - `g:merginal_splitType` to control splitting (vertically/horizontally). 65 | 66 | ### Changed 67 | - Use committer date instead of author date in `historyLog`. 68 | 69 | ### Fixed 70 | - Use exception number instead of string. 71 | - Fixed handling of uninitialized modules. 72 | - Added missing merge commands to `mergeConflicts` buffer. This is considered a fix because we had it in v1. 73 | - Fix Pushing when there are multiple remotes. 74 | - Fix keymaps leaking between buffers. 75 | - Fix command running for Vim8 with `:terminal` support. 76 | 77 | ## [2.0.2] - 2016-07-09 78 | ### Fixed 79 | - Stop refresh() from changing other buffers. 80 | 81 | ## [2.0.1] - 2016-05-01 82 | ### Fixed 83 | - Fixed filtering in history log. 84 | - Added missing open-conflicted-file keymap. This is considered a fix because we had it in v1. 85 | 86 | ## [2.0.0] - 2016-04-09 87 | ### Added 88 | - `?` to display keymap help in the various Merginal buffers. 89 | - `&` to filter the entries in the various Merginal buffers. 90 | 91 | ### Changed 92 | - [**BREAKING**?] Use filetypes instead of autocommands to set up the Merginal buffers. 93 | - [**BREAKING**?] Moved to a basic module framework to organize buffer types. 94 | 95 | ### Fixed 96 | - Fixed bug when calling `:MerginalToggle` from the merginal buffer. 97 | - Fixed showing the current branch on checkout. 98 | - Use `:terminal` for remote commands in Neovim. This is considered a fix, because without it you wouldn't be able to enter the git password in GUI Neovim. 99 | 100 | ## [1.6.0] - 2015-09-23 101 | ### Added 102 | - `mn` keymap for merging with `--no-ff`. 103 | - Conflict-resolving mode for cherry-picking. 104 | 105 | ### Fixed 106 | - Fixed a bug where failure to do an operation would mess up the Vim windows. 107 | 108 | ## [1.5.0] - 2015-02-14 109 | ### Added 110 | - `q` keymap to close the merginal window. 111 | - `pr` keymap for pull-rebase. 112 | - `gl` keymap to open the history log for the branch under the cursor. 113 | - Checking out commits from the log history buffer. 114 | - Diffing against commits in the history log buffer. 115 | - Opening the history log buffer from the rebase amend buffer. 116 | - `` and `` to move between commits in history log. 117 | 118 | ### Changed 119 | - Use echom instead of echo to have them in `:messages` 120 | 121 | ### Fixed 122 | - Clear augroup (in case of reloads) 123 | 124 | ## [1.4.0] - 2014-09-19 125 | ### Added 126 | - `pS` keymap for force pushing branches. 127 | - A buffer type for `rebase-amend`. 128 | - `rn` keymap for renaming branches. 129 | 130 | ## [1.3.0] - 2014-08-11 131 | ### Added 132 | - Rebase functionality - (similar to merge functionality) 133 | - `MerginalToggle` and `MerginalClose` commands. 134 | 135 | ### Changed 136 | - Allow pushing/pulling/rebasing directly on remotes 137 | 138 | ### Fixed 139 | - Fixed a bug where merges without conflicts open empty list. 140 | 141 | ## [1.2.0] - 2014-07-08 142 | ### Added 143 | - `mf` keymap to run Fugitive's `:Gmerge`. 144 | - `merginal#branchDetails`. 145 | - `ct` and `cT` for tracking remote branches. 146 | - `ps`, `pl` and `pf` to push, pull and fetch branches 147 | - Branch diff functionality 148 | 149 | ### Changed 150 | - Made the merginal buffer window have fixed size. 151 | - Made `dd` be able to delete remote branches 152 | 153 | ### Fixed 154 | - Set nonumber in merginal buffer. 155 | - Fixed a bug where Merginal would change user buffers. 156 | 157 | ## [1.1.0] - 2014-06-06 158 | ### Changed 159 | - Use autocmd for adding the keymaps. 160 | - Use Fugitive's style of keymaps(`cc`=`C` etc.) 161 | 162 | ## [1.0.0] - 2014-06-04 163 | ### Added 164 | - Branch list. 165 | - Basic branch commands. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | INTRODUCTION 2 | ============ 3 | 4 | Merginal aims to provide a nice interface for dealing with Git branches. It 5 | offers interactive TUI for: 6 | 7 | * Viewing the list of branches 8 | * Checking out branches from that list 9 | * Creating new branches 10 | * Deleting branches 11 | * Merging branches 12 | * Rebasing branches 13 | * Solving merge conflicts 14 | * Interacting with remotes(pulling, pushing, fetching, tracking) 15 | * Diffing against other branches 16 | * Renaming branches 17 | * Viewing git history for branches 18 | 19 | 20 | REQUIREMENTS 21 | ============ 22 | 23 | Merginal is based on Fugitive, so it requires Fugitive. If you don't have it 24 | already you can get it from https://github.com/tpope/vim-fugitive 25 | 26 | It should go without saying that you need Git. 27 | 28 | Under Windows, vimproc is an optional requirement. Merginal will work without 29 | it, but it'll pop an ugly console window every time it needs to run a Git 30 | command. You can get vimproc from https://github.com/Shougo/vimproc.vim 31 | 32 | 33 | USAGE 34 | ===== 35 | 36 | To use Merginal you need to know but one command: `:Merginal`. It'll open the 37 | branch list buffer, unless the repository is in merge mode then 38 | it'll open the merge conflicts buffer. 39 | 40 | Like Fugitive's commands, `:Merginal` is native to the buffer, and will only 41 | work in buffers that are parts of Git repositories. 42 | 43 | You can also toggle the buffer with `:MerginalToggle` or close it with 44 | `:MerginalClose`. 45 | 46 | To see a list of keymaps available in each Merginal buffer, press `?`. 47 | -------------------------------------------------------------------------------- /autoload/merginal.vim: -------------------------------------------------------------------------------- 1 | "Use vimproc if available under windows to prevent opening a console window 2 | function! merginal#system(command,...) 3 | if has('win32') && exists(':VimProcBang') "We don't need vimproc when we use linux 4 | if empty(a:000) 5 | return vimproc#system(a:command) 6 | else 7 | return vimproc#system(a:command,a:000[0]) 8 | endif 9 | else 10 | if empty(a:000) 11 | return system(a:command) 12 | else 13 | return system(a:command,a:000[0]) 14 | endif 15 | endif 16 | endfunction 17 | 18 | function! merginal#bang(command) 19 | if exists('*termopen') 20 | redraw "Release 'Press ENTER or type command to continue' 21 | let l:oldWinView = winsaveview() 22 | botright new 23 | call winrestview(l:oldWinView) 24 | resize 5 25 | call termopen(a:command) 26 | autocmd BufWinLeave execute winnr('#').'wincmd w' 27 | normal! A 28 | else 29 | execute '!'.a:command 30 | endif 31 | endfunction 32 | 33 | "Opens a file that belongs to a repo in a window that already belongs to that 34 | "repo. Creates a new window if can't find suitable window. 35 | function! merginal#openFileDecidedWindow(fugitiveContext, fileName) 36 | "We have to check with bufexists, because bufnr also match prefixes of the 37 | "file name 38 | let l:fileBuffer=-1 39 | if bufexists(a:fileName) 40 | let l:fileBuffer=bufnr(a:fileName) 41 | endif 42 | 43 | "We have to check with bufloaded, because bufwinnr also matches closed 44 | "windows... 45 | let l:windowToOpenIn=-1 46 | if bufloaded(l:fileBuffer) 47 | let l:windowToOpenIn=bufwinnr(l:fileBuffer) 48 | endif 49 | 50 | "If we found an open window with the correct file, we jump to it 51 | if -1 '.l:keymap.' :'.l:meta.execute.'' 147 | endfor 148 | 149 | if has_key(l:meta, 'command') 150 | execute 'command! -buffer -nargs=0 '.l:meta.command.' '.l:meta.execute 151 | endif 152 | endfor 153 | 154 | call self.refresh() 155 | if has_key(self, 'jumpToCurrentItem') 156 | call self.jumpToCurrentItem() 157 | endif 158 | endif 159 | 160 | let b:merginal = self 161 | 162 | "Check and return if a new buffer was created 163 | return -1 == l:tuiBufferWindow 164 | endfunction 165 | 166 | function! s:f.gotoBuffer(bufferModuleName, ...) dict abort 167 | let l:newBufferObject = merginal#modulelib#createObject(a:bufferModuleName) 168 | if has_key(l:newBufferObject, 'init') 169 | call call(l:newBufferObject.init, a:000, l:newBufferObject) 170 | elseif 0 < a:0 171 | throw 'gotoBuffer called with arguments but '.a:bufferModuleName.' has no "init" method' 172 | endif 173 | call l:newBufferObject.openTuiBuffer(winnr()) 174 | return l:newBufferObject 175 | endfunction 176 | 177 | function! s:f._getSpecialMode() dict abort 178 | return merginal#getSpecialMode(self.fugitiveContext.git_dir) 179 | endfunction 180 | 181 | "Returns the buffer moved to 182 | function! s:f.gotoSpecialModeBuffer() dict abort 183 | let l:mode = self._getSpecialMode() 184 | if empty(l:mode) || l:mode == self.name 185 | return 0 186 | endif 187 | let l:newBufferObject = self.gotoBuffer(l:mode) 188 | return l:newBufferObject 189 | endfunction 190 | 191 | function! s:f.isLineInBody(lineNumber) dict abort 192 | if type(a:lineNumber) == type(0) 193 | let l:line = a:lineNumber 194 | else 195 | let l:line = line(a:lineNumber) 196 | endif 197 | return len(self.header) < l:line 198 | endfunction 199 | 200 | function! s:f.verifyLineInBody(lineNumber) dict abort 201 | if !self.isLineInBody(a:lineNumber) 202 | throw 'In the header section of the merginal buffer' 203 | endif 204 | endfunction 205 | 206 | function! s:f.jumpToIndexInBody(index) dict abort 207 | execute a:index + len(self.header) + 1 208 | endfunction 209 | 210 | function! s:f.isStillInSpecialMode() dict abort 211 | let l:mode = self._getSpecialMode() 212 | return l:mode == self.name 213 | endfunction 214 | 215 | 216 | function! s:f.getFilteredBody() dict abort 217 | return filter(copy(self.body), '0 <= match(v:val, self.filter)') 218 | endfunction 219 | 220 | function! s:f.refresh() dict abort 221 | let self.header = [] 222 | if self.helpVisible 223 | call extend(self.header, self.generateHelp()) 224 | else 225 | call add(self.header, 'Press ? for help') 226 | endif 227 | call add(self.header, '') 228 | call extend(self.header, self.generateHeader()) 229 | let self.body = self.generateBody() 230 | 231 | let l:tuiBufferWindow = self.existingWindowNumber() 232 | if -1 < l:tuiBufferWindow 233 | let l:remember = merginal#util#rememberCursorWindow() 234 | try 235 | execute l:tuiBufferWindow.'wincmd w' 236 | let l:currentLine = line('.') - len(self.header) 237 | let l:currentColumn = col('.') 238 | 239 | setlocal modifiable 240 | "Clear the buffer: 241 | silent normal! gg"_dG 242 | "Write the buffer 243 | call setline(1, self.header + self.getFilteredBody()) 244 | let l:currentLine = l:currentLine + len(self.header) 245 | setlocal nomodifiable 246 | 247 | " Resize window if desired 248 | let l:resize_to_branch_strlen = get(g:, 'merginal_resizeWindowToBranchLen', 0) 249 | if l:resize_to_branch_strlen 250 | let l:cur_win = getwininfo(win_getid())[0] 251 | let l:max_strlen = 0 252 | for line in self.body 253 | if strlen(line) ># l:max_strlen 254 | let l:max_strlen = strlen(line) + get(g:, 'merginal_resizePadding', 5) 255 | endif 256 | endfor 257 | call win_move_separator(winnr(), (l:max_strlen - l:cur_win.width)) 258 | endif 259 | execute l:currentLine 260 | execute 'normal! '.l:currentColumn.'|' 261 | finally 262 | call l:remember.restore() 263 | endtry 264 | endif 265 | endfunction 266 | call s:f.addCommand('refresh', [], 'MerginalRefresh', 'R', 'Refresh the buffer') 267 | 268 | function! s:f.quit() 269 | bdelete 270 | endfunction 271 | call s:f.addCommand('quit', [], 0, 'q', 'Close the buffer') 272 | 273 | function! s:f.toggleHelp() dict abort 274 | let self.helpVisible = !self.helpVisible 275 | call self.refresh() 276 | endfunction 277 | call s:f.addCommand('toggleHelp', [], 0, '?', 'Toggle this help message') 278 | 279 | function! s:f.toggleRemote() dict abort 280 | let self.remoteVisible = !self.remoteVisible 281 | call self.refresh() 282 | endfunction 283 | call s:f.addCommand('toggleRemote', [], 0, 'tr', 'Toggle remote branches') 284 | 285 | function! s:f.promptForFilter() dict abort 286 | let l:newFilter = input('&/') 287 | let self.filter = l:newFilter 288 | call self.refresh() 289 | endfunction 290 | call s:f.addCommand('promptForFilter', [], 0, '&', 'Set filter') 291 | 292 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/branchList.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'branchList', 'immutableBranchList') 2 | 3 | 4 | 5 | function! s:f.checkoutBranch() dict abort 6 | let l:branch = self.branchDetails('.') 7 | call self.gitEcho('checkout', l:branch.handle, '--') 8 | call self.refresh() 9 | call self.jumpToCurrentItem() 10 | call merginal#reloadBuffers() 11 | endfunction 12 | call s:f.addCommand('checkoutBranch', [], 'MerginalCheckout', ['cc', 'C'], 'Checkout the branch under the cursor') 13 | 14 | 15 | function! s:f.trackBranch(promptForName) dict abort 16 | let l:branch = self.branchDetails('.') 17 | if !l:branch.isRemote 18 | throw 'Can not track - branch is not remote' 19 | endif 20 | let l:newBranchName = l:branch.name 21 | if a:promptForName 22 | let l:newBranchName = input('Track `'.l:branch.handle.'` as: ', l:newBranchName) 23 | if empty(l:newBranchName) 24 | echo ' ' 25 | echom 'Branch tracking canceled by user.' 26 | return 27 | endif 28 | endif 29 | call self.gitEcho('checkout', '-b', l:newBranchName, '--track', l:branch.handle, '--') 30 | if !v:shell_error 31 | call merginal#reloadBuffers() 32 | endif 33 | call self.refresh() 34 | call self.jumpToCurrentItem() 35 | endfunction 36 | call s:f.addCommand('trackBranch', [0], 'MerginalTrack', 'ct', 'Track the remote branch under the cursor') 37 | call s:f.addCommand('trackBranch', [1], 'MerginalTrackPrompt', 'cT', 'Track the remote branch under the cursor, prompting for a name') 38 | 39 | function! s:f.promptToCreateNewBranch() dict abort 40 | let l:newBranchName = input('Branch `'.FugitiveHead(b:merginal.fugitiveContext).'` to: ') 41 | if empty(l:newBranchName) 42 | echo ' ' 43 | echom 'Branch creation canceled by user.' 44 | return 45 | endif 46 | call self.gitEcho('checkout', '-b', l:newBranchName, '--') 47 | call merginal#reloadBuffers() 48 | 49 | call self.refresh() 50 | call self.jumpToCurrentItem() 51 | endfunction 52 | call s:f.addCommand('promptToCreateNewBranch', [], 'MerginalNewBranch', ['aa', 'A'], 'Create a new branch') 53 | 54 | function! s:f.deleteBranchUnderCursor() dict abort 55 | let l:branch = self.branchDetails('.') 56 | let l:answer = 0 57 | if l:branch.isLocal 58 | let l:answer = 'yes' == input('Delete branch `'.l:branch.handle.'`? (type "yes" to confirm) ') 59 | elseif l:branch.isRemote 60 | "Deleting remote branches needs a special warning 61 | let l:answer = 'yes-remote' == input('Delete remote(!) branch `'.l:branch.handle.'`? (type "yes-remote" to confirm) ') 62 | endif 63 | if l:answer 64 | if l:branch.isLocal 65 | call self.gitEcho('branch', '-D', l:branch.handle, '--') 66 | else 67 | call self.gitBang('push', l:branch.remote, '--delete', l:branch.name, '--') 68 | endif 69 | call self.refresh() 70 | else 71 | echo ' ' 72 | echom 'Branch deletion canceled by user.' 73 | endif 74 | endfunction 75 | call s:f.addCommand('deleteBranchUnderCursor', [], 'MerginalDelete', ['dd', 'D'], 'Delete the branch under the cursor') 76 | 77 | function! s:f.mergeBranchUnderCursor(...) dict abort 78 | let l:branch = self.branchDetails('.') 79 | let l:gitArgs = ['merge', '--no-commit'] 80 | call extend(l:gitArgs, a:000) 81 | call extend(l:gitArgs, [l:branch.handle, '--']) 82 | call call(self.gitEcho, l:gitArgs, self) 83 | let l:confilctsBuffer = self.gotoSpecialModeBuffer() 84 | if empty(l:confilctsBuffer) 85 | call self.refresh() 86 | else 87 | if empty(l:confilctsBuffer.body) 88 | "If we are in merge mode without actual conflicts, this means 89 | "there are not conflicts and the user can be prompted to enter a 90 | "merge message. 91 | Git 92 | call merginal#closeMerginalBuffer() 93 | endif 94 | endif 95 | call merginal#reloadBuffers() 96 | endfunction 97 | call s:f.addCommand('mergeBranchUnderCursor', [], 'MerginalMerge', ['mm', 'M'], 'Merge the branch under the cursor') 98 | call s:f.addCommand('mergeBranchUnderCursor', ['--no-ff'], 'MerginalMergeNoFF', ['mn'], 'Merge the branch under the cursor using --no-ff') 99 | call s:f.addCommand('mergeBranchUnderCursor', ['--squash'], 'MerginalMergeSquash', ['ms'], 'Merge the branch under the cursor using --squash') 100 | 101 | function! s:f.mergeBranchUnderCursorUsingFugitive() dict abort 102 | let l:branch = self.branchDetails('.') 103 | execute ':Git merge '.l:branch.handle 104 | endfunction 105 | call s:f.addCommand('mergeBranchUnderCursorUsingFugitive', [], 'MerginalMergeUsingFugitive', ['mf'], 'Merge the branch under the cursor using fugitive') 106 | 107 | function! s:f.rebaseBranchUnderCursor() dict abort 108 | let l:branch = self.branchDetails('.') 109 | call self.gitEcho('rebase', l:branch.handle, '--') 110 | call merginal#reloadBuffers() 111 | call self.gotoSpecialModeBuffer() 112 | endfunction 113 | call s:f.addCommand('rebaseBranchUnderCursor', [], 'MerginalRebase', 'rb', 'Rebase the branch under the cursor') 114 | 115 | function! s:f.remoteActionForBranchUnderCursor(action, ...) dict abort 116 | let l:branch = self.branchDetails('.') 117 | if l:branch.isLocal 118 | let l:remotes = self.gitLines('remote') 119 | if empty(l:remotes) 120 | throw 'Can not '.a:action.' - no remotes defined' 121 | endif 122 | 123 | let l:chosenRemoteIndex=0 124 | if 1 < len(l:remotes) 125 | ""Choose the correct text accoring to the action: 126 | let l:prompt = 'Choose remote to '.a:action.' `'.l:branch.handle.'`' 127 | if 'push' == a:action 128 | let l:prompt .= ' to:' 129 | else 130 | let l:prompt .= ' from:' 131 | endif 132 | let l:chosenRemoteIndex = merginal#util#inputList(l:prompt, l:remotes, 'MORE') 133 | "Check that the chosen index is in range 134 | if l:chosenRemoteIndex < 0 135 | echom ' ' 136 | echom string(l:chosenRemoteIndex) 137 | echom ' ' 138 | echom string(l:remotes) 139 | echom ' ' 140 | return 141 | endif 142 | endif 143 | 144 | let l:localBranchName = l:branch.name 145 | let l:chosenRemote = l:remotes[l:chosenRemoteIndex] 146 | 147 | let l:remoteBranchNameCanadidate = self.getRemoteBranchTrackedByLocalBranch(l:branch.name) 148 | if !empty(l:remoteBranchNameCanadidate) 149 | "Check that this is the same remote: 150 | if l:remoteBranchNameCanadidate =~ '\V\^'.escape(l:chosenRemote, '\').'/' 151 | "Remove the remote repository name 152 | let l:remoteBranchName = l:remoteBranchNameCanadidate[len(l:chosenRemote) + 1:(-1)] 153 | endif 154 | endif 155 | elseif l:branch.isRemote 156 | let l:chosenRemote = l:branch.remote 157 | if 'push' == a:action 158 | "For push, we want to specify the remote branch name 159 | let l:remoteBranchName = l:branch.name 160 | 161 | let l:locals = self.getLocalBranchNamesThatTrackARemoteBranch(l:branch.handle) 162 | 163 | if empty(l:locals) 164 | let l:localBranchName = l:branch.name 165 | elseif 1 == len(l:locals) 166 | let l:localBranchName = l:locals[0] 167 | else 168 | let l:chosenLocalIndex = merginal#util#inputList('Choose local branch to push `'.l:branch.handle.'` from:', l:locals, 'MORE') 169 | 170 | "Check that the chosen index is in range 171 | if l:chosenLocalIndex < 0 172 | return 173 | endif 174 | 175 | let l:localBranchName = l:locals[l:chosenLocalIndex] 176 | endif 177 | else 178 | "For pull and fetch, git automatically resolves the tracking 179 | "branch based on the remote branch. 180 | let l:localBranchName = l:branch.name 181 | endif 182 | endif 183 | 184 | if exists('l:remoteBranchName') && empty(l:remoteBranchName) 185 | unlet l:remoteBranchName 186 | endif 187 | 188 | let l:gitCommandWithArgs = [a:action] 189 | for l:flag in a:000 190 | call add(l:gitCommandWithArgs, l:flag) 191 | endfor 192 | 193 | let l:reloadBuffers = 0 194 | 195 | "Pulling requires the --no-commit flag 196 | if 'pull' == a:action 197 | if exists('l:remoteBranchName') 198 | let l:remoteBranchNameAsPrefix = l:remoteBranchName 199 | else 200 | let l:remoteBranchNameAsPrefix = '' 201 | endif 202 | let l:remoteBranchEscapedName = l:remoteBranchNameAsPrefix.l:localBranchName 203 | call add(l:gitCommandWithArgs, '--no-commit') 204 | let l:reloadBuffers = 1 205 | 206 | elseif 'push' == a:action 207 | if exists('l:remoteBranchName') 208 | let l:remoteBranchNameAsSuffix = ':'.l:remoteBranchName 209 | else 210 | let l:remoteBranchNameAsSuffix = '' 211 | endif 212 | let l:remoteBranchEscapedName = l:localBranchName.l:remoteBranchNameAsSuffix 213 | 214 | elseif 'fetch' == a:action 215 | if exists('l:remoteBranchName') 216 | let l:targetBranchName = l:remoteBranchName 217 | else 218 | let l:targetBranchName = l:localBranchName 219 | endif 220 | let l:remoteBranchEscapedName = l:targetBranchName 221 | endif 222 | 223 | call add(l:gitCommandWithArgs, l:chosenRemote) 224 | call add(l:gitCommandWithArgs, l:remoteBranchEscapedName) 225 | 226 | call add(l:gitCommandWithArgs, '--') 227 | 228 | call call(self.gitBang, l:gitCommandWithArgs, self) 229 | "if l:reloadBuffers 230 | "call merginal#reloadBuffers() 231 | "endif 232 | "call self.refresh() 233 | endfunction 234 | call s:f.addCommand('remoteActionForBranchUnderCursor', ['push'], 'MerginalPush', ['ps'], 'Prompt to choose a remote to push the branch under the cursor.') 235 | call s:f.addCommand('remoteActionForBranchUnderCursor', ['push', '--force-with-lease'], 'MerginalPushForce', ['pS'], 'Prompt to choose a remote to force (with lease) push the branch under the cursor.') 236 | call s:f.addCommand('remoteActionForBranchUnderCursor', ['pull'], 'MerginalPull', ['pl'], 'Prompt to choose a remote to pull the branch under the cursor.') 237 | call s:f.addCommand('remoteActionForBranchUnderCursor', ['pull', '--rebase'], 'MerginalPullRebase', ['pr'], 'Prompt to choose a remote to pull-rebase the branch under the cursor.') 238 | call s:f.addCommand('remoteActionForBranchUnderCursor', ['fetch'], 'MerginalFetch', ['pf'], 'Prompt to choose a remote to fetch the branch under the cursor.') 239 | 240 | function! s:f.renameBranchUnderCursor() dict abort 241 | let l:branch = self.branchDetails('.') 242 | if !l:branch.isLocal 243 | throw 'Can not rename - not a local branch' 244 | endif 245 | let l:newName = input('Rename `'.l:branch.handle.'` to: ', l:branch.name) 246 | echo ' ' 247 | if empty(l:newName) 248 | echom 'Branch rename canceled by user.' 249 | return 250 | elseif l:newName==l:branch.name 251 | echom 'Branch name was not modified.' 252 | return 253 | endif 254 | 255 | call self.gitEcho('branch', '-m', l:branch.name, l:newName, '--') 256 | call self.refresh() 257 | endfunction 258 | call s:f.addCommand('renameBranchUnderCursor', [], 'MerginalRenameBranch', 'rn', 'Prompt to rename the branch under the cursor.') 259 | 260 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/cherryPickConflicts.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'cherryPickConflicts', 'conflictsBase') 2 | 3 | function! s:f.generateHeader() dict abort 4 | let l:currentCommit = readfile(self.fugitiveContext.git_dir . '/ORIG_HEAD')[0] 5 | let l:currentCommitMessageLines = self.gitLines('log', '--format=%s', '-n1', l:currentCommit, '--') 6 | call insert(l:currentCommitMessageLines, '=== Reapplying: ===') 7 | call add(l:currentCommitMessageLines, '===================') 8 | call add(l:currentCommitMessageLines, '') 9 | return l:currentCommitMessageLines 10 | endfunction 11 | 12 | function! s:f.lastFileAdded() dict abort 13 | let l:cherryPickConflictsBuffer = bufnr('') 14 | Git 15 | let l:gitStatusBuffer = bufnr('') 16 | execute bufwinnr(l:cherryPickConflictsBuffer).'wincmd w' 17 | wincmd q 18 | execute bufwinnr(l:gitStatusBuffer).'wincmd w' 19 | endfunction 20 | 21 | 22 | function! s:f.cherryPickAction(action) dict abort 23 | call self.gitEcho('cherry-pick', '--'.a:action) 24 | call merginal#reloadBuffers() 25 | if self._getSpecialMode() == self.name 26 | call self.refresh() 27 | else 28 | ""If we finished cherry-picking - close the cherry-pick conflicts buffer 29 | wincmd q 30 | endif 31 | endfunction 32 | call s:f.addCommand('cherryPickAction', ['abort'], 'MerginalAbort', 'ca', 'Abort the cherry-pick.') 33 | call s:f.addCommand('cherryPickAction', ['continue'], 'MerginalContinue', 'cc', 'Continue to the next patch.') 34 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/conflictsBase.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'conflictsBase', 'fileListBase') 2 | 3 | function! s:f.generateBody() dict abort 4 | "Get the list of unmerged files: 5 | let l:conflicts = self.gitLines('ls-files', '--unmerged') 6 | 7 | "Split by tab - the first part is info, the second is the file name 8 | let l:conflicts = map(l:conflicts, 'split(v:val, "\t")') 9 | 10 | "Only take the stage 1 files - stage 2 and 3 are the same files with 11 | "different hash, and we don't care about the hash here 12 | let l:conflicts = filter(l:conflicts, 'v:val[0] =~ "\\v 1$"') 13 | 14 | "Take the file name - we no longer care about the info 15 | let l:conflicts = map(l:conflicts, 'v:val[1]') 16 | 17 | "If the working copy is not the current dir, we can get wrong paths. 18 | "We need to resolve that: 19 | let l:conflicts = map(l:conflicts, 'FugitiveFind(self.fugitiveContext, v:val)') 20 | 21 | "Make the paths as short as possible: 22 | let l:conflicts = map(l:conflicts, 'fnamemodify(v:val, ":~:.")') 23 | 24 | return l:conflicts 25 | endfunction 26 | 27 | function! s:f.fileDetails(lineNumber) dict abort 28 | call self.verifyLineInBody(a:lineNumber) 29 | 30 | let l:line = getline(a:lineNumber) 31 | let l:result = self.filePaths(l:line) 32 | 33 | return l:result 34 | endfunction 35 | 36 | 37 | function! s:f.openConflictedFileUnderCursor() dict abort 38 | echoerr 'openConflictedFileUnderCursor is deprecated - please use openFileUnderCursor instead' 39 | call self.openFileUnderCursor() 40 | endfunction 41 | 42 | function! s:f.addConflictedFileToStagingArea() dict abort 43 | let l:file = self.fileDetails('.') 44 | if empty(l:file.name) 45 | return 46 | endif 47 | 48 | call self.gitEcho('add', '--', fnamemodify(l:file.name, ':p')) 49 | call self.refresh() 50 | 51 | if empty(self.body) "This means that was the last file 52 | call self.lastFileAdded() 53 | endif 54 | endfunction 55 | call s:f.addCommand('addConflictedFileToStagingArea', [], 'MerginalAddConflictedFileToStagingArea', ['aa' ,'A'], 'Add the conflicted file under the cursor to the staging area.') 56 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/diffFiles.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'diffFiles', 'fileListBase') 2 | 3 | function! s:f.init(diffTarget) dict abort 4 | let self.diffTarget = a:diffTarget 5 | endfunction 6 | 7 | function! s:f.generateHeader() dict abort 8 | return [ 9 | \ '=== Diffing With: ===', 10 | \ self.diffTarget, 11 | \ '=====================', 12 | \ ''] 13 | endfunction 14 | 15 | function! s:f.generateBody() dict abort 16 | let l:diffFiles = self.gitLines('diff', '--name-status', self.diffTarget, '--') 17 | return l:diffFiles 18 | endfunction 19 | 20 | function! s:f.fileDetails(lineNumber) dict abort 21 | call self.verifyLineInBody(a:lineNumber) 22 | 23 | let l:line = getline(a:lineNumber) 24 | 25 | let l:matches = matchlist(l:line, '\v([ADM])\t(.*)$') 26 | 27 | if empty(l:matches) 28 | throw 'Unable to get diff files details for `'.l:line.'`' 29 | endif 30 | 31 | let l:result = self.filePaths(l:matches[2]) 32 | 33 | let l:result.isAdded = 0 34 | let l:result.isDeleted = 0 35 | let l:result.isModified = 0 36 | if 'A' == l:matches[1] 37 | let l:result.type = 'added' 38 | let l:result.isAdded = 1 39 | elseif 'D' == l:matches[1] 40 | let l:result.type = 'deleted' 41 | let l:result.isDeleted = 1 42 | else 43 | let l:result.type = 'modified' 44 | let l:result.isModified = 1 45 | endif 46 | 47 | return l:result 48 | endfunction 49 | 50 | 51 | function! s:f.openDiffFileUnderCursor() dict abort 52 | echoerr 'openDiffFileUnderCursor is deprecated - please use openFileUnderCursor instead' 53 | call self.openFileUnderCursor() 54 | endfunction 55 | 56 | 57 | function! s:f.openDiffFileUnderCursorAndDiff(diffType) dict abort 58 | if a:diffType!='h' && a:diffType!='v' 59 | throw 'Bad diff type' 60 | endif 61 | 62 | let l:diffFile = self.fileDetails('.') 63 | 64 | if l:diffFile.isAdded 65 | throw 'File does not exist in other buffer' 66 | endif 67 | 68 | "Close currently open git diffs 69 | let l:currentWindowBuffer = winbufnr('.') 70 | try 71 | windo if 'blob' == get(b:,'fugitive_type','') && exists('w:fugitive_diff_restore') 72 | \| bdelete 73 | \| endif 74 | catch 75 | "do nothing 76 | finally 77 | execute bufwinnr(l:currentWindowBuffer).'wincmd w' 78 | endtry 79 | 80 | call merginal#openFileDecidedWindow(self.fugitiveContext, l:diffFile.fileFullPath) 81 | 82 | execute ':G'.a:diffType.'diffsplit '.fnameescape(self.diffTarget) 83 | endfunction 84 | call s:f.addCommand('openDiffFileUnderCursorAndDiff', ['h'], 'MerginalDiff', 'ds', 'Split-diff against the file under the cursor (if it exists in the other branch)') 85 | call s:f.addCommand('openDiffFileUnderCursorAndDiff', ['v'], 'MerginalVDiff', 'dv', 'VSplit-diff against the file under the cursor (if it exists in the other branch)') 86 | 87 | 88 | function! s:f.checkoutDiffFileUnderCursor() dict abort 89 | let l:diffFile = self.fileDetails('.') 90 | 91 | if l:diffFile.isAdded 92 | throw 'File does not exist in diffed buffer' 93 | endif 94 | 95 | let l:answer = 1 96 | if !empty(glob(l:diffFile.fileFullPath)) 97 | let l:answer = 'yes' == input('Override `'.l:diffFile.fileInTree.'`? (type "yes" to confirm) ') 98 | endif 99 | if l:answer 100 | call self.gitEcho('checkout', self.diffTarget, '--', l:diffFile.fileFullPath) 101 | call merginal#reloadBuffers() 102 | call self.refresh() 103 | else 104 | echo 105 | echom 'File checkout canceled by user.' 106 | endif 107 | endfunction 108 | call s:f.addCommand('checkoutDiffFileUnderCursor', [], 'MerginalCheckoutDiffFile', 'co', 'Check out the file under the cursor (if it exists in the other branch) into the current branch.') 109 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/fileListBase.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'fileListBase', 'base') 2 | 3 | function! s:f.filePaths(filename) dict abort 4 | let l:result = {} 5 | let l:result.name = a:filename " For backwards compatibility 6 | let l:result.fileInTree = fnamemodify(a:filename, ':.') 7 | let l:result.fileFullPath = FugitiveFind(self.fugitiveContext, a:filename) 8 | return l:result 9 | endfunction 10 | 11 | 12 | function! s:f.openFileUnderCursor() dict abort 13 | let l:file = self.fileDetails('.') 14 | if empty(l:file.name) 15 | return 16 | endif 17 | call merginal#openFileDecidedWindow(self.fugitiveContext, l:file.fileFullPath) 18 | endfunction 19 | call s:f.addCommand('openFileUnderCursor', [], 'MerginalOpen', ['', 'o'], 'Open the file under the cursor.') 20 | 21 | 22 | function! s:f.previewFileUnderCursor() dict abort 23 | let l:currentWindowBuffer = winbufnr('.') 24 | try 25 | call self.openFileUnderCursor() 26 | finally 27 | execute bufwinnr(l:currentWindowBuffer).'wincmd w' 28 | endtry 29 | endfunction 30 | call s:f.addCommand('previewFileUnderCursor', [], 'MerginalPreview', 'go', 'Preview the file under the cursor.') 31 | 32 | 33 | function! s:f.openFileUnderCursorInNewWindow(newCommand) dict abort 34 | let l:file = self.fileDetails('.') 35 | if empty(l:file.name) 36 | return 37 | endif 38 | if a:newCommand != 'tabnew' 39 | execute winnr('#').'wincmd w' 40 | endif 41 | execute a:newCommand 42 | if bufexists(l:file.fileFullPath) 43 | execute 'buffer '.bufnr(l:file.fileFullPath) 44 | else 45 | execute 'edit '.l:file.fileFullPath 46 | endif 47 | endfunction 48 | call s:f.addCommand('openFileUnderCursorInNewWindow', ['new'], 'MerginalOpenSplit', 'i', 'Open the file under the cursor in a split.') 49 | call s:f.addCommand('openFileUnderCursorInNewWindow', ['vnew'], 'MerginalOpenVSplit', 's', 'Open the file under the cursor in a vsplit.') 50 | call s:f.addCommand('openFileUnderCursorInNewWindow', ['tabnew'], 'MerginalOpenTab', 't', 'Open the file under the cursor in a new tab.') 51 | 52 | 53 | function! s:f.previewFileUnderCursorInNewWindow(newCommand) dict abort 54 | let l:file = self.fileDetails('.') 55 | if empty(l:file.name) 56 | return 57 | endif 58 | let l:currentWindowBuffer = winbufnr('.') 59 | let l:currentTab = tabpagenr() " The new tab will be created AFTER this one, so this tab number will not change 60 | try 61 | if a:newCommand != 'tabnew' 62 | execute winnr('#').'wincmd w' 63 | endif 64 | execute a:newCommand 65 | if bufexists(l:file.fileFullPath) 66 | execute 'buffer '.bufnr(l:file.fileFullPath) 67 | else 68 | execute 'edit '.l:file.fileFullPath 69 | endif 70 | finally 71 | execute 'tabnext '.l:currentTab 72 | execute bufwinnr(l:currentWindowBuffer).'wincmd w' 73 | endtry 74 | endfunction 75 | call s:f.addCommand('previewFileUnderCursorInNewWindow', ['new'], 'MerginalPreviewSplit', 'gi', 'Preview the file under the cursor in a split.') 76 | call s:f.addCommand('previewFileUnderCursorInNewWindow', ['vnew'], 'MerginalPreviewVSplit', 'gs', 'Preview the file under the cursor in a vsplit.') 77 | call s:f.addCommand('previewFileUnderCursorInNewWindow', ['tabnew'], 'MerginalPreviewTab', 'T', 'Preview the file under the cursor in a new tab.') 78 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/historyLog.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'historyLog', 'base') 2 | 3 | function! s:f.init(branch) dict abort 4 | let self.branch = a:branch 5 | endfunction 6 | 7 | function! s:f.generateHeader() dict abort 8 | return [ 9 | \ '=== Showing History For: ===', 10 | \ self.branch, 11 | \ '============================', 12 | \ ''] 13 | endfunction 14 | 15 | function! s:f.generateBody() dict abort 16 | let l:logLines = self.gitLines('log', '--max-count', get(g:, 'merginal_logCommitCount', -1), '--format=%h %aN%n%ci%n%s%n', self.branch) 17 | if empty(l:logLines[len(l:logLines) - 1]) 18 | call remove(l:logLines, len(l:logLines) - 1) 19 | endif 20 | return l:logLines 21 | endfunction 22 | 23 | function! s:f.getFilteredBody() dict abort 24 | if empty(self.filter) 25 | return copy(self.body) 26 | endif 27 | let l:result = [] 28 | let l:lastBunch = [] 29 | for l:line in self.body 30 | if empty(l:line) 31 | if 0 <= match(l:lastBunch, self.filter) 32 | call extend(l:result, l:lastBunch) 33 | endif 34 | let l:lastBunch = [] 35 | endif 36 | call add(l:lastBunch, l:line) 37 | endfor 38 | if 0 <= match(l:lastBunch, self.filter) 39 | call extend(l:result, l:lastBunch) 40 | endif 41 | if !empty(l:result) && l:result[0] == '' 42 | call remove(l:result, 0) 43 | endif 44 | return l:result 45 | endfunction 46 | 47 | function! s:f.commitHash(lineNumber) dict abort 48 | call self.verifyLineInBody(a:lineNumber) 49 | if type(0) == type(a:lineNumber) 50 | let l:lineNumber = a:lineNumber 51 | else 52 | let l:lineNumber = line(a:lineNumber) 53 | endif 54 | while self.isLineInBody(l:lineNumber) && !empty(getline(l:lineNumber)) 55 | let l:lineNumber -= 1 56 | endwhile 57 | let l:lineNumber += 1 58 | return split(getline(l:lineNumber))[0] 59 | endfunction 60 | 61 | 62 | function! s:f.moveToNextOrPreviousCommit(direction) dict abort 63 | let l:line = line('.') 64 | 65 | "Find the first line of the current commit 66 | while !empty(getline(l:line - 1)) 67 | let l:line -= 1 68 | endwhile 69 | 70 | "Find the first line of the next/prev commit 71 | let l:line += a:direction 72 | while !empty(getline(l:line - 1)) 73 | let l:line += a:direction 74 | endwhile 75 | 76 | if l:line <= 0 || line('$') <= l:line 77 | "We reached past the first/last commit - go back! 78 | let l:line -= a:direction 79 | while !empty(getline(l:line - 1)) 80 | let l:line -= a:direction 81 | endwhile 82 | endif 83 | if self.isLineInBody(l:line) 84 | execute l:line 85 | endif 86 | endfunction 87 | call s:f.addCommand('moveToNextOrPreviousCommit', [-1], '', '', 'Move the cursor to the previous commit.') 88 | call s:f.addCommand('moveToNextOrPreviousCommit', [1], '', '', 'Move the cursor to the next commit.') 89 | 90 | function! s:f.printCommitUnderCurosr(format) dict abort 91 | let l:commitHash = self.commitHash('.') 92 | "Not using self.gitEcho() because we are insterested in the result as more 93 | "than just git command output. Also - using git-log with -1 instead of 94 | "git-show because for some reason git-show ignores the --format flag... 95 | echo join(self.gitLines('log', '-1', '--format='.a:format, l:commitHash, '--'), "\n") 96 | endfunction 97 | call s:f.addCommand('printCommitUnderCurosr', ['fuller'], 'MerginalShow', ['ss', 'S'], "Echo the commit details(using git's --format=fuller)") 98 | 99 | function! s:f.checkoutCommitUnderCurosr() dict abort 100 | let l:commitHash = self.commitHash('.') 101 | call self.gitEcho('checkout', l:commitHash, '--') 102 | call merginal#reloadBuffers() 103 | endfunction 104 | call s:f.addCommand('checkoutCommitUnderCurosr', [], 'MerginalCheckout', ['cc', 'C'], 'Checkout the commit under the cursor.') 105 | 106 | function! s:f.diffWithCommitUnderCursor() dict abort 107 | let l:commitHash = self.commitHash('.') 108 | call self.gotoBuffer('diffFiles', l:commitHash) 109 | endfunction 110 | call s:f.addCommand('diffWithCommitUnderCursor', [], 'MerginalDiff', 'gd', 'Open diff files buffer to diff against the commit under the cursor.') 111 | 112 | function! s:f.cherryPickCommitUnderCursor() dict abort 113 | let l:commitHash = self.commitHash('.') 114 | call self.gitEcho('cherry-pick', l:commitHash, '--') 115 | let l:confilctsBuffer = self.gotoSpecialModeBuffer() 116 | if empty(l:confilctsBuffer) 117 | call self.refresh() 118 | else 119 | if empty(l:confilctsBuffer.body) 120 | "If we are in cherry-pick mode without actual conflicts, this 121 | "means there are not conflicts and the user can be prompted to 122 | "enter a cherry-pick message. 123 | Git 124 | call merginal#closeMerginalBuffer() 125 | endif 126 | endif 127 | endfunction 128 | call s:f.addCommand('cherryPickCommitUnderCursor', [], 'MerginalCherryPick', 'cp', 'Cherry-pick the commit under the cursor') 129 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/immutableBranchList.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'immutableBranchList', 'base') 2 | 3 | function! s:f.generateBody() dict abort 4 | let l:args = ['branch'] 5 | if self.remoteVisible 6 | call add(l:args, '--all') 7 | endif 8 | call extend(l:args, get(g:, 'merginal_branchListFlags', [])) 9 | return call(self.gitLines, l:args, self) 10 | endfunction 11 | 12 | function! s:f.branchDetails(lineNumber) dict abort 13 | call self.verifyLineInBody(a:lineNumber) 14 | 15 | let l:line = getline(a:lineNumber) 16 | let l:result = {} 17 | 18 | "Check if this branch is the currently selected one 19 | let l:result.isCurrent = ('*' == l:line[0]) 20 | let l:line = l:line[2:] 21 | 22 | let l:detachedMatch = matchlist(l:line, '\v^\(detached from ([^/]+)%(/(.*))?\)$') 23 | if !empty(l:detachedMatch) 24 | let l:result.type = 'detached' 25 | let l:result.isLocal = 0 26 | let l:result.isRemote = 0 27 | let l:result.isDetached = 1 28 | let l:result.remote = l:detachedMatch[1] 29 | let l:result.name = l:detachedMatch[2] 30 | if empty(l:detachedMatch[2]) 31 | let l:result.handle = l:detachedMatch[1] 32 | else 33 | let l:result.handle = l:detachedMatch[1].'/'.l:detachedMatch[2] 34 | endif 35 | return l:result 36 | endif 37 | 38 | let l:remoteMatch = matchlist(l:line,'\v^remotes/([^/]+)%(/(\S*))%( \-\> (\S+))?$') 39 | if !empty(l:remoteMatch) 40 | let l:result.type = 'remote' 41 | let l:result.isLocal = 0 42 | let l:result.isRemote = 1 43 | let l:result.isDetached = 0 44 | let l:result.remote = l:remoteMatch[1] 45 | let l:result.name = l:remoteMatch[2] 46 | if empty(l:remoteMatch[2]) 47 | let l:result.handle = l:remoteMatch[1] 48 | else 49 | let l:result.handle = l:remoteMatch[1].'/'.l:remoteMatch[2] 50 | endif 51 | return l:result 52 | endif 53 | 54 | let l:result.type = 'local' 55 | let l:result.isLocal = 1 56 | let l:result.isRemote = 0 57 | let l:result.isDetached = 0 58 | let l:result.remote = '' 59 | let l:result.name = l:line 60 | let l:result.handle = l:line 61 | 62 | return l:result 63 | endfunction 64 | 65 | function! s:f.jumpToCurrentItem() dict abort 66 | "Find the current branch's index 67 | let l:currentBranchIndex = -1 68 | for l:i in range(len(self.body)) 69 | if '*' == self.body[i][0] 70 | let l:currentBranchIndex = l:i 71 | break 72 | endif 73 | endfor 74 | if -1 < l:currentBranchIndex 75 | "Jump to the current branch's line 76 | call self.jumpToIndexInBody(l:currentBranchIndex) 77 | endif 78 | endfunction 79 | 80 | function! s:f.getRemoteBranchTrackedByLocalBranch(localBranchName) dict abort 81 | let l:result = self.gitLines('branch', '--list', a:localBranchName, '-vv', '--') 82 | return matchstr(l:result, '\v\[\zs[^\[\]:]*\ze[\]:]') 83 | endfunction 84 | 85 | function! s:f.getLocalBranchNamesThatTrackARemoteBranch(remoteBranchName) dict abort 86 | "Get verbose list of branches 87 | let l:branchList = self.gitLines('branch', '-vv') 88 | 89 | "Filter for branches that track our remote 90 | let l:checkIfTrackingRegex = '\V['.escape(a:remoteBranchName, '\').'\[\]:]' 91 | let l:branchList = filter(l:branchList, 'v:val =~ l:checkIfTrackingRegex') 92 | 93 | "Extract the branch name from the matching lines 94 | let l:extractBranchNameRegex = '\v^\*?\s*\zs\S+' 95 | let l:branchList = map(l:branchList, 'matchstr(v:val, l:extractBranchNameRegex)') 96 | 97 | return l:branchList 98 | endfunction 99 | 100 | 101 | 102 | 103 | 104 | function! s:f.diffWithBranchUnderCursor() dict abort 105 | let l:branch = self.branchDetails('.') 106 | call self.gotoBuffer('diffFiles', l:branch.handle) 107 | endfunction 108 | call s:f.addCommand('diffWithBranchUnderCursor', [], 'MerginalDiff', 'gd', 'Open diff files buffer to diff against the branch under the cursor.') 109 | 110 | function! s:f.historyLogForBranchUnderCursor() dict abort 111 | let l:branch = self.branchDetails('.') 112 | call self.gotoBuffer('historyLog', l:branch.handle) 113 | endfunction 114 | call s:f.addCommand('historyLogForBranchUnderCursor', [], 'MerginalHistoryLog', 'gl', 'Open history log buffer to view the history of the branch under the cursor.') 115 | 116 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/mergeConflicts.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'mergeConflicts', 'conflictsBase') 2 | 3 | function! s:f.lastFileAdded() dict abort 4 | let l:mergeConflictsBuffer = bufnr('') 5 | Git 6 | let l:gitStatusBuffer = bufnr('') 7 | execute bufwinnr(l:mergeConflictsBuffer).'wincmd w' 8 | wincmd q 9 | execute bufwinnr(l:gitStatusBuffer).'wincmd w' 10 | endfunction 11 | 12 | function! s:f.mergeAction(action) dict abort 13 | call self.gitEcho('merge', '--'.a:action) 14 | call merginal#reloadBuffers() 15 | let l:mode = self._getSpecialMode() 16 | if l:mode == self.name 17 | call self.refresh() 18 | elseif empty(l:mode) 19 | "If we finished merging - close the merge conflicts buffer 20 | wincmd q 21 | else 22 | call self.gotoBuffer(l:mode) 23 | endif 24 | endfunction 25 | call s:f.addCommand('mergeAction', ['abort'], 'MerginalAbort', 'ma', 'Abort the merge.') 26 | call s:f.addCommand('mergeAction', ['continue'], 'MerginalContinue', 'mc', 'Conclude the merge') 27 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/rebaseAmend.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'rebaseAmend', 'immutableBranchList') 2 | 3 | function! s:f.generateHeader() dict abort 4 | let l:amendedCommit = readfile(self.fugitiveContext.git_dir . '/rebase-merge/amend') 5 | let l:amendedCommitShort = self.gitRun('rev-parse', '--short', l:amendedCommit[0], '--') 6 | let l:amendedCommitShort = substitute(l:amendedCommitShort,'\v[\r\n]','','g') 7 | let l:header = ['=== Amending '.l:amendedCommitShort.' ==='] 8 | 9 | let l:amendedCommitMessage = readfile(self.fugitiveContext.git_dir . '/rebase-merge/message') 10 | let l:header += l:amendedCommitMessage 11 | 12 | call add(l:header,repeat('=', len(l:header[0]))) 13 | call add(l:header, '') 14 | 15 | return l:header 16 | endfunction 17 | 18 | 19 | function! s:f.rebaseAction(action) dict abort 20 | call self.gitEcho('rebase', '--'.a:action) 21 | call merginal#reloadBuffers() 22 | let l:mode = self._getSpecialMode() 23 | if l:mode == self.name 24 | call self.refresh() 25 | elseif empty(l:mode) 26 | "If we finished rebasing - close the rebase amend buffer 27 | wincmd q 28 | else 29 | call self.gotoBuffer(l:mode) 30 | endif 31 | endfunction 32 | call s:f.addCommand('rebaseAction', ['abort'], 'MerginalAbort', 'ra', 'Abort the rebase.') 33 | call s:f.addCommand('rebaseAction', ['skip'], 'MerginalSkip', 'rs', 'Skip the current patch') 34 | call s:f.addCommand('rebaseAction', ['continue'], 'MerginalContinue', 'rc', 'Continue to the next patch.') 35 | -------------------------------------------------------------------------------- /autoload/merginal/buffers/rebaseConflicts.vim: -------------------------------------------------------------------------------- 1 | call merginal#modulelib#makeModule(s:, 'rebaseConflicts', 'conflictsBase') 2 | 3 | function! s:f.generateHeader() dict abort 4 | let l:currentCommit = readfile(self.fugitiveContext.git_dir . '/ORIG_HEAD')[0] 5 | let l:currentCommitMessageLines = self.gitLines('log', '--format=%s', '-n1', l:currentCommit, '--') 6 | call insert(l:currentCommitMessageLines, '=== Reapplying: ===') 7 | call add(l:currentCommitMessageLines, '===================') 8 | call add(l:currentCommitMessageLines, '') 9 | return l:currentCommitMessageLines 10 | endfunction 11 | 12 | function! s:f.lastFileAdded() dict abort 13 | echo 'Added the last file of this patch.' 14 | echo 'Continue to the next patch (y/N)?' 15 | let l:answer = getchar() 16 | if char2nr('y') == l:answer || char2nr('Y') == l:answer 17 | call self.rebaseAction('continue') 18 | endif 19 | endfunction 20 | 21 | 22 | function! s:f.rebaseAction(action) dict abort 23 | call self.gitEcho('rebase', '--'.a:action) 24 | call merginal#reloadBuffers() 25 | let l:mode = self._getSpecialMode() 26 | if l:mode == self.name 27 | call self.refresh() 28 | elseif empty(l:mode) 29 | "If we finished rebasing - close the rebase conflicts buffer 30 | wincmd q 31 | else 32 | call self.gotoBuffer(l:mode) 33 | endif 34 | endfunction 35 | call s:f.addCommand('rebaseAction', ['abort'], 'MerginalAbort', 'ra', 'Abort the rebase.') 36 | call s:f.addCommand('rebaseAction', ['skip'], 'MerginalSkip', 'rs', 'Skip the current patch') 37 | call s:f.addCommand('rebaseAction', ['continue'], 'MerginalContinue', 'rc', 'Continue to the next patch.') 38 | -------------------------------------------------------------------------------- /autoload/merginal/modulelib.vim: -------------------------------------------------------------------------------- 1 | 2 | let s:f = {} 3 | let s:modules = {} 4 | 5 | function! merginal#modulelib#makeModule(namespace, name, parent) 6 | let s:modules[a:name] = a:namespace 7 | let a:namespace.f = merginal#modulelib#prototype() 8 | let a:namespace.moduleName = a:name 9 | let a:namespace.parent = a:parent 10 | endfunction 11 | 12 | function! s:populate(object, moduleName) 13 | if !has_key(s:modules, a:moduleName) 14 | execute 'runtime autoload/merginal/buffers/'.a:moduleName.'.vim' 15 | endif 16 | let l:module = s:modules[a:moduleName] 17 | 18 | if !empty(l:module.parent) 19 | call s:populate(a:object, l:module.parent) 20 | endif 21 | 22 | let l:f = l:module.f 23 | 24 | for l:k in keys(l:f) 25 | if l:k != '_meta' && !has_key(s:f, l:k) 26 | let a:object[l:k] = l:f[l:k] 27 | endif 28 | endfor 29 | 30 | call extend(a:object._meta, l:f._meta) 31 | endfunction 32 | 33 | function! merginal#modulelib#createObject(moduleName) 34 | let l:obj = {} 35 | let l:obj.name = a:moduleName 36 | let l:obj._meta = [] 37 | call s:populate(l:obj, a:moduleName) 38 | return l:obj 39 | endfunction 40 | 41 | function! merginal#modulelib#prototype() 42 | let l:prototype = copy(s:f) 43 | let l:prototype._meta = [] 44 | return l:prototype 45 | endfunction 46 | 47 | function! s:f.new() dict abort 48 | let l:obj = {} 49 | for l:k in keys(self) 50 | if !has_key(s:f, l:k) 51 | let l:obj[l:k] = self[l:k] 52 | endif 53 | endfor 54 | 55 | return l:obj 56 | endfunction 57 | 58 | function! s:f.addCommand(functionName, args, command, keymaps, doc) dict abort 59 | let l:meta = {} 60 | 61 | let l:args = [] 62 | for l:arg in a:args 63 | call add(l:args, string(l:arg)) 64 | endfor 65 | let l:meta.execute = 'call b:merginal.'.a:functionName.'('.join(l:args, ', ').')' 66 | 67 | if !empty(a:command) 68 | let l:meta.command = a:command 69 | endif 70 | 71 | if empty(a:keymaps) 72 | elseif type(a:keymaps) == type([]) 73 | let l:meta.keymaps = a:keymaps 74 | else 75 | let l:meta.keymaps = [a:keymaps] 76 | endif 77 | 78 | if !empty(a:doc) 79 | let l:meta.doc = a:doc 80 | endif 81 | 82 | "let self._meta[a:functionName] = l:meta 83 | call add(self._meta, l:meta) 84 | endfunction 85 | -------------------------------------------------------------------------------- /autoload/merginal/util.vim: -------------------------------------------------------------------------------- 1 | "Similar to Vim's inputlist, but adds numbers and a 'more' option for huge 2 | "lists. If no options selected, returns -1(not 0 like inputlist!) 3 | function! merginal#util#inputList(prompt, options, morePrompt) abort 4 | let l:takeFrom = 0 5 | while l:takeFrom < len(a:options) 6 | let l:takeThisTime = &lines - 2 7 | if l:takeFrom + l:takeThisTime < len(a:options) 8 | let l:more = l:takeThisTime 9 | let l:takeThisTime -= 1 10 | else 11 | let l:more = 0 12 | endif 13 | 14 | let l:options = [a:prompt] 15 | 16 | for l:i in range(min([l:takeThisTime, len(a:options) - l:takeFrom])) 17 | call add(l:options, printf('%i) %s', 1 + l:i, a:options[l:takeFrom + l:i])) 18 | endfor 19 | if l:more 20 | call add(l:options, printf('%i) %s', l:more, a:morePrompt)) 21 | endif 22 | let l:selected = inputlist(l:options) 23 | if l:selected <= 0 || len(l:options) <= l:selected 24 | return -1 25 | elseif l:more && l:selected < l:more 26 | return l:takeFrom + l:selected - 1 27 | elseif !l:more && l:selected < len(l:options) 28 | return l:takeFrom + l:selected - 1 29 | endif 30 | 31 | "Create a new line for the next inputlist's prompt 32 | echo ' ' 33 | 34 | let l:takeFrom += l:takeThisTime 35 | endwhile 36 | endfunction 37 | 38 | function! merginal#util#makeColumns(widths, texts) abort 39 | let l:brokenToLines = [] 40 | for l:i in range(len(a:texts)) 41 | let l:text = a:texts[l:i] 42 | let l:width = a:widths[l:i] 43 | let l:lines = [] 44 | let l:line = '' 45 | let l:words = split(l:text, ' ') 46 | for l:word in l:words 47 | if l:width < len(l:line) + 1 + len(l:word) 48 | if !empty(l:line) 49 | call add(l:lines, l:line) 50 | endif 51 | while l:width < len(l:word) 52 | call add(l:lines, l:word[:l:width - 1]) 53 | let l:word = l:word[l:width :] 54 | endwhile 55 | let l:line = '' 56 | endif 57 | if !empty(l:line) 58 | let l:line .= ' ' 59 | endif 60 | let l:line .= l:word 61 | endfor 62 | if !empty(l:line) 63 | call add(l:lines, l:line) 64 | endif 65 | call add(l:brokenToLines, l:lines) 66 | endfor 67 | 68 | let l:maxLength = max(map(copy(l:brokenToLines), 'len(v:val)')) 69 | for l:lines in l:brokenToLines 70 | while len(l:lines) < l:maxLength 71 | call add(l:lines, '') 72 | endwhile 73 | endfor 74 | 75 | let l:result = [] 76 | for l:i in range(l:maxLength) 77 | let l:resultLine = '' 78 | for l:j in range(len(l:brokenToLines)) 79 | let l:width = a:widths[l:j] 80 | let l:line = l:brokenToLines[l:j][l:i] 81 | let l:resultLine .= l:line.repeat(' ', l:width - len(l:line)) 82 | let l:resultLine .= ' ' 83 | endfor 84 | let l:resultLine = substitute(l:resultLine, '\v\s*$', '', '') 85 | call add(l:result, l:resultLine) 86 | endfor 87 | 88 | return l:result 89 | endfunction 90 | 91 | let s:RememberdCursorWindow = {} 92 | function s:RememberdCursorWindow.restore() abort dict 93 | execute self.previousWindow.'wincmd w' 94 | execute self.currentWindow.'wincmd w' 95 | endfunction 96 | 97 | function! merginal#util#rememberCursorWindow() abort 98 | let l:rememberdCursorWindow = copy(s:RememberdCursorWindow) 99 | let l:rememberdCursorWindow.previousWindow = winnr('#') 100 | let l:rememberdCursorWindow.currentWindow = winnr() 101 | return l:rememberdCursorWindow 102 | endfunction 103 | -------------------------------------------------------------------------------- /doc/merginal.txt: -------------------------------------------------------------------------------- 1 | *merginal.txt* 2 | 3 | 4 | Author: Idan Arye 5 | License: Same terms as Vim itself (see |license|) 6 | 7 | Version: 2.2.4 8 | 9 | INTRODUCTION *merginal* 10 | 11 | Merginal aims provide a nice inteface for dealing with Git branches. It 12 | offers interactive TUI for: 13 | 14 | * Viewing the list of branches 15 | * Checking out branches from that list 16 | * Creating new branches 17 | * Deleting branches 18 | * Merging branches 19 | * Rebasing branches 20 | * Solving merge conflicts 21 | * Renaming branches 22 | * Viewing git history for branches 23 | 24 | 25 | REQUIREMENTS *merginal-requirements* 26 | 27 | Merginal is based on Fugitive, so it requires Fugitive. If you don't have it 28 | already you can get it from https://github.com/tpope/vim-fugitive 29 | 30 | It should go without saying that you need Git. 31 | 32 | Under Windows, vimproc is an optional requirement. Merginal will work without 33 | it, but it'll pop an ugly console window every time it needs to run a Git 34 | command. You can get vimproc from https://github.com/Shougo/vimproc.vim 35 | 36 | 37 | USAGE *merginal-usage* 38 | 39 | To use Merginal you need to know but one command: *:Merginal*. It'll open the 40 | |merginal-branch-list| buffer, unless the repository is in merge mode then 41 | it'll open the |merginal-merge-conflicts| buffer. 42 | 43 | Like Fugitive's commands, |:Merginal| is native to the buffer, and will only 44 | work in buffers that are parts of Git repositories. 45 | 46 | You can also toggle the buffer with |:MerginalToggle| or close it with 47 | |:MerginalClose|. 48 | 49 | To see a list of keymaps available in each Merginal buffer, press ?. 50 | 51 | 52 | CONFIGURATION *merginal-configuration* 53 | 54 | Set *g:merginal_windowWidth* or *g:merginal_windowSize* to the size of the window 55 | where the Merginal buffer will be shown. 56 | 57 | Set *g:merginal_splitType* to choose if vim-merginal will split vertically (the default) 58 | or horizontally (set this to '', empty string). 59 | 60 | Set *g:merginal_logCommitCount* to limit the number of commits displayed in the Merginal 61 | buffer. Please note that there is no `See More` feature so there will be no indicator if 62 | there are unseen commits. 63 | 64 | Set *g:merginal_remoteVisible* to choose if vim-merginal will view remote branches (the default) 65 | or hide (set this to 0). 66 | 67 | Set *g:merginal_showCommands* 0 to hide git command being run (default is 68 | 1). 69 | 70 | Set *g:merginal_resizeWindowToBranchLen* to 1 to automatically resize the split 71 | to the longest branch name length. This overrides g:merginal_windowWidth and 72 | g:merginal_windowSize. Default is 0. 73 | 74 | Set *g:merginal_resizePadding* to the number of characters you would like padded to the end 75 | of the branch name length when using *g:merginal_resizeWindowToBranchLen*. Default 5. 76 | 77 | Set *g:merginal_branchListFlags* to a list of flags that will be added to the 78 | "git branch" command when generating the |merginal-branch-list|. For example, 79 | to sort the branches in the list by reverse sorted order of last commit date, 80 | add the following to your Vim configuration: 81 | > 82 | let g:merginal_branchListFlags = ['--sort=-committerdate'] 83 | < 84 | -------------------------------------------------------------------------------- /plugin/merginal.vim: -------------------------------------------------------------------------------- 1 | function! s:toggleBasedOnMergeMode() abort 2 | let l:merginalWindowNumber = bufwinnr('Merginal:') 3 | if 0 <= l:merginalWindowNumber 4 | let l:merginalBufferNumber = winbufnr(l:merginalWindowNumber) 5 | let l:bufferObject = getbufvar(l:merginalBufferNumber, 'merginal') 6 | let l:mode = l:bufferObject._getSpecialMode() 7 | if l:mode == '' 8 | let l:mode = 'branchList' 9 | endif 10 | if l:bufferObject.name == l:mode 11 | call merginal#closeMerginalBuffer() 12 | return 13 | elseif l:merginalWindowNumber == winnr() 14 | call l:bufferObject.gotoBuffer(l:mode) 15 | else 16 | execute l:merginalWindowNumber.'wincmd w' 17 | endif 18 | else 19 | call merginal#openMerginalBuffer() 20 | endif 21 | endfunction 22 | 23 | command! -nargs=0 Merginal call merginal#openMerginalBuffer() 24 | command! -nargs=0 MerginalToggle call s:toggleBasedOnMergeMode() 25 | command! -nargs=0 MerginalClose call merginal#closeMerginalBuffer() 26 | --------------------------------------------------------------------------------