diff --git a/README.md b/README.md index 4382746..0ed444e 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,13 @@ Take a look at [TL;DR](#tldr) to start using it immediatly. * [ ] Chase all corner cases. Please remember that vimagit is at an early development stage. If you try vimagit and nothing is working, please don't throw it, fill an [issue](https://github.com/jreybert/vimagit/issues/new) on github :heart: ! More to come: -* Vizualize and checkout branches. +* Make vimagit more efficient for huge repositories, with a lot of diffs. +* Add a push function, taking care if needed about the remote repository and branch. +* Handle commit fixup! and squash!, with a smart git log popup. +* Handle multiple git repositories within one vim session. +* Stage multiple hunks or file by visually selecting them. * Go through history, cherry-pick changes. +* Vizualize and checkout branches. * Something is missing? Open an [issue](https://github.com/jreybert/vimagit/issues/new)! > Why should I use vimagit, there are already plethora git plugins for vim? @@ -92,6 +97,18 @@ There are 5 sections: * only lines starting with a + sign can be modified * no line can be deleted +### Visual selection + +It is possible to stage part of hunk, by different ways: +* By visually selecting some lines, then staging the selection with **S**. +* By marking some lines "to be staged" with **M**, then staging these selected lines with **S**. +* Staging individual lines with **L**. + +Visual selection and marked lines have some limitations for the moment: +* It only work for "staging", not for "unstaging". +* Selection/marks must be within a single hunk. +* Marks not within the hunk currently staged are lost during stage process magit buffer refresh. + ### Commands #### magit#show_magit() @@ -104,7 +121,7 @@ It takes 3 parameters: - 'h', curent window is split horizontally, and magit is displayed in new buffer - 'c', magit is displayed in current buffer - * show_all_files: define is file diffs are shown by default for this session + * show_all_files: define how file diffs are shown by default for this session (see [g:magit_default_show_all_files](#gmagit_default_show_all_files)) * foldlevel: set default magit buffer foldlevel for this session (see [g:magit_default_fold_level](#gmagit_default_fold_level)) @@ -120,23 +137,23 @@ For each mapping, user can redefine the behavior with its own mapping. Each vari Following mappings are broadly set, and are applied in all vim buffers. -**\M** +##### \M Open Magit buffer #### Local mappings Following mappings are set locally, for magit buffer only, in normal mode. -**Enter**,**\** +##### Enter,\ * All files are folded by default. To see the changes in a file, move cursor to the filename line, and press Enter. You can close the changes display retyping Enter. -**zo,zO** +##### zo,zO * Typing zo on a file will unhide its diffs. -**zc,zC** +##### zc,zC * Typing zc on a file will hide its diffs. -**S** +##### S * If cursor is in a hunk, stage/unstage hunk at cursor position. * If cursor is in diff header, stage/unstage whole file at cursor position. * If some lines in the hunk are selected (using **v**), stage only visual selected lines (only works for staging). @@ -144,48 +161,48 @@ Following mappings are set locally, for magit buffer only, in normal mode. * When cursor is in "Unstaged changes" section, it will stage the hunk/file. * On the other side, when cursor is in "Staged changes" section, it will unstage hunk/file. -**F** +##### F * Stage/unstage the whole file at cursor position. * When cursor is in "Unstaged changes" section, it will stage the file. * On the other side, when cursor is in "Staged changes" section, it will unstage file. -**L** +##### L * Stage the line under the cursor. -**M** +##### M * Mark the line under the cursor "to be staged". * If some lines in the hunk are selected (using **v**), mark selected lines "to be staged". * To staged marked lines in a hunk, move cursor to this hunk and press **S**. -**DDD** +##### DDD * If cursor is in a hunk, discard hunk at cursor position. * If cursor is in diff header, discard whole file at cursor position. * Only works in "Unstaged changes" section. -**CC** +##### CC * If not in commit section, set commit mode to "New commit" and show "Commit message" section with brand new commit message. * If in commit section, commit the all staged changes in commit mode previously set. -**:w** +##### :w * If in commit section, commit the all staged changes in commit mode previously set. -**CA** +##### CA * If not in commit section, set commit mode to "Amend commit" and show "Commit message" section with previous commit message. * If in commit section, commit the staged changes in commit mode previously set. -**CF** +##### CF * Amend the staged changes into the previous commit, without modifying previous commit message. -**I** +##### I * Add the file under the cursor in .gitgnore -**R** +##### R * Refresh magit buffer -**q** +##### q * Close the magit buffer -**h** +##### h * Toggle help showing in magit buffer ### Options @@ -206,10 +223,11 @@ To disable chatty inline help in magit buffer (default 1) #### g:magit_default_show_all_files When this variable is set to 0, all diff files are hidden by default. -When this variable is set to 1, all diff files are shown by default. -Default value is 0. +When this variable is set to 1, all diff for modified files are shown by default. +When this variable is set to 2, all diff for all files are shown by default. +Default value is 1. NB: for repository with large number of differences, display may be slow. -> let g:magit_default_show_all_files=[01] +> let g:magit_default_show_all_files=[012] #### g:magit_default_fold_level @@ -220,6 +238,13 @@ When set to 2, filenames and hunks are unfolded. Default value is 1. > let g:magit_default_fold_level=[012] +#### g:magit_default_sections + +With this variable, the user is able to choose which sections are displayed in magit +buffer, and in which order. +Default value: +> let g:magit_default_sections = ['info', 'global_help', 'commit', 'staged', 'unstaged'] + #### g:magit_warning_max_lines This variable is the maximum number of diff lines that vimagit will display diff --git a/autoload/magit/git.vim b/autoload/magit/git.vim index 66b0181..0f54775 100644 --- a/autoload/magit/git.vim +++ b/autoload/magit/git.vim @@ -54,6 +54,47 @@ function! magit#git#git_dir() return s:magit_git_dir endfunction +" magit#git#git_diff: helper function to get diff of a file +" nota: when git fail (due to misformated patch for example), an error +" message is raised. +" param[in] filemane: it must be quoted if it contains spaces +" param[in] status: status of the file (see g:magit_git_status_code) +" param[in] mode: can be staged or unstaged +function! magit#git#git_diff(filename, status, mode) + let dev_null = ( a:status == '?' ) ? " /dev/null " : " " + let staged_flag = ( a:mode == 'staged' ) ? " --staged " : " " + let git_cmd="git diff --no-ext-diff " . staged_flag . + \ "--no-color --patch -- " . dev_null . " " + \ .a:filename + silent let diff_list=magit#utils#systemlist(git_cmd) + if ( empty(diff_list) ) + echohl WarningMsg + echom "diff command \"" . diff_cmd . "\" returned nothing" + echohl None + throw 'diff error' + endif + return diff_list +endfunction + +" magit#git#git_sub_summary: helper function to get diff of a submodule +" nota: when git fail (due to misformated patch for example), an error +" message is raised. +" param[in] filemane: it must be quoted if it contains spaces +" param[in] mode: can be staged or unstaged +function! magit#git#git_sub_summary(filename, mode) + let staged_flag = ( a:mode == 'staged' ) ? " --cached " : " --files " + let git_cmd="git submodule summary " . staged_flag . " HEAD " + \ .a:filename + silent let diff_list=magit#utils#systemlist(git_cmd) + if ( empty(diff_list) ) + echohl WarningMsg + echom "diff command \"" . diff_cmd . "\" returned nothing" + echohl None + throw 'diff error' + endif + return diff_list +endfunction + " magit#git#git_add: helper function to add a whole file " nota: when git fail (due to misformated patch for example), an error " message is raised. diff --git a/autoload/magit/state.vim b/autoload/magit/state.vim index 032416e..1be452b 100644 --- a/autoload/magit/state.vim +++ b/autoload/magit/state.vim @@ -16,6 +16,18 @@ function! magit#state#toggle_file_visible() dict let self.visible = ( self.visible == 0 ) ? 1 : 0 endfunction +" magit#state#init_file_visible: init file visible status, among several conditions +function! magit#state#init_file_visible() dict + if ( !self.new ) + return self.is_visible() + else + if ( self.status == 'M' || b:magit_default_show_all_files > 1 ) + call self.set_visible(b:magit_default_show_all_files) + endif + return self.is_visible() + endif +endfunction + " magit#state#is_file_dir: file getter function " return 1 if current file is a directory, 0 otherwise function! magit#state#is_file_dir() dict @@ -63,6 +75,7 @@ let s:hunk_template = { " s:diff_template: template for diff object (nested in s:file_template) " WARNING: this variable must be deepcopy()'ied let s:diff_template = { +\ 'len': 0, \ 'header': [], \ 'hunks': [s:hunk_template], \} @@ -70,7 +83,9 @@ let s:diff_template = { " s:file_template: template for file object " WARNING: this variable must be deepcopy()'ied let s:file_template = { +\ 'new': 1, \ 'exists': 0, +\ 'visible': 0, \ 'filename': '', \ 'status': '', \ 'empty': 0, @@ -82,6 +97,7 @@ let s:file_template = { \ 'is_dir': function("magit#state#is_file_dir"), \ 'is_visible': function("magit#state#is_file_visible"), \ 'set_visible': function("magit#state#set_file_visible"), +\ 'init_visible': function("magit#state#init_file_visible"), \ 'toggle_visible': function("magit#state#toggle_file_visible"), \ 'must_be_added': function("magit#state#must_be_added"), \ 'get_header': function("magit#state#file_get_header"), @@ -101,7 +117,6 @@ function! magit#state#get_file(mode, filename, ...) dict let create = ( a:0 == 1 ) ? a:1 : 0 if ( file_exists == 0 && create == 1 ) let self.dict[a:mode][a:filename] = deepcopy(s:file_template) - let self.dict[a:mode][a:filename].visible = b:magit_default_show_all_files let self.dict[a:mode][a:filename].filename = a:filename elseif ( file_exists == 0 && create == 0 ) throw 'file_doesnt_exists' @@ -125,27 +140,38 @@ function! magit#state#file_get_filename_header() dict endif endfunction +function! magit#state#check_max_lines(file) dict + let total_lines = self.nb_diff_lines + a:file.diff.len + if ( total_lines > g:magit_warning_max_lines && b:magit_warning_max_lines_answered == 0 ) + echohl WarningMsg + let ret = input("There are " . total_lines . " diff lines to display. Do you want to display all diffs? y(es) / N(o) : ", "") + echohl None + let b:magit_warning_max_lines_answered = 1 + if ( ret !~? '^y\%(e\%(s\)\?\)\?$' ) + call a:file.set_visible(0) + let a:file.diff.len = 0 + let b:magit_default_show_all_files = 0 + return 1 + endif + endif + return 0 +endfunction + " magit#state#add_file: method to add a file with all its " properties (filename, exists, status, header and hunks) " param[in] mode: can be staged or unstaged " param[in] status: one character status code of the file (AMDRCU?) " param[in] filename: filename function! magit#state#add_file(mode, status, filename, depth) dict - let dev_null = ( a:status == '?' ) ? " /dev/null " : " " - let staged_flag = ( a:mode == 'staged' ) ? " --staged " : " " - let diff_cmd="git diff --no-ext-diff " . staged_flag . - \ "--no-color --patch -- " . dev_null . " " - \ . magit#utils#add_quotes(a:filename) - let diff_list=magit#utils#systemlist(diff_cmd) - if ( empty(diff_list) ) - echoerr "diff command \"" . diff_cmd . "\" returned nothing" - endif let file = self.get_file(a:mode, a:filename, 1) let file.exists = 1 let file.status = a:status let file.depth = a:depth + " discard previous diff + let file.diff = deepcopy(s:diff_template) + if ( a:status == '?' && getftype(a:filename) == 'link' ) let file.status = 'L' let file.symlink = resolve(a:filename) @@ -153,14 +179,26 @@ function! magit#state#add_file(mode, status, filename, depth) dict elseif ( magit#utils#is_submodule(a:filename)) let file.status = 'S' let file.submodule = 1 + if ( !file.is_visible() ) + return + endif + let diff_list=magit#git#git_sub_summary(magit#utils#add_quotes(a:filename), + \ a:mode) + let file.diff.len = len(diff_list) + + if ( self.check_max_lines(file) != 0 ) + return + endif + let file.diff.hunks[0].header = '' let file.diff.hunks[0].lines = diff_list - if ( file.is_visible() ) - let self.nb_diff_lines += len(diff_list) - endif + let self.nb_diff_lines += file.diff.len elseif ( a:status == '?' && isdirectory(a:filename) == 1 ) let file.status = 'N' let file.dir = 1 + if ( !file.is_visible() ) + return + endif for subfile in magit#utils#ls_all(a:filename) call self.add_file(a:mode, a:status, subfile, a:depth + 1) endfor @@ -172,14 +210,25 @@ function! magit#state#add_file(mode, status, filename, depth) dict let file.binary = 1 let file.diff.hunks[0].header = 'Binary file' else + if ( !file.init_visible() ) + return + endif let line = 0 " match( - while ( line < len(diff_list) && diff_list[line] !~ "^@.*" ) + let diff_list=magit#git#git_diff(magit#utils#add_quotes(a:filename), + \ a:status, a:mode) + let file.diff.len = len(diff_list) + + if ( self.check_max_lines(file) != 0 ) + return + endif + + while ( line < file.diff.len && diff_list[line] !~ "^@.*" ) call add(file.diff.header, diff_list[line]) let line += 1 endwhile - if ( line < len(diff_list) ) + if ( line < file.diff.len ) let hunk = file.diff.hunks[0] let hunk.header = diff_list[line] @@ -193,10 +242,9 @@ function! magit#state#add_file(mode, status, filename, depth) dict call add(hunk.lines, diff_line) endfor endif - if ( file.is_visible() ) - let self.nb_diff_lines += len(diff_list) - endif + let self.nb_diff_lines += file.diff.len endif + endfunction " magit#state#update: update self.dict @@ -209,6 +257,7 @@ function! magit#state#update() dict for diff_dict_mode in values(self.dict) for file in values(diff_dict_mode) let file.exists = 0 + let file.new = 0 " always discard previous diff let file.diff = deepcopy(s:diff_template) endfor @@ -283,6 +332,7 @@ let magit#state#state = { \ 'get_files': function("magit#state#get_files"), \ 'add_file': function("magit#state#add_file"), \ 'set_files_visible': function("magit#state#set_files_visible"), + \ 'check_max_lines': function("magit#state#check_max_lines"), \ 'update': function("magit#state#update"), \ 'dict': { 'staged': {}, 'unstaged': {}}, \ } diff --git a/doc/vimagit.txt b/doc/vimagit.txt index ac02191..2ee997b 100644 --- a/doc/vimagit.txt +++ b/doc/vimagit.txt @@ -109,6 +109,19 @@ INLINE MODIFICATIONS *vimagit-inline-modification* * only lines starting with a + sign can be modified * no line can be deleted +VISUAL SELECTION *vimagit-visual-selection* + +It is possible to stage part of hunk, by different ways: +* By visually selecting some lines, then staging the selection with S. +* By marking some lines "to be staged" with M, then staging these selected + lines with S. +* Staging individual lines with L. + +Visual selection and marked lines have some limitations for the moment: +* It only work for "staging", not for "unstaging". +* Selection/marks must be within a single hunk. +* Marks not within the hunk currently staged are lost during stage process magit + buffer refresh. COMMANDS *vimagit-commands* @@ -121,12 +134,12 @@ It takes 3 parameters: - 'h', curent window is split horizontally, and magit is displayed in new buffer - 'c', magit is displayed in current buffer - * show_all_files: define is file diffs are shown by default for this session + * show_all_files: define how file diffs are shown by default for this session (see |vimagit-g:magit_default_show_all_files|) * foldlevel: set default magit buffer foldlevel for this session (see |vimagit-g:magit_default_fold_level|) - *:Magit* *magit#show_magit('v')* + *:Magit* *magit#show_magit('v')* :Magit open magit buffer @@ -263,10 +276,11 @@ let g:magit_show_help=[01] *vimagit-g:magit_default_show_all_files* When this variable is set to 0, all diff files are hidden by default. -When this variable is set to 1, all diff files are shown by default. -Default value is 0. +When this variable is set to 1, all diff for modified files are shown by default. +When this variable is set to 2, all diff for all files are shown by default. +Default value is 1. NB: for repository with large number of differences, display may be slow. -let g:magit_default_show_all_files=[01] +let g:magit_default_show_all_files=[012] *vimagit-g:magit_default_fold_level* Default foldlevel for magit buffer. @@ -276,6 +290,12 @@ When set to 2, filenames and hunks are unfolded. Default value is 1. let g:magit_default_fold_level=[012] + *vimagit-g:magit_default_sections* +With this variable, the user is able to choose which sections are displayed in +magit buffer, and in which order. +Default value: +let g:magit_default_sections = ['info', 'global_help', 'commit', 'staged', 'unstaged'] + *vimagit-g:magit_warning_max_lines* This variable is the maximum number of diff lines that vimagit will display without warning the user. If the number of diff lines to display is greater than diff --git a/plugin/magit.vim b/plugin/magit.vim index 31afbf2..ea24755 100644 --- a/plugin/magit.vim +++ b/plugin/magit.vim @@ -45,8 +45,9 @@ let g:magit_folding_close_mapping = get(g:, 'magit_folding_close_mapping', " user options let g:magit_enabled = get(g:, 'magit_enabled', 1) let g:magit_show_help = get(g:, 'magit_show_help', 1) -let g:magit_default_show_all_files = get(g:, 'magit_default_show_all_files', 0) +let g:magit_default_show_all_files = get(g:, 'magit_default_show_all_files', 1) let g:magit_default_fold_level = get(g:, 'magit_default_fold_level', 1) +let g:magit_default_sections = get(g:, 'magit_default_sections', ['info', 'global_help', 'commit', 'staged', 'unstaged']) let g:magit_warning_max_lines = get(g:, 'magit_warning_max_lines', 10000) @@ -227,32 +228,34 @@ let s:magit_commit_mode='' " 'CC': prepare a brand new commit message " 'CA': get the last commit message function! s:mg_get_commit_section() - let commit_mode_str="" - if ( s:magit_commit_mode == 'CC' ) - let commit_mode_str="normal" - elseif ( s:magit_commit_mode == 'CA' ) - let commit_mode_str="amend" - endif - silent put ='' - silent put =g:magit_sections.commit_start - silent put ='Commit mode: '.commit_mode_str - call mg_section_help('commit') - silent put =magit#utils#underline(g:magit_sections.commit_start) - silent put ='' + if ( s:magit_commit_mode != '' ) + let commit_mode_str="" + if ( s:magit_commit_mode == 'CC' ) + let commit_mode_str="normal" + elseif ( s:magit_commit_mode == 'CA' ) + let commit_mode_str="amend" + endif + silent put ='' + silent put =g:magit_sections.commit_start + silent put ='Commit mode: '.commit_mode_str + call mg_section_help('commit') + silent put =magit#utils#underline(g:magit_sections.commit_start) + silent put ='' - let git_dir=magit#git#git_dir() - " refresh the COMMIT_EDITMSG file - if ( s:magit_commit_mode == 'CC' ) - silent! call magit#utils#system("GIT_EDITOR=/bin/false git commit -e 2> /dev/null") - elseif ( s:magit_commit_mode == 'CA' ) - silent! call magit#utils#system("GIT_EDITOR=/bin/false git commit --amend -e 2> /dev/null") - endif - if ( filereadable(git_dir . 'COMMIT_EDITMSG') ) - let comment_char=mg_comment_char() - let commit_msg=magit#utils#join_list(filter(readfile(git_dir . 'COMMIT_EDITMSG'), 'v:val !~ "^' . comment_char . '"')) - put =commit_msg + let git_dir=magit#git#git_dir() + " refresh the COMMIT_EDITMSG file + if ( s:magit_commit_mode == 'CC' ) + silent! call magit#utils#system("GIT_EDITOR=/bin/false git commit -e 2> /dev/null") + elseif ( s:magit_commit_mode == 'CA' ) + silent! call magit#utils#system("GIT_EDITOR=/bin/false git commit --amend -e 2> /dev/null") + endif + if ( filereadable(git_dir . 'COMMIT_EDITMSG') ) + let comment_char=mg_comment_char() + let commit_msg=magit#utils#join_list(filter(readfile(git_dir . 'COMMIT_EDITMSG'), 'v:val !~ "^' . comment_char . '"')) + put =commit_msg + endif + put =g:magit_sections.commit_end endif - put =g:magit_sections.commit_end endfunction " s:mg_comment_char: this function gets the commentChar from git config @@ -502,6 +505,16 @@ function! magit#open_close_folding(...) call magit#update_buffer() endfunction +" s:mg_display_functions: Dict wrapping all display related functions +" This Dict should be accessed through g:magit_default_sections +let s:mg_display_functions = { + \ 'info': { 'fn': function("s:mg_get_info"), 'arg': []}, + \ 'global_help': { 'fn': function("s:mg_section_help"), 'arg': ['global']}, + \ 'commit': { 'fn': function("s:mg_get_commit_section"), 'arg': []}, + \ 'staged': { 'fn': function("s:mg_get_staged_section"), 'arg': ['staged']}, + \ 'unstaged': { 'fn': function("s:mg_get_staged_section"), 'arg': ['unstaged']}, + \ 'stash': { 'fn': function("s:mg_get_stashes"), 'arg': []}, +\ } " magit#update_buffer: this function: " 1. checks that current buffer is the wanted one @@ -527,26 +540,19 @@ function! magit#update_buffer() " delete buffer silent! execute "silent :%delete _" - call mg_get_info() - call mg_section_help('global') - if ( s:magit_commit_mode != '' ) - call mg_get_commit_section() - endif call s:state.update() - if ( s:state.nb_diff_lines > g:magit_warning_max_lines && b:magit_warning_answered_yes == 0 ) - let ret = input("There are " . s:state.nb_diff_lines . " diff lines to display. Do you want to display all diffs? y(es) / N(o) : ", "") - if ( ret !~? '^y\%(e\%(s\)\?\)\?$' ) - let b:magit_default_show_all_files = 0 - call s:state.set_files_visible(0) - else - let b:magit_warning_answered_yes = 1 - endif - endif - - call mg_get_staged_section('staged') - call mg_get_staged_section('unstaged') - call mg_get_stashes() + for section in g:magit_default_sections + try + let func = s:mg_display_functions[section] + catch + echohl WarningMsg + echom 'unknown section to display: ' . section + echom 'please check your redefinition of g:magit_default_sections' + echohl None + endtry + call call(func.fn, func.arg) + endfor call winrestview(l:winview) @@ -589,7 +595,7 @@ function! magit#show_magit(display, ...) let b:magit_default_show_all_files = g:magit_default_show_all_files let b:magit_default_fold_level = g:magit_default_fold_level - let b:magit_warning_answered_yes = 0 + let b:magit_warning_max_lines_answered = 0 if ( a:0 > 0 ) let b:magit_default_show_all_files = a:1 @@ -598,11 +604,13 @@ function! magit#show_magit(display, ...) let b:magit_default_fold_level = a:2 endif - silent! execute "bdelete " . g:magit_buffer_name - execute "file " . g:magit_buffer_name + if ( bufexists(g:magit_buffer_name) ) + silent! execute "bdelete " . g:magit_buffer_name + endif + silent! execute "file " . g:magit_buffer_name setlocal buftype=nofile - setlocal bufhidden=delete + setlocal bufhidden=hide setlocal noswapfile setlocal foldmethod=syntax let &l:foldlevel = b:magit_default_fold_level