From 92abcce1ffa1aec3bbb1451fa2d3f2dae3978603 Mon Sep 17 00:00:00 2001 From: Andy Stewart Date: Wed, 8 Jan 2014 17:07:06 +0100 Subject: [PATCH] Stage and revert individual hunks. --- README.mkd | 14 +++++++- autoload/diff.vim | 49 ++++++++++++++++++++++------ doc/gitgutter.txt | 8 +++++ plugin/gitgutter.vim | 77 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 136 insertions(+), 12 deletions(-) diff --git a/README.mkd b/README.mkd index ab49532..113280d 100644 --- a/README.mkd +++ b/README.mkd @@ -1,6 +1,6 @@ ## Vim Git Gutter -A Vim plugin which shows a git diff in the 'gutter' (sign column). It shows whether each line has been added, modified, and where lines have been removed. +A Vim plugin which shows a git diff in the 'gutter' (sign column). It shows whether each line has been added, modified, and where lines have been removed. You can also stage and revert individual hunks. ### Screenshot @@ -65,6 +65,18 @@ nmap gh GitGutterNextHunk nmap gH GitGutterPrevHunk ``` +When your cursor is in a hunk, you can: + +* stage the hunk with `hs` or +* revert it with `hr`. + +To set your own mappings for these, for example if you prefer the mnemonics hunk-add and hunk-undo: + +```viml +nmap ha GitGutterStageHunk +nmap hu GitGutterRevertHunk +``` + Finally, you can force vim-gitgutter to update its signs across all visible buffers with `:GitGutterAll`. See the customisation section below for how to change the defaults. diff --git a/autoload/diff.vim b/autoload/diff.vim index ca316cf..f5fbbe1 100644 --- a/autoload/diff.vim +++ b/autoload/diff.vim @@ -1,8 +1,9 @@ let s:grep_available = executable('grep') let s:grep_command = ' | ' . (g:gitgutter_escape_grep ? '\grep' : 'grep') . ' -e "^@@ "' +let s:hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@' -function! diff#run_diff(realtime) +function! diff#run_diff(realtime, use_external_grep) if a:realtime let blob_name = ':./' . fnamemodify(utility#file(),':t') let blob_file = tempname() @@ -10,7 +11,7 @@ function! diff#run_diff(realtime) else let cmd = 'git diff --no-ext-diff --no-color -U0 ' . g:gitgutter_diff_args . ' ' . shellescape(utility#file()) endif - if s:grep_available + if a:use_external_grep && s:grep_available let cmd .= s:grep_command endif let cmd = utility#escape(cmd) @@ -31,21 +32,29 @@ function! diff#run_diff(realtime) endfunction function! diff#parse_diff(diff) - let hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@' let hunks = [] for line in split(a:diff, '\n') - let matches = matchlist(line, hunk_re) - if len(matches) > 0 - let from_line = str2nr(matches[1]) - let from_count = (matches[2] == '') ? 1 : str2nr(matches[2]) - let to_line = str2nr(matches[3]) - let to_count = (matches[4] == '') ? 1 : str2nr(matches[4]) - call add(hunks, [from_line, from_count, to_line, to_count]) + let hunk_info = diff#parse_hunk(line) + if len(hunk_info) == 4 + call add(hunks, hunk_info) endif endfor return hunks endfunction +function! diff#parse_hunk(line) + let matches = matchlist(a:line, s:hunk_re) + if len(matches) > 0 + let from_line = str2nr(matches[1]) + let from_count = (matches[2] == '') ? 1 : str2nr(matches[2]) + let to_line = str2nr(matches[3]) + let to_count = (matches[4] == '') ? 1 : str2nr(matches[4]) + return [from_line, from_count, to_line, to_count] + else + return [] + end +endfunction + function! diff#process_hunks(hunks) call hunk#reset() let modified_lines = [] @@ -153,3 +162,23 @@ function! diff#process_modified_and_removed(modifications, from_count, to_count, endwhile let a:modifications[-1] = [a:to_line + offset - 1, 'modified_removed'] endfunction + +function! diff#generate_diff_for_hunk(hunk) + return diff#discard_hunks(diff#run_diff(0, 0), a:hunk) +endfunction + +function! diff#discard_hunks(diff, hunk_to_keep) + let modified_diff = [] + let keep_line = 1 " start by keeping header + for line in split(a:diff, '\n') + let hunk_info = diff#parse_hunk(line) + if len(hunk_info) == 4 " start of new hunk + let keep_line = (hunk_info == a:hunk_to_keep) + endif + if keep_line + call add(modified_diff, line) + endif + endfor + " call append('$', modified_diff) + return join(modified_diff, "\n") . "\n" +endfunction diff --git a/doc/gitgutter.txt b/doc/gitgutter.txt index 16e2c87..22cab4c 100644 --- a/doc/gitgutter.txt +++ b/doc/gitgutter.txt @@ -86,6 +86,14 @@ Commands for jumping between marked hunks: :GitGutterPrevHunk *:GitGutterPrevHunk* Jump to the previous marked hunk. Takes a count. +Commands for staging or reverting individual hunks: + + :GitGutterStageHunk *:GitGutterStageHunk* + Stage the hunk the cursor is in. + + :GitGutterRevertHunk *:GitGutterRevertHunk* + Revert the hunk the cursor is in. + =============================================================================== 5. CUSTOMISATION *GitGutterCustomisation* diff --git a/plugin/gitgutter.vim b/plugin/gitgutter.vim index 6887e15..d242f39 100644 --- a/plugin/gitgutter.vim +++ b/plugin/gitgutter.vim @@ -55,7 +55,7 @@ function! GitGutter(file, realtime) call utility#set_file(a:file) if utility#is_active() if !a:realtime || utility#has_fresh_changes(a:file) - let diff = diff#run_diff(a:realtime || utility#has_unsaved_changes(a:file)) + let diff = diff#run_diff(a:realtime || utility#has_unsaved_changes(a:file), 1) let s:hunks = diff#parse_diff(diff) let modified_lines = diff#process_hunks(s:hunks) if g:gitgutter_sign_column_always @@ -154,6 +154,68 @@ function! GitGutterPrevHunk(count) endfunction command -count=1 GitGutterPrevHunk call GitGutterPrevHunk() +function! GitGutterStageHunk() + if utility#is_active() + " Ensure the working copy of the file is up to date. + " It doesn't make sense to stage a hunk otherwise. + silent write + + " find current hunk (i.e. which one the cursor is in) + let current_hunk = [] + let current_line = line('.') + for hunk in s:hunks + if current_line >= hunk[2] && current_line < hunk[2] + (hunk[3] == 0 ? 1 : hunk[3]) + let current_hunk = hunk + break + endif + endfor + if len(current_hunk) != 4 + return + endif + + " construct a diff + let diff_for_hunk = diff#generate_diff_for_hunk(current_hunk) + + " apply the diff + call system(utility#command_in_directory_of_file('git apply --cached --unidiff-zero - '), diff_for_hunk) + + " refresh gitgutter's view of buffer + silent execute "GitGutter" + endif +endfunction +command GitGutterStageHunk call GitGutterStageHunk() + +function! GitGutterRevertHunk() + if utility#is_active() + " Ensure the working copy of the file is up to date. + " It doesn't make sense to stage a hunk otherwise. + write + + " find current hunk (i.e. which one the cursor is in) + let current_hunk = [] + let current_line = line('.') + for hunk in s:hunks + if current_line >= hunk[2] && current_line < hunk[2] + (hunk[3] == 0 ? 1 : hunk[3]) + let current_hunk = hunk + break + endif + endfor + if len(current_hunk) != 4 + return + endif + + " construct a diff + let diff_for_hunk = diff#generate_diff_for_hunk(current_hunk) + + " apply the diff + call system(utility#command_in_directory_of_file('git apply --reverse --unidiff-zero - '), diff_for_hunk) + + " reload file + silent edit + endif +endfunction +command GitGutterRevertHunk call GitGutterRevertHunk() + " Returns the git-diff hunks for the file or an empty list if there " aren't any hunks. " @@ -182,6 +244,7 @@ function! GitGutterGetHunkSummary() return hunk#summary() endfunction + nnoremap GitGutterNextHunk :execute v:count1 . "GitGutterNextHunk" nnoremap GitGutterPrevHunk :execute v:count1 . "GitGutterPrevHunk" @@ -190,6 +253,18 @@ if !hasmapto('GitGutterNextHunk') && maparg(']h', 'n') ==# '' nmap [h GitGutterPrevHunk endif + +nnoremap GitGutterStageHunk :GitGutterStageHunk +nnoremap GitGutterRevertHunk :GitGutterRevertHunk + +if !hasmapto('GitGutterStageHunk') && maparg('ha', 'n') ==# '' + nmap ha GitGutterStageHunk +endif +if !hasmapto('GitGutterRevertHunk') && maparg('hr', 'n') ==# '' + nmap hr GitGutterRevertHunk +endif + + augroup gitgutter autocmd!