From 121cd1ddca25188fb422bcda70fd3394471f1340 Mon Sep 17 00:00:00 2001 From: Stefan Liebl <S.Liebl@gmx.de> Date: Tue, 10 Feb 2015 16:20:04 +0100 Subject: [PATCH] + merginal --- vimfiles/GetLatest/GetLatestVimScripts.dat | 1 + vimfiles/autoload/merginal.vim | 1052 ++++++++++++++++++++ vimfiles/doc/merginal.txt | 142 +++ vimfiles/doc/tags | 9 + vimfiles/plugin/merginal.vim | 51 + 5 files changed, 1255 insertions(+) create mode 100755 vimfiles/autoload/merginal.vim create mode 100755 vimfiles/doc/merginal.txt create mode 100755 vimfiles/plugin/merginal.vim diff --git a/vimfiles/GetLatest/GetLatestVimScripts.dat b/vimfiles/GetLatest/GetLatestVimScripts.dat index a863f5b..29cc84b 100644 --- a/vimfiles/GetLatest/GetLatestVimScripts.dat +++ b/vimfiles/GetLatest/GetLatestVimScripts.dat @@ -39,3 +39,4 @@ ScriptID SourceID Filename 2830 22798 csv.vim 3849 22637 git-time-lapse 4932 22787 diffchar.vim +4955 22442 Merginal diff --git a/vimfiles/autoload/merginal.vim b/vimfiles/autoload/merginal.vim new file mode 100755 index 0000000..1ebc5c0 --- /dev/null +++ b/vimfiles/autoload/merginal.vim @@ -0,0 +1,1052 @@ +"Use vimproc if available under windows to prevent opening a console window +function! merginal#system(command,...) + if has('win32') && exists(':VimProcBang') "We don't need vimproc when we use linux + if empty(a:000) + return vimproc#system(a:command) + else + return vimproc#system(a:command,a:000[0]) + endif + else + if empty(a:000) + return system(a:command) + else + return system(a:command,a:000[0]) + endif + endif +endfunction + +"Opens a file that belongs to a repo in a window that already belongs to that +"repo. Creates a new window if can't find suitable window. +function! merginal#openFileDecidedWindow(repo,fileName) + "We have to check with bufexists, because bufnr also match prefixes of the + "file name + let l:fileBuffer=-1 + if bufexists(a:fileName) + let l:fileBuffer=bufnr(a:fileName) + endif + + "We have to check with bufloaded, because bufwinnr also matches closed + "windows... + let l:windowToOpenIn=-1 + if bufloaded(l:fileBuffer) + let l:windowToOpenIn=bufwinnr(l:fileBuffer) + endif + + "If we found an open window with the correct file, we jump to it + if -1<l:windowToOpenIn + execute l:windowToOpenIn.'wincmd w' + else + "Check if the previous window can be used + let l:previousWindow=winnr('#') + if s:isWindowADisposableWindowOfRepo(l:previousWindow,a:repo) + execute winnr('#').'wincmd w' + else + "If the previous window can't be used, check if any open + "window can be used + let l:windowsToOpenTheFileIn=merginal#getListOfDisposableWindowsOfRepo(a:repo) + if empty(l:windowsToOpenTheFileIn) + "If no open window can be used, open a new Vim window + new + else + execute l:windowsToOpenTheFileIn[0].'wincmd w' + endif + endif + if -1<l:fileBuffer + "If the buffer is already open, jump to it + execute 'buffer '.l:fileBuffer + else + "Otherwise, load it + execute 'edit '.fnameescape(a:fileName) + endif + endif + diffoff "Just in case... +endfunction + +"Check if the current window is modifiable, saved, and belongs to the repo +function! s:isCurrentWindowADisposableWindowOfRepo(repo) + if !&modifiable + return 0 + endif + if &modified + return 0 + endif + try + return a:repo==fugitive#repo() + catch + return 0 + endtry +endfunction + +"Calls s:isCurrentWindowADisposableWindowOfRepo with a window number +function! s:isWindowADisposableWindowOfRepo(winnr,repo) + let l:currentWindow=winnr() + try + execute a:winnr.'wincmd w' + return s:isCurrentWindowADisposableWindowOfRepo(a:repo) + finally + execute l:currentWindow.'wincmd w' + endtry +endfunction + +"Get a list of windows that yield true with s:isWindowADisposableWindowOfRepo +function! merginal#getListOfDisposableWindowsOfRepo(repo) + let l:result=[] + let l:currentWindow=winnr() + windo if s:isCurrentWindowADisposableWindowOfRepo(a:repo) | call add(l:result,winnr()) | endif + execute l:currentWindow.'wincmd w' + return l:result +endfunction + +"Get the repository that belongs to a window +function! s:getRepoOfWindow(winnr) + "Ignore bad window numbers + if a:winnr<=0 + return {} + endif + let l:currentWindow=winnr() + try + execute a:winnr.'wincmd w' + return fugitive#repo() + catch + return {} + finally + execute l:currentWindow.'wincmd w' + endtry +endfunction + +"Reload the buffers +function! merginal#reloadBuffers() + let l:currentWindow=winnr() + try + silent windo if ''==&buftype + \| edit + \| endif + catch + "do nothing + endtry + execute l:currentWindow.'wincmd w' +endfunction + +"Exactly what it says on tin +function! merginal#runGitCommandInTreeReturnResult(repo,command) + let l:dir=getcwd() + execute 'cd '.fnameescape(a:repo.tree()) + try + return merginal#system(a:repo.git_command().' '.a:command) + finally + execute 'cd '.fnameescape(l:dir) + endtry +endfunction + +"Like merginal#runGitCommandInTreeReturnResult but split result to lines +function! merginal#runGitCommandInTreeReturnResultLines(repo,command) + let l:dir=getcwd() + execute 'cd '.fnameescape(a:repo.tree()) + try + return split(merginal#system(a:repo.git_command().' '.a:command),'\r\n\|\n\|\r') + finally + execute 'cd '.fnameescape(l:dir) + endtry +endfunction + +"Returns 1 if there was a merginal bufffer to close +function! merginal#closeMerginalBuffer() + let l:merginalWindowNumber=bufwinnr('Merginal:') + if 0<=l:merginalWindowNumber + let l:currentWindow=winnr() + try + execute l:merginalWindowNumber.'wincmd w' + wincmd q + "If the current window is after the merginal window, closing the + "merginal window will decrease the current window's nubmer. + if l:merginalWindowNumber<l:currentWindow + let l:currentWindow=l:currentWindow-1 + endif + return 1 + finally + execute l:currentWindow.'wincmd w' + endtry + end + return 0 +endfunction + +"Returns 1 if a new buffer was opened, 0 if it already existed +function! merginal#openTuiBuffer(bufferName,inWindow) + let l:repo=fugitive#repo() + + let l:tuiBufferWindow=bufwinnr(bufnr(a:bufferName)) + + if -1<l:tuiBufferWindow "Jump to the already open buffer + execute l:tuiBufferWindow.'wincmd w' + else "Open a new buffer + if merginal#isMerginalWindow(a:inWindow) + execute a:inWindow.'wincmd w' + enew + else + 40vnew + endif + setlocal buftype=nofile + setlocal bufhidden=wipe + setlocal nomodifiable + setlocal winfixwidth + setlocal winfixheight + setlocal nonumber + setlocal norelativenumber + execute 'silent file '.a:bufferName + call fugitive#detect(l:repo.dir()) + endif + + "At any rate, reassign the active repository + let b:merginal_repo=l:repo + let b:headerLinesCount=0 + + "Check and return if a new buffer was created + return -1==l:tuiBufferWindow +endfunction + + +"Check if a window belongs to Merginal +function! merginal#isMerginalWindow(winnr) + if a:winnr<=0 + return 0 + endif + let l:buffer=winbufnr(a:winnr) + if l:buffer<=0 + return 0 + endif + "check for the merginal repo buffer variable + return !empty(getbufvar(l:buffer,'merginal_repo')) +endfunction + + +"For the branch in the specified line, retrieve: +" - type: 'local', 'remote' or 'detached' +" - isCurrent, isLocal, isRemote, isDetached +" - remote: the name of the remote or '' for local branches +" - name: the name of the branch, without the remote +" - handle: the named used for referring the branch in git commands +function! merginal#branchDetails(lineNumber) + if !exists('b:merginal_repo') + throw 'Unable to get branch details outside the merginal window' + endif + if line(a:lineNumber)<=b:headerLinesCount + throw 'Unable to get branch details for the header of the merginal window' + endif + let l:line=getline(a:lineNumber) + let l:result={} + + + "Check if this branch is the currently selected one + let l:result.isCurrent=('*'==l:line[0]) + let l:line=l:line[2:] + + let l:detachedMatch=matchlist(l:line,'\v^\(detached from ([^/]+)%(/(.*))?\)$') + if !empty(l:detachedMatch) + let l:result.type='detached' + let l:result.isLocal=0 + let l:result.isRemote=0 + let l:result.isDetached=1 + let l:result.remote=l:detachedMatch[1] + let l:result.name=l:detachedMatch[2] + if empty(l:detachedMatch[2]) + let l:result.handle=l:detachedMatch[1] + else + let l:result.handle=l:detachedMatch[1].'/'.l:detachedMatch[2] + endif + return l:result + endif + + let l:remoteMatch=matchlist(l:line,'\v^remotes/([^/]+)%(/(\S*))%( \-\> (\S+))?$') + if !empty(l:remoteMatch) + let l:result.type='remote' + let l:result.isLocal=0 + let l:result.isRemote=1 + let l:result.isDetached=0 + let l:result.remote=l:remoteMatch[1] + let l:result.name=l:remoteMatch[2] + if empty(l:remoteMatch[2]) + let l:result.handle=l:remoteMatch[1] + else + let l:result.handle=l:remoteMatch[1].'/'.l:remoteMatch[2] + endif + return l:result + endif + + let l:result.type='local' + let l:result.isLocal=1 + let l:result.isRemote=0 + let l:result.isDetached=0 + let l:result.remote='' + let l:result.name=l:line + let l:result.handle=l:line + + return l:result +endfunction + +"For the file in the specified line, retrieve: +" - name: the name of the file +function! merginal#fileDetails(lineNumber) + if !exists('b:merginal_repo') + throw 'Unable to get file details outside the merginal window' + endif + if line(a:lineNumber)<=b:headerLinesCount + throw 'Unable to get branch details for the header of the merginal window' + endif + let l:line=getline(a:lineNumber) + let l:result={} + + let l:result.name=l:line + + return l:result +endfunction + +function! merginal#getLocalBranchNamesThatTrackARemoteBranch(remoteBranchName) + "Get verbose list of branches + let l:branchList=split(merginal#system(b:merginal_repo.git_command('branch','-vv')),'\r\n\|\n\|\r') + + "Filter for branches that track our remote + let l:checkIfTrackingRegex='\V['.escape(a:remoteBranchName,'\').'\[\]:]' + let l:branchList=filter(l:branchList,'v:val=~l:checkIfTrackingRegex') + + "Extract the branch name from the matching lines + "let l:extractBranchNameRegex='\v^\*?\s*(\S+)' + "let l:branchList=map(l:branchList,'matchlist(v:val,l:extractBranchNameRegex)[1]') + let l:extractBranchNameRegex='\v^\*?\s*\zs\S+' + let l:branchList=map(l:branchList,'matchstr(v:val,l:extractBranchNameRegex)') + + return l:branchList +endfunction + +function! merginal#getRemoteBranchTrackedByLocalBranch(localBranchName) + let l:result=merginal#system(b:merginal_repo.git_command('branch','--list',a:localBranchName,'-vv')) + echo l:result + return matchstr(l:result,'\v\[\zs[^\[\]:]*\ze[\]:]') +endfunction + + +"Check if the current buffer's repo is in rebase mode +function! merginal#isRebaseMode() + return isdirectory(fugitive#repo().dir('rebase-apply')) +endfunction + +"Check if the current buffer's repo is in rebase amend mode +function! merginal#isRebaseAmendMode() + return isdirectory(fugitive#repo().dir('rebase-merge')) +endfunction + +"Check if the current buffer's repo is in merge mode +function! merginal#isMergeMode() + "Use glob() to check for file existence + return !empty(glob(fugitive#repo().dir('MERGE_MODE'))) +endfunction + +"Open the branch list buffer for controlling buffers +function! merginal#openBranchListBuffer(...) + if merginal#openTuiBuffer('Merginal:Branches',get(a:000,1,bufwinnr('Merginal:'))) + doautocmd User Merginal_BranchList + endif + + "At any rate, refresh the buffer: + call merginal#tryRefreshBranchListBuffer(1) +endfunction + +augroup merginal + autocmd User Merginal_BranchList nnoremap <buffer> R :call merginal#tryRefreshBranchListBuffer(0)<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> C :call <SID>checkoutBranchUnderCursor()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> cc :call <SID>checkoutBranchUnderCursor()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> ct :call <SID>trackBranchUnderCursor(0)<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> cT :call <SID>trackBranchUnderCursor(1)<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> A :call <SID>promptToCreateNewBranch()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> aa :call <SID>promptToCreateNewBranch()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> D :call <SID>deleteBranchUnderCursor()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> dd :call <SID>deleteBranchUnderCursor()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> M :call <SID>mergeBranchUnderCursor()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> mm :call <SID>mergeBranchUnderCursor()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> mf :call <SID>mergeBranchUnderCursorUsingFugitive()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> rb :call <SID>rebaseBranchUnderCursor()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> ps :call <SID>remoteActionForBranchUnderCursor('push',0)<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> pS :call <SID>remoteActionForBranchUnderCursor('push',1)<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> pl :call <SID>remoteActionForBranchUnderCursor('pull',0)<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> pf :call <SID>remoteActionForBranchUnderCursor('fetch',0)<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> gd :call <SID>diffWithBranchUnderCursor()<Cr> + autocmd User Merginal_BranchList nnoremap <buffer> rn :call <SID>renameBranchUnderCursor()<Cr> +augroup END + +"If the current buffer is a branch list buffer - refresh it! +function! merginal#tryRefreshBranchListBuffer(jumpToCurrentBranch) + if 'Merginal:Branches'==bufname('') + let l:branchList=split(merginal#system(b:merginal_repo.git_command('branch','--all')),'\r\n\|\n\|\r') + let l:currentLine=line('.') + + setlocal modifiable + "Clear the buffer: + normal ggdG + "Write the branch list: + call setline(1,l:branchList) + setlocal nomodifiable + + + if a:jumpToCurrentBranch + "Find the current branch's index + let l:currentBranchIndex=-1 + for i in range(len(l:branchList)) + if '*'==l:branchList[i][0] + let l:currentBranchIndex=i + break + endif + endfor + if -1<l:currentBranchIndex + "Jump to the current branch's line + execute l:currentBranchIndex+1 + endif + else + execute l:currentLine + endif + endif +endfunction + +"Exactly what it says on tin +function! s:checkoutBranchUnderCursor() + if 'Merginal:Branches'==bufname('') + let l:branch=merginal#branchDetails('.') + echo merginal#runGitCommandInTreeReturnResult(b:merginal_repo,'--no-pager checkout '.shellescape(l:branch.handle)) + call merginal#reloadBuffers() + call merginal#tryRefreshBranchListBuffer(0) + endif +endfunction + +"Track what it says on tin +function! s:trackBranchUnderCursor(promptForName) + if 'Merginal:Branches'==bufname('') + let l:branch=merginal#branchDetails('.') + if !l:branch.isRemote + throw 'Can not track - branch is not remote' + endif + let l:newBranchName=l:branch.name + if a:promptForName + let l:newBranchName=input('Track `'.l:branch.handle.'` as: ',l:newBranchName) + if empty(l:newBranchName) + echo ' ' + echo 'Branch tracking canceled by the user' + return + endif + endif + echo merginal#runGitCommandInTreeReturnResult(b:merginal_repo,'--no-pager checkout -b '.shellescape(l:newBranchName).' --track '.shellescape(l:branch.handle)) + call merginal#reloadBuffers() + call merginal#tryRefreshBranchListBuffer(0) + endif +endfunction + +"Uses the current branch as the source +function! s:promptToCreateNewBranch() + if 'Merginal:Branches'==bufname('') + let l:newBranchName=input('Branch `'.b:merginal_repo.head().'` to: ') + if empty(l:newBranchName) + echo ' ' + echo 'Branch creation canceled by the user' + return + endif + echo merginal#runGitCommandInTreeReturnResult(b:merginal_repo,'--no-pager checkout -b '.shellescape(l:newBranchName)) + call merginal#reloadBuffers() + call merginal#tryRefreshBranchListBuffer(1) + endif +endfunction + +"Verifies the decision +function! s:deleteBranchUnderCursor() + if 'Merginal:Branches'==bufname('') + let l:branch=merginal#branchDetails('.') + let l:answer=0 + if l:branch.isLocal + let l:answer='yes'==input('Delete branch `'.l:branch.handle.'`?(type "yes" to confirm) ') + elseif l:branch.isRemote + "Deleting remote branches needs a special warning + let l:answer='yes-remote'==input('Delete remote(!) branch `'.l:branch.handle.'`?(type "yes-remote" to confirm) ') + endif + if l:answer + if l:branch.isLocal + echo ' ' + echo merginal#runGitCommandInTreeReturnResult(b:merginal_repo,'--no-pager branch -D '.shellescape(l:branch.handle)) + else + execute '!'.b:merginal_repo.git_command('push').' '.shellescape(l:branch.remote).' --delete '.shellescape(l:branch.name) + endif + call merginal#reloadBuffers() + call merginal#tryRefreshBranchListBuffer(0) + else + echo ' ' + echo 'Branch deletion canceled by the user' + endif + endif +endfunction + +"If there are merge conflicts, opens the merge conflicts buffer +function! s:mergeBranchUnderCursor() + if 'Merginal:Branches'==bufname('') + let l:branch=merginal#branchDetails('.') + echo ' ' + echo merginal#runGitCommandInTreeReturnResult(b:merginal_repo,'merge --no-commit '.shellescape(l:branch.handle)) + call merginal#reloadBuffers() + if v:shell_error + call merginal#openMergeConflictsBuffer(winnr()) + elseif merginal#isMergeMode() + "If we are in merge mode without a shell error, that means there + "are not conflicts and the user can be prompted to enter a merge + "message. + Gstatus + call merginal#closeMerginalBuffer() + endif + endif +endfunction + +"Use Fugitive's :Gmerge. It was added to Fugitive after I implemented +"Merginal's merge, and I don't want to remove it since it can still more +"comfortable for some. +function! s:mergeBranchUnderCursorUsingFugitive() + if 'Merginal:Branches'==bufname('') + let l:branch=merginal#branchDetails('.') + execute ':Gmerge '.l:branchName.handle + endif +endfunction + +"If there are rebase conflicts, opens the rebase conflicts buffer +function! s:rebaseBranchUnderCursor() + if 'Merginal:Branches'==bufname('') + let l:branch=merginal#branchDetails('.') + echo ' ' + echo merginal#runGitCommandInTreeReturnResult(b:merginal_repo,'rebase '.shellescape(l:branch.handle)) + call merginal#reloadBuffers() + if v:shell_error + call merginal#openRebaseConflictsBuffer(winnr()) + endif + endif +endfunction + +"Run various remote actions +function! s:remoteActionForBranchUnderCursor(remoteAction,force) + if 'Merginal:Branches'==bufname('') + let l:branch=merginal#branchDetails('.') + if l:branch.isLocal + let l:remotes=merginal#runGitCommandInTreeReturnResultLines(b:merginal_repo,'remote') + if empty(l:remotes) + throw 'Can not '.a:remoteAction.' - no remotes defined' + endif + + let l:chosenRemoteIndex=0 + if 1<len(l:remotes) + let l:listForInputlist=map(copy(l:remotes),'v:key+1.") ".v:val') + "Choose the correct text accoring to the action: + if 'push'==a:remoteAction + call insert(l:listForInputlist,'Choose remote to '.a:remoteAction.' `'.l:branch.handle.'` to:') + else + call insert(l:listForInputlist,'Choose remote to '.a:remoteAction.' `'.l:branch.handle.'` from:') + endif + let l:chosenRemoteIndex=inputlist(l:listForInputlist) + + "Check that the chosen index is in range + if l:chosenRemoteIndex<=0 || len(l:remotes)<l:chosenRemoteIndex + return + endif + + let l:chosenRemoteIndex=l:chosenRemoteIndex-1 + endif + + let l:localBranchName=l:branch.name + let l:chosenRemote=l:remotes[l:chosenRemoteIndex] + + let l:remoteBranchNameCanadidate=merginal#getRemoteBranchTrackedByLocalBranch(l:branch.name) + echo ' ' + if !empty(l:remoteBranchNameCanadidate) + "Check that this is the same remote: + if l:remoteBranchNameCanadidate=~'\V\^'.escape(l:chosenRemote,'\').'/' + "Remote the remote repository name + let l:remoteBranchName=l:remoteBranchNameCanadidate[len(l:chosenRemote)+1:(-1)] + endif + endif + elseif l:branch.isRemote + let l:chosenRemote=l:branch.remote + if 'push'==a:remoteAction + "For push, we want to specify the remote branch name + let l:remoteBranchName=l:branch.name + + let l:locals=merginal#getLocalBranchNamesThatTrackARemoteBranch(l:branch.handle) + if empty(l:locals) + let l:localBranchName=l:branch.name + elseif 1==len(l:locals) + let l:localBranchName=l:locals[0] + else + let l:listForInputlist=map(copy(l:locals),'v:key+1.") ".v:val') + call insert(l:listForInputlist,'Choose local branch to push `'.l:branch.handle.'` from:') + let l:chosenLocalIndex=inputlist(l:listForInputlist) + + "Check that the chosen index is in range + if l:chosenLocalIndex<=0 || len(l:locals)<l:chosenLocalIndex + return + endif + + let l:localBranchName=l:locals[l:chosenLocalIndex-1] + endif + else + "For pull and fetch, git automatically resolves the tracking + "branch based on the remote branch. + let l:localBranchName=l:branch.name + endif + endif + + if exists('l:remoteBranchName') && empty(l:remoteBranchName) + unlet l:remoteBranchName + endif + + let l:gitCommandWithArgs=[a:remoteAction] + if a:force + call add(l:gitCommandWithArgs,'--force') + endif + + let l:reloadBuffers=0 + + "Pulling requires the --no-commit flag + if 'pull'==a:remoteAction + if exists('l:remoteBranchName') + let l:remoteBranchNameAsPrefix=shellescape(l:remoteBranchName).':' + else + let l:remoteBranchNameAsPrefix='' + endif + let l:remoteBranchEscapedName=l:remoteBranchNameAsPrefix.shellescape(l:localBranchName) + call add(l:gitCommandWithArgs,'--no-commit') + let l:reloadBuffers=1 + + elseif 'push'==a:remoteAction + if exists('l:remoteBranchName') + let l:remoteBranchNameAsSuffix=':'.shellescape(l:remoteBranchName) + else + let l:remoteBranchNameAsSuffix='' + endif + let l:remoteBranchEscapedName=shellescape(l:localBranchName).l:remoteBranchNameAsSuffix + + elseif 'fetch'==a:remoteAction + if exists('l:remoteBranchName') + let l:targetBranchName=l:remoteBranchName + else + let l:targetBranchName=l:localBranchName + endif + let l:remoteBranchEscapedName=shellescape(l:targetBranchName) + execute '!'.b:merginal_repo.git_command(a:remoteAction).' '.shellescape(l:chosenRemote).' '.shellescape(l:targetBranchName) + endif + execute '!'.call(b:merginal_repo.git_command,l:gitCommandWithArgs,b:merginal_repo).' '.shellescape(l:chosenRemote).' '.l:remoteBranchEscapedName + if l:reloadBuffers + call merginal#reloadBuffers() + endif + call merginal#tryRefreshBranchListBuffer(0) + endif +endfunction + + +"Opens the diff files buffer +function! s:diffWithBranchUnderCursor() + if 'Merginal:Branches'==bufname('') + \|| 'Merginal:RebaseAmend'==bufname('') + let l:branch=merginal#branchDetails('.') + if l:branch.isCurrent + throw 'Can not diff against the current branch' + endif + call merginal#openDiffFilesBuffer(l:branch) + endif +endfunction + + +"Prompts for a new name to the branch and renames it +function! s:renameBranchUnderCursor() + if 'Merginal:Branches'==bufname('') + let l:branch=merginal#branchDetails('.') + if !l:branch.isLocal + throw 'Can not rename - not a local branch' + endif + let l:newName=input('Rename `'.l:branch.handle.'` to: ',l:branch.name) + echo ' ' + if empty(l:newName) + echo 'Branch rename canceled by the user' + return + elseif l:newName==l:branch.name + echo 'Branch name was not modified' + return + endif + + let l:gitCommand=b:merginal_repo.git_command('branch','-m',l:branch.name,l:newName) + let l:result=merginal#system(l:gitCommand) + echo l:result + call merginal#tryRefreshBranchListBuffer(0) + endif +endfunction + + + +"Open the merge conflicts buffer for resolving merge conflicts +function! merginal#openMergeConflictsBuffer(...) + let l:currentFile=expand('%:~:.') + if merginal#openTuiBuffer('Merginal:Conflicts',get(a:000,1,bufwinnr('Merginal:'))) + doautocmd User Merginal_MergeConflicts + endif + + "At any rate, refresh the buffer: + call merginal#tryRefreshMergeConflictsBuffer(l:currentFile) +endfunction + +augroup merginal + autocmd User Merginal_MergeConflicts nnoremap <buffer> R :call merginal#tryRefreshMergeConflictsBuffer(0)<Cr> + autocmd User Merginal_MergeConflicts nnoremap <buffer> <Cr> :call <SID>openMergeConflictUnderCursor()<Cr> + autocmd User Merginal_MergeConflicts nnoremap <buffer> A :call <SID>addConflictedFileToStagingArea()<Cr> + autocmd User Merginal_MergeConflicts nnoremap <buffer> aa :call <SID>addConflictedFileToStagingArea()<Cr> +augroup END + +"Returns 1 if all merges are done +function! s:refreshConflictsBuffer(fileToJumpTo,headerLines) + "Get the list of unmerged files: + let l:conflicts=split(merginal#system(b:merginal_repo.git_command('ls-files','--unmerged')),'\r\n\|\n\|\r') + "Split by tab - the first part is info, the second is the file name + let l:conflicts=map(l:conflicts,'split(v:val,"\t")') + "Only take the stage 1 files - stage 2 and 3 are the same files with + "different hash, and we don't care about the hash here + let l:conflicts=filter(l:conflicts,'v:val[0] =~ "\\v 1$"') + "Take the file name - we no longer care about the info + let l:conflicts=map(l:conflicts,'v:val[1]') + "If the working copy is not the current dir, we can get wrong paths. + "We need to resulve that: + let l:conflicts=map(l:conflicts,'b:merginal_repo.tree(v:val)') + "Make the paths as short as possible: + let l:conflicts=map(l:conflicts,'fnamemodify(v:val,":~:.")') + + + let l:currentLine=line('.')-b:headerLinesCount + + setlocal modifiable + "Clear the buffer: + normal ggdG + "Write the branch list: + call setline(1,a:headerLines+l:conflicts) + let b:headerLinesCount=len(a:headerLines) + let l:currentLine=l:currentLine+b:headerLinesCount + setlocal nomodifiable + if empty(l:conflicts) + return 1 + endif + + if empty(a:fileToJumpTo) + if 0<l:currentLine + execute l:currentLine + endif + else + let l:lineNumber=search('\V\^'+escape(a:fileToJumpTo,'\')+'\$','cnw') + if 0<l:lineNumber + execute l:lineNumber + else + execute l:currentLine + endif + endif + return 0 +endfunction + +"Returns 1 if all merges are done +function! merginal#tryRefreshMergeConflictsBuffer(fileToJumpTo) + if 'Merginal:Conflicts'==bufname('') + return s:refreshConflictsBuffer(a:fileToJumpTo,[]) + endif + return 0 +endfunction + +"Exactly what it says on tin +function! s:openMergeConflictUnderCursor() + if 'Merginal:Conflicts'==bufname('') + \|| 'Merginal:Rebase'==bufname('') + let l:file=merginal#fileDetails('.') + if empty(l:file.name) + return + endif + call merginal#openFileDecidedWindow(b:merginal_repo,l:file.name) + endif +endfunction + +"If that was the last merge conflict, automatically opens Fugitive's status +"buffer +function! s:addConflictedFileToStagingArea() + if 'Merginal:Conflicts'==bufname('') + \|| 'Merginal:Rebase'==bufname('') + let l:file=merginal#fileDetails('.') + if empty(l:file.name) + return + endif + echo merginal#runGitCommandInTreeReturnResult(b:merginal_repo,'--no-pager add '.shellescape(fnamemodify(l:file.name,':p'))) + + if 'Merginal:Conflicts'==bufname('') + if merginal#tryRefreshMergeConflictsBuffer(0) + "If this returns 1, that means this is the last branch, and we + "should open gufitive's status window + let l:mergeConflictsBuffer=bufnr('') + Gstatus + let l:gitStatusBuffer=bufnr('') + execute bufwinnr(l:mergeConflictsBuffer).'wincmd w' + wincmd q + execute bufwinnr(l:gitStatusBuffer).'wincmd w' + endif + else + if merginal#tryRefreshRebaseConflictsBuffer(0) + echo 'Added the last file of this patch.' + echo 'Continue to the next patch(y/n)?' + let l:answer=getchar() + if char2nr('y')==l:answer || char2nr('Y')==l:answer + call s:rebaseAction('continue') + endif + endif + endif + endif +endfunction + + +"Open the diff files buffer for diffing agains another branch +function! merginal#openDiffFilesBuffer(diffBranch,...) + if merginal#openTuiBuffer('Merginal:Diff',get(a:000,1,bufwinnr('Merginal:'))) + doautocmd User Merginal_DiffFiles + endif + + let b:merginal_diffBranch=a:diffBranch + + "At any rate, refresh the buffer: + call merginal#tryRefreshDiffFilesBuffer() +endfunction + +augroup merginal + autocmd User Merginal_DiffFiles nnoremap <buffer> R :call merginal#tryRefreshDiffFilesBuffer()<Cr> + autocmd User Merginal_DiffFiles nnoremap <buffer> <Cr> :call <SID>openDiffFileUnderCursor()<Cr> + autocmd User Merginal_DiffFiles nnoremap <buffer> ds :call <SID>openDiffFileUnderCursorAndDiff('s')<Cr> + autocmd User Merginal_DiffFiles nnoremap <buffer> dv :call <SID>openDiffFileUnderCursorAndDiff('v')<Cr> + autocmd User Merginal_DiffFiles nnoremap <buffer> co :call <SID>checkoutDiffFileUnderCursor()<Cr> +augroup END + + +"For the diff file in the specified line, retrieve: +" - type: 'added', 'deleted' or 'modified' +" - isAdded, isDeleted, isModified +" - fileInTree: the path of the file relative to the repo +" - fileFullPath: the full path to the file +function! merginal#diffFileDetails(lineNumber) + if !exists('b:merginal_repo') + throw 'Unable to get diff file details outside the merginal window' + endif + let l:line=getline(a:lineNumber) + let l:result={} + + let l:matches=matchlist(l:line,'\v([ADM])\t(.*)$') + + if empty(l:matches) + throw 'Unable to get diff files details for `'.l:line.'`' + endif + + let l:result.isAdded=0 + let l:result.isDeleted=0 + let l:result.isModified=0 + if 'A'==l:matches[1] + let l:result.type='added' + let l:result.isAdded=1 + elseif 'D'==l:matches[1] + let l:result.type='deleted' + let l:result.isDeleted=1 + else + let l:result.type='modified' + let l:result.isModified=1 + endif + + let l:result.fileInTree=l:matches[2] + let l:result.fileFullPath=b:merginal_repo.tree(l:matches[2]) + + return l:result +endfunction + +"If the current buffer is a branch list buffer - refresh it! +function! merginal#tryRefreshDiffFilesBuffer() + if 'Merginal:Diff'==bufname('') + let l:diffBranch=b:merginal_diffBranch + let l:diffFiles=merginal#runGitCommandInTreeReturnResultLines(b:merginal_repo,'diff --name-status '.shellescape(l:diffBranch.handle)) + let l:currentLine=line('.') + + setlocal modifiable + "Clear the buffer: + normal ggdG + "Write the diff files list: + call setline(1,l:diffFiles) + setlocal nomodifiable + + execute l:currentLine + endif +endfunction + +"Exactly what it says on tin +function! s:openDiffFileUnderCursor() + if 'Merginal:Diff'==bufname('') + let l:diffFile=merginal#diffFileDetails('.') + + if l:diffFile.isDeleted + throw 'File does not exist in current buffer' + endif + + call merginal#openFileDecidedWindow(b:merginal_repo,l:diffFile.fileFullPath) + endif +endfunction + +"Exactly what it says on tin +function! s:openDiffFileUnderCursorAndDiff(diffType) + if a:diffType!='s' && a:diffType!='v' + throw 'Bad diff type' + endif + if 'Merginal:Diff'==bufname('') + let l:diffFile=merginal#diffFileDetails('.') + + if l:diffFile.isAdded + throw 'File does not exist in other buffer' + endif + + let l:repo=b:merginal_repo + let l:diffBranch=b:merginal_diffBranch + + "Close currently open git diffs + let l:currentWindowBuffer=winbufnr('.') + try + windo if 'blob'==get(b:,'fugitive_type','') && exists('w:fugitive_diff_restore') + \| bdelete + \| endif + catch + "do nothing + finally + execute bufwinnr(l:currentWindowBuffer).'wincmd w' + endtry + + call merginal#openFileDecidedWindow(l:repo,l:diffFile.fileFullPath) + + execute ':G'.a:diffType.'diff '.fnameescape(l:diffBranch.handle) + endif +endfunction + +"Checks out the file from the other branch to the current branch +function! s:checkoutDiffFileUnderCursor() + if 'Merginal:Diff'==bufname('') + let l:diffFile=merginal#diffFileDetails('.') + + if l:diffFile.isAdded + throw 'File does not exist in diffed buffer' + endif + + let l:answer=1 + if !empty(glob(l:diffFile.fileFullPath)) + let l:answer='yes'==input('Override `'.l:diffFile.fileInTree.'`?(type "yes" to confirm) ') + endif + if l:answer + echo merginal#runGitCommandInTreeReturnResult(b:merginal_repo,'--no-pager checkout '.shellescape(b:merginal_diffBranch.handle) + \.' -- '.shellescape(l:diffFile.fileFullPath)) + call merginal#reloadBuffers() + call merginal#tryRefreshDiffFilesBuffer() + else + echo ' ' + echo 'File checkout canceled by the user' + endif + endif +endfunction + + +"Open the rebase conflicts buffer for resolving rebase conflicts +function! merginal#openRebaseConflictsBuffer(...) + let l:currentFile=expand('%:~:.') + if merginal#openTuiBuffer('Merginal:Rebase',get(a:000,1,bufwinnr('Merginal:'))) + doautocmd User Merginal_RebaseConflicts + endif + + "At any rate, refresh the buffer: + call merginal#tryRefreshRebaseConflictsBuffer(l:currentFile) +endfunction + +augroup merginal + autocmd User Merginal_RebaseConflicts nnoremap <buffer> R :call merginal#tryRefreshRebaseConflictsBuffer(0)<Cr> + autocmd User Merginal_RebaseConflicts nnoremap <buffer> <Cr> :call <SID>openMergeConflictUnderCursor()<Cr> + autocmd User Merginal_RebaseConflicts nnoremap <buffer> A :call <SID>addConflictedFileToStagingArea()<Cr> + autocmd User Merginal_RebaseConflicts nnoremap <buffer> aa :call <SID>addConflictedFileToStagingArea()<Cr> + autocmd User Merginal_RebaseConflicts nnoremap <buffer> ra :call <SID>rebaseAction('abort')<Cr> + autocmd User Merginal_RebaseConflicts nnoremap <buffer> rs :call <SID>rebaseAction('skip')<Cr> + autocmd User Merginal_RebaseConflicts nnoremap <buffer> rc :call <SID>rebaseAction('continue')<Cr> +augroup END + +"Returns 1 if all rebase conflicts are done +function! merginal#tryRefreshRebaseConflictsBuffer(fileToJumpTo) + if 'Merginal:Rebase'==bufname('') + let l:currentCommitMessageLines=readfile(b:merginal_repo.dir('rebase-apply','msg-clean')) + call insert(l:currentCommitMessageLines,'=== Reapplying: ===') + call add(l:currentCommitMessageLines,'===================') + call add(l:currentCommitMessageLines,'') + return s:refreshConflictsBuffer(a:fileToJumpTo,l:currentCommitMessageLines) + endif + return 0 +endfunction + +"Run various rebase actions +function! s:rebaseAction(remoteAction) + if 'Merginal:Rebase'==bufname('') + \|| 'Merginal:RebaseAmend'==bufname('') + echo merginal#runGitCommandInTreeReturnResult(b:merginal_repo,'--no-pager rebase --'.a:remoteAction) + call merginal#reloadBuffers() + if merginal#isRebaseMode() + call merginal#tryRefreshRebaseConflictsBuffer(0) + elseif merginal#isRebaseAmendMode() + call merginal#tryRefreshRebaseAmendBuffer() + else + "If we finished rebasing - close the rebase conflicts buffer + wincmd q + endif + endif +endfunction + + + +"Open the rebase amend buffer +function! merginal#openRebaseAmendBuffer(...) + let l:currentFile=expand('%:~:.') + if merginal#openTuiBuffer('Merginal:RebaseAmend',get(a:000,1,bufwinnr('Merginal:'))) + doautocmd User Merginal_RebaseAmend + endif + + "At any rate, refresh the buffer: + call merginal#tryRefreshRebaseAmendBuffer() +endfunction + +autocmd User Merginal_RebaseAmend nnoremap <buffer> R :call merginal#tryRefreshRebaseAmendBuffer()<Cr> +autocmd User Merginal_RebaseAmend nnoremap <buffer> ra :call <SID>rebaseAction('abort')<Cr> +autocmd User Merginal_RebaseAmend nnoremap <buffer> rs :call <SID>rebaseAction('skip')<Cr> +autocmd User Merginal_RebaseAmend nnoremap <buffer> rc :call <SID>rebaseAction('continue')<Cr> +autocmd User Merginal_RebaseAmend nnoremap <buffer> gd :call <SID>diffWithBranchUnderCursor()<Cr> + +function! merginal#tryRefreshRebaseAmendBuffer() + if 'Merginal:RebaseAmend'==bufname('') + "let l:gitStatusOutput=split(merginal#system(b:merginal_repo.git_command('status','--all')),'\r\n\|\n\|\r') + let l:currentLine=line('.') + let l:newBufferLines=[] + let l:amendedCommit=readfile(b:merginal_repo.dir('rebase-merge','amend')) + let l:amendedCommitShort=merginal#system(b:merginal_repo.git_command('rev-parse','--short',l:amendedCommit[0])) + let l:amendedCommitShort=substitute(l:amendedCommitShort,'\v[\r\n]','','g') + let l:amendedCommitMessage=readfile(b:merginal_repo.dir('rebase-merge','message')) + call add(l:newBufferLines,'=== Amending '.l:amendedCommitShort.' ===') + let l:newBufferLines+=l:amendedCommitMessage + call add(l:newBufferLines,repeat('=',len(l:newBufferLines[0]))) + call add(l:newBufferLines,'') + + let b:headerLinesCount=len(l:newBufferLines)+1 + + let l:branchList=split(merginal#system(b:merginal_repo.git_command('branch','--all')),'\r\n\|\n\|\r') + "The first line is a reminder that we are rebasing + "call remove(l:branchList,0) + let l:newBufferLines+=l:branchList + + + setlocal modifiable + "Clear the buffer: + normal ggdG + "Write the new buffer lines: + call setline(1,l:newBufferLines) + "call setline(1,l:branchList) + setlocal nomodifiable + endif + return 0 +endfunction diff --git a/vimfiles/doc/merginal.txt b/vimfiles/doc/merginal.txt new file mode 100755 index 0000000..124d252 --- /dev/null +++ b/vimfiles/doc/merginal.txt @@ -0,0 +1,142 @@ +*merginal.txt* + + +Author: Idan Arye <https://github.com/idanarye/> +License: Same terms as Vim itself (see |license|) + +Version: 1.4.0 + +INTRODUCTION *merginal* + +Merginal aims provide a nice inteface for dealing with Git branches. It +offers interactive TUI for: + +* Viewing the list of branches +* Checking out branches from that list +* Creating new branches +* Deleting branches +* Merging branches +* Rebasing branches +* Solving merge conflicts +* Renaming branches + + +REQUIREMENTS *merginal-requirements* + +Merginal is based on Fugitive, so it requires Fugitive. If you don't have it +already you can get it from https://github.com/tpope/vim-fugitive + +It should go without saying that you need Git. + +Under Windows, vimproc is an optional requirement. Merginal will work without +it, but it'll pop an ugly console window every time it needs to run a Git +command. You can get vimproc from https://github.com/Shougo/vimproc.vim + + +USAGE *merginal-usage* + +To use Merginal you need to know but one command: *:Merginal*. It'll open the +|merginal-branch-list| buffer, unless the repository is in merge mode then +it'll open the |merginal-merge-conflicts| buffer. + +Like Fugitive's commands, |:Merginal| is native to the buffer, and will only +work in buffers that are parts of Git repositories. + +You can also toggle the buffer with |:MerginalToggle| or close it with +|:MerginalClose|. + + +THE BRANCH LIST *merginal-branch-list* + +The branch list shows a list of branches. While in that list, you can use the +following keymaps to interact with the branches: + +R Refresh the buffer list. +cc Checkout the branch under the cursor. +ct Track the remote branch under the cursor. +cT Track the remote branch under the cursor, prompting for a name. +C Same as cc. +aa Create a new branch from the currently checked out branch. You'll be + prompted to enter a name for the new branch. +A Same as aa. +dd Delete the branch under the cursor. +D Same as dd. +mm Merge the branch under the cursor into the currently checked out + branch. If there are merge conflicts, the |merginal-merge-conflicts| + buffer will open in place of the branch list buffer. +M Same as mm. +mf Merge the branch under the cursor into the currently checked out branch + using Fugitive's |:Gmerge| command. +rb Rebase the currently checked out branch against the branch under the + cursor. If there are rebase conflicts, the |merginal-rebase-conflicts| + buffer will open in place of + the branch list buffer. +ps Prompt to choose a remote to push the branch under the cursor. +pS Prompt to choose a remote to force push the branch under the cursor. +pl Prompt to choose a remote to pull the branch under the cursor. +pf Prompt to choose a remote to fetch the branch under the cursor. +gd Open |merginal-diff-files| buffer to diff against the branch under the + cursor. +rn Prompt to rename the branch under the cursor. + + +MERGE CONFLICTS *merginal-merge-conflicts* + +The merge conflicts buffer is used to solve merge conflicts. It shows all the +files that have merge conflicts and offers the following keymaps: + +R Refresh the merge conflicts list. +<Cr> Open the conflicted file under the cursor. +aa Add the conflicted file under the cursor to the staging area. If that + was the last conflicted file, the merge conflicts buffer will close and + |fugitive-:Gstatus| will open. +A Same as aa. + + +REBASE CONFLICTS *merginal-rebase-conflicts* + +The rebase conflicts buffer is used to solve rebase conflicts. It shows the +currently applied commit message and all the files that have rebase conflicts, +and offers the following keymaps: + +R Refresh the rebase conflicts list. +<Cr> Open the conflicted file under the cursor. +aa Add the conflicted file under the cursor to the staging area. If that + was the last conflicted file, prompt the user to continue to the next + patch. +A Same as aa. +ra Abort the rebase +rc Continue to the next patch. +rs Skip the current patch + + +REBASE AMEND *merginal-rebase-amend* + +The rebase amend buffer is shown when you amend a patch during a rebase. It +shows the amended commit's shortened hash and commit message. Additionally, it +shows all the branches so you can diff against them while the patch. If offers +the folloing keymaps: + +R Refresh the rebase amend buffer. +gd Open |merginal-diff-files| buffer to diff against the branch under the + cursor. +ra Abort the rebase +rc Continue to the next patch. +rs Skip the current patch + + +DIFF FILES *merginal-diff-files* + +The diff files buffer is used to diff against another branch. It displays all +the differences between the currently checked out branch and the branch it was +opened against, and offerts the following keymaps: + +R Refresh the diff files list. +<Cr> Open the file under the cursor(if it exists in the currently checked + out branch). +ds Split-diff against the file under the cursor(if it exists in the other + branch) +ds VSplit-diff against the file under the cursor(if it exists in the other + branch) +co Check out the file under the cursor(if it exists in the other branch) + into the current branch. diff --git a/vimfiles/doc/tags b/vimfiles/doc/tags index 90177b7..6b7fb68 100644 --- a/vimfiles/doc/tags +++ b/vimfiles/doc/tags @@ -1940,6 +1940,15 @@ matchit-troubleshoot matchit.txt /*matchit-troubleshoot* matchit-v_% matchit.txt /*matchit-v_%* matchit.txt matchit.txt /*matchit.txt* matchit.vim matchit.txt /*matchit.vim* +merginal merginal.txt /*merginal* +merginal-branch-list merginal.txt /*merginal-branch-list* +merginal-diff-files merginal.txt /*merginal-diff-files* +merginal-merge-conflicts merginal.txt /*merginal-merge-conflicts* +merginal-rebase-amend merginal.txt /*merginal-rebase-amend* +merginal-rebase-conflicts merginal.txt /*merginal-rebase-conflicts* +merginal-requirements merginal.txt /*merginal-requirements* +merginal-usage merginal.txt /*merginal-usage* +merginal.txt merginal.txt /*merginal.txt* n_<Plug>TComment-Comment tcomment.txt /*n_<Plug>TComment-Comment* n_<Plug>TComment-Commentb tcomment.txt /*n_<Plug>TComment-Commentb* n_<Plug>TComment-Commentc tcomment.txt /*n_<Plug>TComment-Commentc* diff --git a/vimfiles/plugin/merginal.vim b/vimfiles/plugin/merginal.vim new file mode 100755 index 0000000..1cda509 --- /dev/null +++ b/vimfiles/plugin/merginal.vim @@ -0,0 +1,51 @@ +function! s:openBasedOnMergeMode() abort + if merginal#isRebaseMode() + call merginal#openRebaseConflictsBuffer() + elseif merginal#isRebaseAmendMode() + call merginal#openRebaseAmendBuffer() + elseif merginal#isMergeMode() + call merginal#openMergeConflictsBuffer() + else + call merginal#openBranchListBuffer() + endif +endfunction + +function! s:toggleBasedOnMergeMode() abort + let l:repo=fugitive#repo() + let l:merginalWindowNumber=bufwinnr('Merginal:') + if 0<=l:merginalWindowNumber + let l:merginalBufferNumber=winbufnr(l:merginalWindowNumber) + let l:merginalBufferName=bufname(l:merginalBufferNumber) + + "If we are not on the same dir we need to reload the merginal buffer + "anyways: + if getbufvar(l:merginalBufferNumber,'merginal_repo').dir()==l:repo.dir() + if merginal#isRebaseMode() + if 'Merginal:Rebase'==l:merginalBufferName + call merginal#closeMerginalBuffer() + return + endif + elseif merginal#isRebaseAmendMode() + if 'Merginal:RebaseAmend'==l:merginalBufferName + call merginal#closeMerginalBuffer() + return + endif + elseif merginal#isMergeMode() + if 'Merginal:Conflicts'==l:merginalBufferName + call merginal#closeMerginalBuffer() + return + endif + else + if 'Merginal:Branches'==l:merginalBufferName + call merginal#closeMerginalBuffer() + return + endif + end + endif + endif + call s:openBasedOnMergeMode() +endfunction + +autocmd User Fugitive command! -buffer -nargs=0 Merginal call s:openBasedOnMergeMode() +autocmd User Fugitive command! -buffer -nargs=0 MerginalToggle call s:toggleBasedOnMergeMode() +autocmd User Fugitive command! -buffer -nargs=0 MerginalClose call merginal#closeMerginalBuffer()