diff --git a/README.md b/README.md index e9f9063..27810b2 100644 --- a/README.md +++ b/README.md @@ -27,18 +27,20 @@ Take a look at [TL;DR](#tldr) to start using it immediatly. * [x] See all your changes, staged changes, untracked/removed/renamed files in one unique buffer. * [x] Staged/unstaged/discard changes with one key press, moving the cursor around. Stage at hunk or file level. Line and partial line staging are ongoing. +* [x] Stage part of hunks, by visual select, lines or selecting bunch of lines with marks. * [x] Start to write the commit message in one key press, commit also in one key press. * [x] Modify in line the content just before staging it. * [x] Visualize stashes. Apply, pop, drop are on going. * [x] Add file to .gitignore file. -* [ ] 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 on github :heart: ! +* [ ] 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: -* Partial hunk staging (next release). * Vizualize and checkout branches. * Go through history, cherry-pick changes. * Something is missing? Open an [issue](https://github.com/jreybert/vimagit/issues/new)! +The plugin is fully tested for various versions of vim on linux: vim 7.3.249, vim 7.4.273, neovim. It is also tested for macos X: vim, macvim and neovim. Anyway, if you feel that vimagit behaves oddly (slow refresh, weird display order...) please fill an [issue](https://github.com/jreybert/vimagit/issues/new). + For the most enthusiastic, you can try the branch [next](https://github.com/jreybert/vimagit/tree/next). It is quite stable, just check its travis status before fetching it. > Why should I use vimagit, there are already plethora git plugins for vim? @@ -93,7 +95,22 @@ There are 5 sections: ### Commands -**:Magit** +#### magit#show_magit() + +Function to open magit buffer. +It takes 3 parameters: + * orientation (mandatory): it can be + - 'v', curent window is split vertically, and magit is displayed in new + buffer + - '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 + (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)) + +#### :Magit * open magit buffer. ### Mappings @@ -123,6 +140,8 @@ Following mappings are set locally, for magit buffer only, in normal mode. **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). + * If some lines in the hunk are marked (using **M**), stage only marked lines (only works for staging). * 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. @@ -131,6 +150,14 @@ Following mappings are set locally, for magit buffer only, in normal mode. * 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** + * Stage the line under the cursor. + +**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** * If cursor is in a hunk, discard hunk at cursor position. * If cursor is in diff header, discard whole file at cursor position. @@ -166,11 +193,43 @@ Following mappings are set locally, for magit buffer only, in normal mode. User can define in its prefered |vimrc| some options. -To disable vimagit plugin -> let g:magit_enabled=0 +#### g:magit_enabled + +To enable or disable vimagit plugin. +Default value is 1. +> let g:magit_enabled=[01] + +#### g:magit_show_help + +To disable chatty inline help in magit buffer (default 1) +> let g:magit_show_help=[01] + +#### 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. +NB: for repository with large number of differences, display may be slow. +> let g:magit_default_show_all_files=[01] + +#### g:magit_default_fold_level + +Default foldlevel for magit buffer. +When set to 0, both filenames and hunks are folded. +When set to 1, filenames are unfolded and hunks are folded. +When set to 2, filenames and hunks are unfolded. +Default value is 1. +> let g:magit_default_fold_level=[012] + +#### g:magit_warning_max_lines -To disable chatty inline help in magit buffer -> let g:magit_show_help=0 +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 +this variable, vimagit will ask a confirmation to the user before refreshing the +buffer. If user answer is 'yes', vimagit will display diff lines as expected. +If user answer is 'no', vimagit will close all file diffs before refreshing. +Default value is 10000. +> let g:magit_warning_max_lines=val ## Installation diff --git a/autoload/magit/git.vim b/autoload/magit/git.vim new file mode 100644 index 0000000..0389b90 --- /dev/null +++ b/autoload/magit/git.vim @@ -0,0 +1,19 @@ +" magit#git#get_status: this function returns the git status output formated +" into a List of Dict as +" [ {staged', 'unstaged', 'filename'}, ... ] +function! magit#git#get_status() + let file_list = [] + + " systemlist v7.4.248 problem again + " we can't use git status -z here, because system doesn't make the + " difference between NUL and NL. -status z terminate entries with NUL, + " instead of NF + let status_list=magit#utils#systemlist("git status --porcelain") + for file_status_line in status_list + let line_match = matchlist(file_status_line, '\(.\)\(.\) \%(.\{-\} -> \)\?"\?\(.\{-\}\)"\?$') + let filename = line_match[3] + call add(file_list, { 'staged': line_match[1], 'unstaged': line_match[2], 'filename': filename }) + endfor + return file_list +endfunction + diff --git a/autoload/magit/sign.vim b/autoload/magit/sign.vim new file mode 100644 index 0000000..7a53390 --- /dev/null +++ b/autoload/magit/sign.vim @@ -0,0 +1,118 @@ +" Got lot of stuf from vim-gitgutter +" https://github.com/airblade/vim-gitgutter + +" Vim doesn't namespace sign ids so every plugin shares the same +" namespace. Sign ids are simply integers so to avoid clashes with other +" signs we guess at a clear run. +" +" Note also we currently never reset s:next_sign_id. +let s:first_sign_id = 42000 +let s:next_sign_id = s:first_sign_id +let s:dummy_sign_id = s:first_sign_id - 1 +" Remove-all-signs optimisation requires Vim 7.3.596+. +let s:supports_star = v:version > 703 || (v:version == 703 && has("patch596")) + +let s:bufnr = bufnr(g:magit_buffer_name) + +function! magit#sign#remove_all(...) + if ( a:0 == 1 ) + let pattern = a:1 + else + let pattern = '^Magit.*' + endif + let signs = magit#sign#find_signs(pattern, 1, line('$')) + call magit#sign#remove_signs(signs) +endfunction + +" magit#sign#remove_signs: unplace a list of signs +" param[in] sign_ids: list of signs dict +function! magit#sign#remove_signs(sign_ids) + let bufnr = magit#utils#bufnr() + for sign in values(a:sign_ids) + execute "sign unplace" sign.id + endfor +endfunction + +function! magit#sign#add_sign(line, type, bufnr) + let id = get_next_sign_id() + execute ":sign place " . id . + \ " line=" . a:line . " name=" . s:magit_mark_signs[a:type] . + \ " buffer=" . a:bufnr + return id +endfunction + +function! magit#sign#remove_sign(id) + execute ":sign unplace " . a:id +endfunction + +" s:get_next_sign_id: helper function to increment sign ids +function! s:get_next_sign_id() + let next_id = s:next_sign_id + let s:next_sign_id += 1 + return next_id +endfunction + +" magit#sign#find_signs: this function returns signs matching a pattern in a +" range of lines +" param[in] pattern: regex pattern to match +" param[in] startline,endline: range of lines +" FIXME: find since which version "sign place" is sorted +function! magit#sign#find_signs(pattern, startline, endline) + let bufnr = magit#utils#bufnr() + " : {'id': , 'name': } + let found_signs = {} + + redir => signs + silent execute "sign place buffer=" . bufnr + redir END + + for sign_line in filter(split(signs, '\n'), 'v:val =~# "="') + " Typical sign line: line=88 id=1234 name=GitGutterLineAdded + " We assume splitting is faster than a regexp. + let components = split(sign_line) + let name = split(components[2], '=')[1] + let line_number = str2nr(split(components[0], '=')[1]) + if ( name =~# a:pattern && + \ line_number >= a:startline && + \ line_number <= a:endline ) + let id = str2nr(split(components[1], '=')[1]) + let found_signs[line_number] = {'id': id, 'name': name} + endif + endfor + return found_signs +endfunction + +" magit#sign#find_stage_signs: helper function to get marked lines for stage +" param[in] startline,endline: range of lines +" return Dict of marked lines +function! magit#sign#find_stage_signs(startline, endline) + return magit#sign#find_signs(s:magit_mark_signs.M, a:startline, a:endline) +endfunction + +" s:magit_mark_sign: string of the sign for lines to be staged +let s:magit_mark_signs = {'M': 'MagitTBS', 'S': 'MagitBS', 'E': 'MagitBE'} + +" magit#sign#init: initializer function for signs +function! magit#sign#init() + execute "sign define " . s:magit_mark_signs.M . " text=S> linehl=Visual" + execute "sign define " . s:magit_mark_signs.S + execute "sign define " . s:magit_mark_signs.E +endfunction + +" magit#sign#toggle_signs: toggle marks for range of lines +" marked lines are unmarked, non marked are marked +" param[in] type; type of sign to toggle (see s:magit_mark_signs) +" param[in] startline,endline: range of lines +function! magit#sign#toggle_signs(type, startline, endline) + let bufnr = magit#utils#bufnr() + let current_signs = magit#sign#find_signs(s:magit_mark_signs[a:type], a:startline, a:endline) + let line = a:startline + while ( line <= a:endline ) + if ( has_key(current_signs, line) == 0 ) + call magit#sign#add_sign(line, a:type, bufnr) + else + call magit#sign#remove_sign(current_signs[line].id) + endif + let line += 1 + endwhile +endfunction diff --git a/autoload/magit/state.vim b/autoload/magit/state.vim new file mode 100644 index 0000000..8781329 --- /dev/null +++ b/autoload/magit/state.vim @@ -0,0 +1,270 @@ +function! magit#state#is_file_visible() dict + return self.visible +endfunction + +function! magit#state#set_file_visible(val) dict + let self.visible = a:val +endfunction + +function! magit#state#toggle_file_visible() dict + let self.visible = ( self.visible == 0 ) ? 1 : 0 +endfunction + +function! magit#state#is_file_dir() dict + return self.dir != 0 +endfunction + +function! magit#state#get_files(mode) dict + return self.dict[a:mode] +endfunction + +function! magit#state#must_be_added() dict + return ( self.empty == 1 || + \ self.symlink != '' || + \ self.dir != 0 || + \ self.binary == 1 || + \ self.submodule == 1 ) +endfunction + +" magit#state#file_get_hunks: function accessor for hunks objects +" return: List of List of hunks lines +function! magit#state#file_get_hunks() dict + return self.diff.hunks +endfunction + +" magit#state#file_get_flat_hunks: function accessor for hunks lines +" return: all hunks lines of a file, including hunk headers +function! magit#state#file_get_flat_hunks() dict + let hunks = self.diff.hunks + let lines = [] + for hunk in hunks + call add(lines, hunk.header) + call add(lines, hunk.lines) + endfor + return lines +endfunction + +" s:hunk_template: template for hunk object (nested in s:diff_template) +" WARNING: this variable must be deepcopy()'ied +let s:hunk_template = { +\ 'header': '', +\ 'lines': [], +\ 'marks': [], +\} + +" s:diff_template: template for diff object (nested in s:file_template) +" WARNING: this variable must be deepcopy()'ied +let s:diff_template = { +\ 'header': [], +\ 'hunks': [s:hunk_template], +\} + +" s:file_template: template for file object +" WARNING: this variable must be deepcopy()'ied +let s:file_template = { +\ 'exists': 0, +\ 'filename': '', +\ 'status': '', +\ 'empty': 0, +\ 'dir': 0, +\ 'binary': 0, +\ 'submodule': 0, +\ 'symlink': '', +\ 'diff': s:diff_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"), +\ '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"), +\ 'get_hunks' : function("magit#state#file_get_hunks"), +\ 'get_flat_hunks' : function("magit#state#file_get_flat_hunks"), +\ 'get_filename_header' : function("magit#state#file_get_filename_header"), +\} + +" magit#state#get_file: function accessor for file +" param[in] mode: can be staged or unstaged +" param[in] filename: filename to access +" param[in] create: boolean. If 1, non existing file in Dict will be created. +" if 0, 'file_doesnt_exists' exception will be thrown +" return: Dict of file +function! magit#state#get_file(mode, filename, ...) dict + let file_exists = has_key(self.dict[a:mode], a:filename) + 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' + endif + return self.dict[a:mode][a:filename] +endfunction + +" magit#state#file_get_header: function accessor for diff header +" param[in] mode: can be staged or unstaged +" param[in] filename: header of filename to access +" return: List of diff header lines +function! magit#state#file_get_header() dict + return self.diff.header +endfunction + +function! magit#state#file_get_filename_header() dict + if ( self.status == 'L' ) + return g:magit_git_status_code.L . ': ' . self.filename . ' -> ' . self.symlink + else + return g:magit_git_status_code[self.status] . ': ' . self.filename + endif +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 + + if ( a:status == '?' && getftype(a:filename) == 'link' ) + let file.status = 'L' + let file.symlink = resolve(a:filename) + let file.diff.hunks[0].header = 'New symbolic link file' + elseif ( magit#utils#is_submodule(a:filename)) + let file.status = 'S' + let file.submodule = 1 + 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 + elseif ( a:status == '?' && isdirectory(a:filename) == 1 ) + let file.status = 'N' + let file.dir = 1 + for subfile in magit#utils#ls_all(a:filename) + call self.add_file(a:mode, a:status, subfile, a:depth + 1) + endfor + elseif ( a:status == '?' && getfsize(a:filename) == 0 ) + let file.status = 'E' + let file.empty = 1 + let file.diff.hunks[0].header = 'New empty file' + elseif ( magit#utils#is_binary(magit#utils#add_quotes(a:filename))) + let file.binary = 1 + let file.diff.hunks[0].header = 'Binary file' + else + let line = 0 + " match( + while ( line < len(diff_list) && diff_list[line] !~ "^@.*" ) + call add(file.diff.header, diff_list[line]) + let line += 1 + endwhile + + if ( line < len(diff_list) ) + let hunk = file.diff.hunks[0] + let hunk.header = diff_list[line] + + for diff_line in diff_list[line+1 : -1] + if ( diff_line =~ "^@.*" ) + let hunk = deepcopy(s:hunk_template) + call add(file.diff.hunks, hunk) + let hunk.header = diff_line + continue + endif + call add(hunk.lines, diff_line) + endfor + endif + if ( file.is_visible() ) + let self.nb_diff_lines += len(diff_list) + endif + endif +endfunction + +" magit#state#update: update self.dict +" if a file does not exists anymore (because all its changes have been +" committed, deleted, discarded), it is removed from g:mg_diff_dict +" else, its diff is discarded and regenrated +" what is resilient is its 'visible' parameter +function! magit#state#update() dict + let self.nb_diff_lines = 0 + for diff_dict_mode in values(self.dict) + for file in values(diff_dict_mode) + let file.exists = 0 + " always discard previous diff + let file.diff = deepcopy(s:diff_template) + endfor + endfor + + let dir = getcwd() + try + call magit#utils#lcd(magit#utils#top_dir()) + call magit#utils#refresh_submodule_list() + for [mode, diff_dict_mode] in items(self.dict) + let status_list = magit#git#get_status() + for file_status in status_list + let status=file_status[mode] + + " untracked code apperas in staged column, we skip it + if ( status == ' ' || ( ( mode == 'staged' ) && status == '?' ) ) + continue + endif + call self.add_file(mode, status, file_status.filename, 0) + endfor + endfor + finally + call magit#utils#lcd(dir) + endtry + + " remove files that have changed their mode or been committed/deleted/discarded... + for diff_dict_mode in values(self.dict) + for [key, file] in items(diff_dict_mode) + if ( file.exists == 0 ) + unlet diff_dict_mode[key] + endif + endfor + endfor +endfunction + +function! magit#state#set_files_visible(is_visible) dict + for diff_dict_mode in values(self.dict) + for file in values(diff_dict_mode) + call file.set_visible(a:is_visible) + endfor + endfor +endfunction + +" dict: structure containing all diffs +" It is formatted as follow +" { +" 'staged': { +" 'filename': s:file_template, +" 'filename': s:file_template, +" ... +" }, +" 'unstaged': { +" 'filename': s:file_template, +" 'filename': s:file_template, +" ... +" }, +" } +let magit#state#state = { + \ 'nb_diff_lines': 0, + \ 'get_file': function("magit#state#get_file"), + \ 'get_files': function("magit#state#get_files"), + \ 'add_file': function("magit#state#add_file"), + \ 'set_files_visible': function("magit#state#set_files_visible"), + \ 'update': function("magit#state#update"), + \ 'dict': { 'staged': {}, 'unstaged': {}}, + \ } + diff --git a/autoload/magit/utils.vim b/autoload/magit/utils.vim new file mode 100644 index 0000000..a8ec6ef --- /dev/null +++ b/autoload/magit/utils.vim @@ -0,0 +1,209 @@ +" s:magit_top_dir: top directory of git tree +" it is evaluated only once +" FIXME: it won't work when playing with multiple git directories wihtin one +" vim session +let s:magit_top_dir='' +" magit#utils#top_dir: return the absolute path of current git worktree +" return top directory +function! magit#utils#top_dir() + if ( s:magit_top_dir == '' ) + let s:magit_top_dir=magit#utils#strip( + \ system("git rev-parse --show-toplevel")) . "/" + if ( v:shell_error != 0 ) + echoerr "Git error: " . s:magit_top_dir + endif + endif + return s:magit_top_dir +endfunction + +" s:magit_git_dir: git directory +" it is evaluated only once +" FIXME: it won't work when playing with multiple git directories wihtin one +" vim session +let s:magit_git_dir='' +" magit#utils#git_dir: return the absolute path of current git worktree +" return git directory +function! magit#utils#git_dir() + if ( s:magit_git_dir == '' ) + let s:magit_git_dir=magit#utils#strip(system("git rev-parse --git-dir")) . "/" + if ( v:shell_error != 0 ) + echoerr "Git error: " . s:magit_git_dir + endif + endif + return s:magit_git_dir +endfunction + +" s:magit#utils#is_binary: check if file is a binary file +" param[in] filename: the file path. it must quoted if it contains spaces +function! magit#utils#is_binary(filename) + return ( match(system("file --mime " . a:filename ), + \ a:filename . ".*charset=binary") != -1 ) +endfunction + +" magit#utils#ls_all: list all files (including hidden ones) in a given path +" return : list of filenames +function! magit#utils#ls_all(path) + return split(globpath(a:path, '.[^.]*', 1) . "\n" . + \ globpath(a:path, '*', 1), '\n') +endfunction + +let s:submodule_list = [] +" magit#utils#refresh_submodule_list: this function refresh the List s:submodule_list +" magit#utils#is_submodule() is using s:submodule_list +function! magit#utils#refresh_submodule_list() + let s:submodule_list = map(split(system("git submodule status"), "\n"), 'split(v:val)[1]') +endfunction + +" magit#utils#is_submodule search if dirname is in s:submodule_list +" param[in] dirname: must end with / +" INFO: must be called from top work tree +function! magit#utils#is_submodule(dirname) + return ( index(s:submodule_list, a:dirname) != -1 ) +endfunction + +" s:magit_cd_cmd: plugin variable to choose lcd/cd command, 'lcd' if exists, +" 'cd' otherwise +let s:magit_cd_cmd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' +" magit#utils#lcd: helper function to lcd. use cd if lcd doesn't exists +function! magit#utils#lcd(dir) + execute s:magit_cd_cmd . a:dir +endfunction + +" magit#utils#system: wrapper for system, which only takes String as input in vim, +" although it can take String or List input in neovim. +" INFO: temporarly change pwd to git top directory, then restore to previous +" pwd at the end of function +" param[in] ...: command + optional args +" return: command output as a string +function! magit#utils#system(...) + let dir = getcwd() + try + execute s:magit_cd_cmd . magit#utils#top_dir() + " List as system() input is since v7.4.247, it is safe to check + " systemlist, which is sine v7.4.248 + if exists('*systemlist') + return call('system', a:000) + else + if ( a:0 == 2 ) + if ( type(a:2) == type([]) ) + " ouch, this one is tough: input is very very sensitive, join + " MUST BE done with "\n", not '\n' !! + let arg=join(a:2, "\n") + else + let arg=a:2 + endif + return system(a:1, arg) + else + return system(a:1) + endif + endif + finally + execute s:magit_cd_cmd . dir + endtry +endfunction + +" magit#utils#systemlist: wrapper for systemlist, which only exists in neovim for +" the moment. +" INFO: temporarly change pwd to git top directory, then restore to previous +" pwd at the end of function +" param[in] ...: command + optional args to execute, args can be List or String +" return: command output as a list +function! magit#utils#systemlist(...) + let dir = getcwd() + try + execute s:magit_cd_cmd . magit#utils#top_dir() + " systemlist since v7.4.248 + if exists('*systemlist') + return call('systemlist', a:000) + else + return split(call('magit#utils#system', a:000), '\n') + endif + finally + execute s:magit_cd_cmd . dir + endtry +endfunction + +" magit#utils#underline: helper function to underline a string +" param[in] title: string to underline +" return a string composed of strlen(title) '=' +function! magit#utils#underline(title) + return substitute(a:title, ".", "=", "g") +endfunction + +" magit#utils#strip: helper function to strip a string +" WARNING: it only works with monoline string +" param[in] string: string to strip +" return: stripped string +function! magit#utils#strip(string) + return substitute(a:string, '^\s*\(.\{-}\)\s*\n\=$', '\1', '') +endfunction + +" magit#utils#join_list: helper function to concatente a list of strings with newlines +" param[in] list: List to to concat +" return: concatenated list +function! magit#utils#join_list(list) + return join(a:list, "\n") . "\n" +endfunction + +" magit#utils#add_quotes: helper function to protect filename with quotes +" return quoted filename +function! magit#utils#add_quotes(filename) + return '"' . a:filename . '"' +endfunction + +" magit#utils#remove_quotes: helper function to remove quotes aroudn filename +" return unquoted filename +function! magit#utils#remove_quotes(filename) + let ret=matchlist(a:filename, '"\([^"]*\)"') + if ( empty(ret) ) + throw 'no quotes found: ' . a:filename + endif + return ret[1] +endfunction + +" magit#utils#fatten: flat a nested list. it return a one dimensional list with +" primary elements +" https://gist.github.com/dahu/3322468 +" param[in] list: a List, can be nested or not +" return: one dimensional list +function! magit#utils#flatten(list) + let val = [] + for elem in a:list + if type(elem) == type([]) + call extend(val, magit#utils#flatten(elem)) + else + call extend(val, [elem]) + endif + unlet elem + endfor + return val +endfunction + +" magit#utils#append_file: helper function to append to a file +" Version working with file *possibly* containing trailing newline +" param[in] file: filename to append +" param[in] lines: List of lines to append +function! magit#utils#append_file(file, lines) + let fcontents=[] + if ( filereadable(a:file) ) + let fcontents=readfile(a:file, 'b') + endif + if !empty(fcontents) && empty(fcontents[-1]) + call remove(fcontents, -1) + endif + call writefile(fcontents+a:lines, a:file, 'b') +endfunction + +" s:bufnr: local variable to store current magit buffer id +let s:bufnr = 0 +" magit#utils#setbufnr: function to set current magit buffer id +" param[in] bufnr: current magit buffer id +function! magit#utils#setbufnr(bufnr) + let s:bufnr = a:bufnr +endfunction + +" magit#utils#bufnr: function to get current magit buffer id +" return: current magit buffer id +function! magit#utils#bufnr() + return s:bufnr +endfunction diff --git a/common/magit_common.vim b/common/magit_common.vim index c14c3be..47d488b 100644 --- a/common/magit_common.vim +++ b/common/magit_common.vim @@ -21,6 +21,8 @@ let g:magit_git_status_code = { \ '!': 'ignored', \ 'E': 'empty', \ 'L': 'symlink', + \ 'N': 'new dir', + \ 'S': 'submodule', \ } " Regular expressions used to select blocks diff --git a/doc/vimagit.txt b/doc/vimagit.txt index 2e0aaca..98d1c3e 100644 --- a/doc/vimagit.txt +++ b/doc/vimagit.txt @@ -112,6 +112,20 @@ INLINE MODIFICATIONS *vimagit-inline-modification* COMMANDS *vimagit-commands* + *magit#show_magit()* +Function to open magit buffer. +It takes 3 parameters: + * orientation (mandatory): it can be + - 'v', curent window is split vertically, and magit is displayed in new + buffer + - '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 + (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 open magit buffer @@ -151,11 +165,16 @@ Following mappings are set locally, for magit buffer only, in normal mode. *vimagit-g:magit_folding_close_mapping* zc,zC Typing zc on a file will hide its diffs. - *vimagit-S* *magit#stage_hunk()* + *vimagit-S* *magit#stage_hunk(0)* + *magit#stage_vselect()* *vimagit-g:magit_stage_hunk_mapping* 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 (see |visual-use|), stage + only selected lines (only works for staging). + If some lines in the hunk are marked (see |vimagit-M|), stage only + these lines (only works for staging). 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 @@ -169,8 +188,18 @@ Following mappings are set locally, for magit buffer only, in normal mode. On the other side, when cursor is in "Staged changes" section, it will unstage file. + *vimagit-L* *magit#stage_vselect()* + L Stage the line under the cursor (only works for staging) + + *vimagit-M* *magit#mark_vselect()* + M + Mark the current line to be staged. + If some lines in the hunk are selected (see |visual-use|), mark + selected lines. + To staged marked lines, press S (see |vimagit-S|) in the current + hunk. - *vimagit-DDD* *magit#discard_hunk()* + *vimagit-DDD* *magit#stage_hunk(1)* *vimagit-g:magit_discard_hunk_mapping* DDD If cursor is in a hunk, discard hunk at cursor position. If cursor is in diff header, discard whole file at cursor @@ -224,12 +253,37 @@ Following mappings are set locally, for magit buffer only, in normal mode. User can define in its prefered |vimrc| some options. *vimagit-g:magit_enabled* -To disable vimagit plugin -let g:magit_enabled=0 +To enable or disable vimagit plugin. +Default value is 1. +let g:magit_enabled=[01] *vimagit-g:magit_show_help* -To disable chatty inline help in magit buffer -let g:magit_show_help=0 +To disable chatty inline help in magit buffer (default 1) +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. +NB: for repository with large number of differences, display may be slow. +let g:magit_default_show_all_files=[01] + + *vimagit-g:magit_default_fold_level* +Default foldlevel for magit buffer. +When set to 0, both filenames and hunks are folded. +When set to 1, filenames are unfolded and hunks are folded. +When set to 2, filenames and hunks are unfolded. +Default value is 1. +let g:magit_default_fold_level=[012] + + *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 +this variable, vimagit will ask a confirmation to the user before refreshing the +buffer. If user answer is 'yes', vimagit will display diff lines as expected. +If user answer is 'no', vimagit will close all file diffs before refreshing. +Defaulty value is 10000. +let g:magit_warning_max_lines=val =============================================================================== 6. FAQ *vimagit-FAQ* diff --git a/plugin/magit.vim b/plugin/magit.vim index e2d18e8..4b5ee38 100644 --- a/plugin/magit.vim +++ b/plugin/magit.vim @@ -15,8 +15,10 @@ let g:loaded_magit = 1 " syntax files execute 'source ' . resolve(expand(':p:h')) . '/../common/magit_common.vim' -" g:magit_unstaged_buffer_name: vim buffer name for vimagit -let g:magit_unstaged_buffer_name = "magit-playground" +" g:magit_buffer_name: vim buffer name for vimagit +let g:magit_buffer_name = "magit-playground" + +let s:state = deepcopy(magit#state#state) " s:set: helper function to set user definable variable " param[in] var: variable to set @@ -38,6 +40,8 @@ call s:set('g:magit_show_magit_mapping', 'M' ) " these mapping are applied locally, for magit buffer only call s:set('g:magit_stage_file_mapping', 'F' ) call s:set('g:magit_stage_hunk_mapping', 'S' ) +call s:set('g:magit_stage_line_mapping', 'L' ) +call s:set('g:magit_mark_line_mapping', 'M' ) call s:set('g:magit_discard_hunk_mapping', 'DDD' ) call s:set('g:magit_commit_mapping_command', 'w' ) call s:set('g:magit_commit_mapping', 'CC' ) @@ -55,210 +59,39 @@ call s:set('g:magit_folding_close_mapping', [ 'zc', 'zC' ]) " user options call s:set('g:magit_enabled', 1) call s:set('g:magit_show_help', 1) +call s:set('g:magit_default_show_all_files', 0) +call s:set('g:magit_default_fold_level', 1) + +call s:set('g:magit_warning_max_lines', 10000) execute "nnoremap " . g:magit_show_magit_mapping . " :call magit#show_magit('v')" " }}} " {{{ Internal functions -" s:magit_top_dir: top directory of git tree -" it is evaluated only once -" FIXME: it won't work when playing with multiple git directories wihtin one -" vim session -let s:magit_top_dir='' -" s:mg_top_dir: return the absolute path of current git worktree -" return top directory -function! s:mg_top_dir() - if ( s:magit_top_dir == '' ) - let s:magit_top_dir=mg_strip(system("git rev-parse --show-toplevel")) . "/" - if ( v:shell_error != 0 ) - echoerr "Git error: " . s:magit_top_dir - endif - endif - return s:magit_top_dir -endfunction - -" s:magit_git_dir: git directory -" it is evaluated only once -" FIXME: it won't work when playing with multiple git directories wihtin one -" vim session -let s:magit_git_dir='' -" s:mg_git_dir: return the absolute path of current git worktree -" return git directory -function! s:mg_git_dir() - if ( s:magit_git_dir == '' ) - let s:magit_git_dir=mg_strip(system("git rev-parse --git-dir")) . "/" - if ( v:shell_error != 0 ) - echoerr "Git error: " . s:magit_git_dir - endif - endif - return s:magit_git_dir -endfunction - -" s:magit_cd_cmd: plugin variable to choose lcd/cd command, 'lcd' if exists, -" 'cd' otherwise -let s:magit_cd_cmd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' - -" s:mg_system: wrapper for system, which only takes String as input in vim, -" although it can take String or List input in neovim. -" INFO: temporarly change pwd to git top directory, then restore to previous -" pwd at the end of function -" param[in] ...: command + optional args -" return: command output as a string -function! s:mg_system(...) - let dir = getcwd() - try - execute s:magit_cd_cmd . mg_top_dir() - " List as system() input is since v7.4.247, it is safe to check - " systemlist, which is sine v7.4.248 - if exists('*systemlist') - return call('system', a:000) - else - if ( a:0 == 2 ) - if ( type(a:2) == type([]) ) - " ouch, this one is tough: input is very very sensitive, join - " MUST BE done with "\n", not '\n' !! - let arg=join(a:2, "\n") - else - let arg=a:2 - endif - return system(a:1, arg) - else - return system(a:1) - endif - endif - finally - execute s:magit_cd_cmd . dir - endtry -endfunction - -" s:mg_systemlist: wrapper for systemlist, which only exists in neovim for -" the moment. -" INFO: temporarly change pwd to git top directory, then restore to previous -" pwd at the end of function -" param[in] ...: command + optional args to execute, args can be List or String -" return: command output as a list -function! s:mg_systemlist(...) - let dir = getcwd() - try - execute s:magit_cd_cmd . mg_top_dir() - " systemlist since v7.4.248 - if exists('*systemlist') - return call('systemlist', a:000) - else - return split(call('mg_system', a:000), '\n') - endif - finally - execute s:magit_cd_cmd . dir - endtry -endfunction - -" s:mg_underline: helper function to underline a string -" param[in] title: string to underline -" return a string composed of strlen(title) '=' -function! s:mg_underline(title) - return substitute(a:title, ".", "=", "g") -endfunction - -" s:mg_strip: helper function to strip a string -" WARNING: it only works with monoline string -" param[in] string: string to strip -" return: stripped string -function! s:mg_strip(string) - return substitute(a:string, '^\s*\(.\{-}\)\s*\n\=$', '\1', '') -endfunction - -" s:mg_join_list: helper function to concatente a list of strings with newlines -" param[in] list: List to to concat -" return: concatenated list -function! s:mg_join_list(list) - return join(a:list, "\n") . "\n" -endfunction - -" s:mg_add_quotes: helper function to protect filename with quotes -" return quoted filename -function! s:mg_add_quotes(filename) - return '"' . a:filename . '"' -endfunction - -" s:mg_remove_quotes: helper function to remove quotes aroudn filename -" return unquoted filename -function! s:mg_remove_quotes(filename) - let ret=matchlist(a:filename, '"\([^"]*\)"') - if ( empty(ret) ) - throw 'no quotes found: ' . a:filename - endif - return ret[1] -endfunction - -" s:mg_fatten: flat a nested list. it return a one dimensional list with -" primary elements -" https://gist.github.com/dahu/3322468 -" param[in] list: a List, can be nested or not -" return: one dimensional list -function! s:mg_flatten(list) - let val = [] - for elem in a:list - if type(elem) == type([]) - call extend(val, mg_flatten(elem)) - else - call extend(val, [elem]) - endif - unlet elem - endfor - return val -endfunction - -" s:mg_append_file: helper function to append to a file -" Version working with file *possibly* containing trailing newline -" param[in] file: filename to append -" param[in] lines: List of lines to append -function! s:mg_append_file(file, lines) - let fcontents=[] - if ( filereadable(a:file) ) - let fcontents=readfile(a:file, 'b') - endif - if !empty(fcontents) && empty(fcontents[-1]) - call remove(fcontents, -1) - endif - call writefile(fcontents+a:lines, a:file, 'b') -endfunction - -" s:mg_get_status_list: this function returns the git status output formated -" into a List of Dict as -" [ {staged', 'unstaged', 'filename'}, ... ] -function! s:mg_get_status_list() - let file_list = [] - - " systemlist v7.4.248 problem again - " we can't use git status -z here, because system doesn't make the - " difference between NUL and NL. -status z terminate entries with NUL, - " instead of NF - let status_list=mg_systemlist("git status --porcelain") - for file_status_line in status_list - let line_match = matchlist(file_status_line, '\(.\)\(.\) \%(.\{-\} -> \)\?"\?\(.\{-\}\)"\?$') - let filename = line_match[3] - call add(file_list, { 'staged': line_match[1], 'unstaged': line_match[2], 'filename': filename }) - endfor - return file_list -endfunction - " s:magit_inline_help: Dict containing inline help for each section let s:magit_inline_help = { \ 'staged': [ -\'S if cursor in diff header, unstage file', +\'S if cursor on filename header, unstage file', \' if cursor in hunk, unstage hunk', -\'F if cursor in diff header or hunk, unstage file', +\'F if cursor on filename header or hunk, unstage whole file', \], \ 'unstaged': [ -\'S if cursor in diff header, stage file', +\'S if cursor on filename header, stage file', \' if cursor in hunk, stage hunk', -\'F if cursor in diff header or hunk, stage file', +\' if visual selection in hunk (with v), stage selection', +\' if lines marked in hunk (with M), stage marked lines', +\'L stage the line under the cursor', +\'M if cursor in hunk, mark line under cursor "to be staged"', +\' if visual selection in hunk (with v), mark selected lines "to be' +\' staged"', +\'F if cursor on filename header or hunk, stage whole file', \'DDD discard file changes (warning, changes will be lost)', \'I add file in .gitgnore', \], \ 'global': [ -\'C CC set commit mode to normal, and show "Commit message" section', +\' if cursor on filename header line, unhide diffs for this file', +\'CC set commit mode to normal, and show "Commit message" section', \'CA set commit mode amend, and show "Commit message" section with previous', \' commit message', \'CF amend staged changes to previous commit without modifying the previous', @@ -271,8 +104,8 @@ let s:magit_inline_help = { \'You will still be able to toggle inline help with h', \], \ 'commit': [ -\'C CC commit all staged changes with commit mode previously set (normal or', -\':w amend) with message written in this section', +\'CC,:w commit all staged changes with commit mode previously set (normal or', +\' amend) with message written in this section', \], \} @@ -301,155 +134,55 @@ endfunction " protected functions like magit#update_buffer function! s:mg_get_info() silent put ='' - silent put =g:magit_sections['info'] - silent put =mg_underline(g:magit_sections['info']) + silent put =g:magit_sections.info + silent put =magit#utils#underline(g:magit_sections.info) silent put ='' - let branch=mg_system("git rev-parse --abbrev-ref HEAD") - let commit=mg_system("git show -s --oneline") + let branch=magit#utils#system("git rev-parse --abbrev-ref HEAD") + let commit=magit#utils#system("git show -s --oneline") silent put ='Current branch: ' . branch silent put ='Last commit: ' . commit silent put ='' endfunction -" s:mg_diff_dict: big main global variable, containing all diffs -" It is formatted as follow -" { 'staged_or_unstaged': staged/unstaged -" [ -" { 'filename': -" { 'visible': bool, -" 'status' : g:magit_git_status_code, -" 'exists' : bool -" 'diff' : [ [header], [hunk0], [hunk1], ...] -" } -" }, -" ... -" ] -" } -let s:mg_diff_dict = { 'staged': {}, 'unstaged': {} } - -" s:mg_diff_dict_get_file: mg_diff_dict accessor for file -" param[in] mode: can be staged or unstaged -" param[in] filename: filename to access -" param[in] create: boolean. If 1, non existing file in Dict will be created. -" if 0, 'file_doesnt_exists' exception will be thrown -" return: Dict of file -function! s:mg_diff_dict_get_file(mode, filename, create) - let file_exists = has_key(s:mg_diff_dict[a:mode], a:filename) - if ( file_exists == 0 && a:create == 1 ) - let s:mg_diff_dict[a:mode][a:filename] = {} - let s:mg_diff_dict[a:mode][a:filename]['visible'] = 0 - elseif ( file_exists == 0 && a:create == 0 ) - throw 'file_doesnt_exists' - endif - return s:mg_diff_dict[a:mode][a:filename] -endfunction - -" s:mg_diff_dict_get_header: mg_diff_dict accessor for diff header -" param[in] mode: can be staged or unstaged -" param[in] filename: header of filename to access -" return: List of diff header lines -function! s:mg_diff_dict_get_header(mode, filename) - let diff_dict_file = s:mg_diff_dict_get_file(a:mode, a:filename, 0) - return diff_dict_file['diff'][0] -endfunction - -" s:mg_diff_dict_get_hunks: mg_diff_dict accessor for hunks -" param[in] mode: can be staged or unstaged -" param[in] filename: hunks of filename to access -" return: List of List of hunks lines -function! s:mg_diff_dict_get_hunks(mode, filename) - let diff_dict_file = s:mg_diff_dict_get_file(a:mode, a:filename, 0) - return diff_dict_file['diff'][1:-1] -endfunction - -" s:mg_diff_dict_add_file: mg_diff_dict 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! s:mg_diff_dict_add_file(mode, status, filename) - 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 . " " - \ . mg_add_quotes(a:filename) - let diff_list=s:mg_systemlist(diff_cmd) - if ( empty(diff_list) ) - echoerr "diff command \"" . diff_cmd . "\" returned nothing" - endif - let diff_dict_file = mg_diff_dict_get_file(a:mode, a:filename, 1) - let diff_dict_file['diff'] = [] - let diff_dict_file['exists'] = 1 - let diff_dict_file['status'] = a:status - let diff_dict_file['empty'] = 0 - let diff_dict_file['binary'] = 0 - let diff_dict_file['symlink'] = '' - if ( a:status == '?' && getftype(a:filename) == 'link' ) - let diff_dict_file['symlink'] = resolve(a:filename) - call add(diff_dict_file['diff'], ['no header']) - call add(diff_dict_file['diff'], ['New symbolic link file']) - elseif ( a:status == '?' && getfsize(a:filename) == 0 ) - let diff_dict_file['empty'] = 1 - call add(diff_dict_file['diff'], ['no header']) - call add(diff_dict_file['diff'], ['New empty file']) - elseif ( match(system("file --mime " . mg_add_quotes(a:filename)), a:filename . ".*charset=binary") != -1 ) - let diff_dict_file['binary'] = 1 - call add(diff_dict_file['diff'], ['no header']) - call add(diff_dict_file['diff'], ['Binary file']) - else - let index = 0 - call add(diff_dict_file['diff'], []) - for diff_line in diff_list - if ( diff_line =~ "^@.*" ) - let index+=1 - call add(diff_dict_file['diff'], []) - endif - call add(diff_dict_file['diff'][index], diff_line) - endfor - endif -endfunction - -" s:mg_update_diff_dict: update s:mg_diff_dict -" if a file does not exists anymore (because all its changes have been -" committed, deleted, discarded), it is removed from s:mg_diff_dict -" else, its diff is discarded and regenrated -" what is resilient is its 'visible' parameter -function! s:mg_update_diff_dict() - for diff_dict_mode in values(s:mg_diff_dict) - for file in values(diff_dict_mode) - let file['exists'] = 0 - " always discard previous diff - unlet file['diff'] - endfor - endfor - - for [mode, diff_dict_mode] in items(s:mg_diff_dict) +function! s:mg_display_files(mode, curdir, depth) - let status_list = s:mg_get_status_list() - for file_status in status_list - let status=file_status[mode] + " FIXME: ouch, must store subdirs in more efficient way + for filename in sort(keys(s:state.get_files(a:mode))) + let file = s:state.get_file(a:mode, filename, 0) + if ( file.depth != a:depth || filename !~ a:curdir . '.*' ) + continue + endif + put =file.get_filename_header() - " untracked code apperas in staged column, we skip it - if ( status == ' ' || ( ( mode == 'staged' ) && status == '?' ) ) + if ( file.dir != 0 ) + if ( file.visible == 1 ) + call s:mg_display_files(a:mode, filename, a:depth + 1) continue endif - call mg_diff_dict_add_file(mode, status, file_status['filename']) - endfor - endfor + endif - " remove files that have changed their mode or been committed/deleted/discarded... - for diff_dict_mode in values(s:mg_diff_dict) - for [key, file] in items(diff_dict_mode) - if ( file['exists'] == 0 ) - unlet diff_dict_mode[key] + if ( file.visible == 0 ) + put ='' + continue + endif + if ( file.exists == 0 ) + echoerr "Error, " . filename . " should not exists" + endif + let hunks = file.get_hunks() + for hunk in hunks + if ( hunk.header != '' ) + silent put =hunk.header + endif + if ( !empty(hunk.lines) ) + silent put =hunk.lines endif endfor + put ='' endfor endfunction - " s:mg_get_staged_section: this function writes in current buffer all staged -" or unstaged files, using s:mg_diff_dict information +" or unstaged files, using s:state.dict information " WARNING: this function writes in file, it should only be called through " protected functions like magit#update_buffer " param[in] mode: 'staged' or 'unstaged' @@ -457,45 +190,24 @@ function! s:mg_get_staged_section(mode) put ='' put =g:magit_sections[a:mode] call mg_section_help(a:mode) - put =s:mg_underline(g:magit_sections[a:mode]) + put =magit#utils#underline(g:magit_sections[a:mode]) put ='' - - for [ filename, file_props ] in items(s:mg_diff_dict[a:mode]) - if ( file_props['empty'] == 1 ) - put =g:magit_git_status_code['E'] . ': ' . filename - elseif ( file_props['symlink'] != '' ) - put =g:magit_git_status_code['L'] . ': ' . filename . ' -> ' . file_props['symlink'] - else - put =g:magit_git_status_code[file_props['status']] . ': ' . filename - endif - if ( file_props['visible'] == 0 ) - put ='' - continue - endif - if ( file_props['exists'] == 0 ) - echoerr "Error, " . filename . " should not exists" - endif - let hunks=mg_diff_dict_get_hunks(a:mode, filename) - for diff_line in hunks - silent put =diff_line - endfor - put ='' - endfor + call s:mg_display_files(a:mode, '', 0) endfunction " s:mg_get_stashes: this function write in current buffer all stashes " WARNING: this function writes in file, it should only be called through " protected functions like magit#update_buffer function! s:mg_get_stashes() - silent! let stash_list=mg_systemlist("git stash list") + silent! let stash_list=magit#utils#systemlist("git stash list") if ( v:shell_error != 0 ) echoerr "Git error: " . stash_list endif if (!empty(stash_list)) silent put ='' - silent put =g:magit_sections['stash'] - silent put =mg_underline(g:magit_sections['stash']) + silent put =g:magit_sections.stash + silent put =magit#utils#underline(g:magit_sections.stash) silent put ='' for stash in stash_list @@ -529,30 +241,31 @@ function! s:mg_get_commit_section() let commit_mode_str="amend" endif silent put ='' - silent put =g:magit_sections['commit_start'] + silent put =g:magit_sections.commit_start silent put ='Commit mode: '.commit_mode_str call mg_section_help('commit') - silent put =mg_underline(g:magit_sections['commit_start']) + silent put =magit#utils#underline(g:magit_sections.commit_start) silent put ='' - let git_dir=mg_git_dir() + let git_dir=magit#utils#git_dir() " refresh the COMMIT_EDITMSG file if ( s:magit_commit_mode == 'CC' ) - silent! call mg_system("GIT_EDITOR=/bin/false git commit -e 2> /dev/null") + silent! call magit#utils#system("GIT_EDITOR=/bin/false git commit -e 2> /dev/null") elseif ( s:magit_commit_mode == 'CA' ) - silent! call mg_system("GIT_EDITOR=/bin/false git commit --amend -e 2> /dev/null") + 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=mg_join_list(filter(readfile(git_dir . 'COMMIT_EDITMSG'), 'v:val !~ "^' . 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'] + put =g:magit_sections.commit_end endfunction " s:mg_comment_char: this function gets the commentChar from git config function! s:mg_comment_char() - silent! let git_result=mg_strip(mg_system("git config --get core.commentChar")) + silent! let git_result=magit#utils#strip( + \ magit#utils#system("git config --get core.commentChar")) if ( v:shell_error != 0 ) return '#' else @@ -560,8 +273,8 @@ function! s:mg_comment_char() endif endfunction -" s:mg_search_block: helper function, to get a block of text, giving a start -" and multiple end pattern +" s:mg_search_block: helper function, to get start and end line of a block, +" giving a start and multiple end pattern " a "pattern parameter" is a List: " @[0]: end pattern regex " @[1]: number of line to exclude above (negative), below (positive) or none (0) @@ -573,9 +286,7 @@ endfunction " (smallest region search) " param[in] upperlimit_pattern: regex of upper limit. If start_pattern line is " inferior to upper_limit line, block is discarded -" return: a list. -" @[0]: return status -" @[1]: List of selected block lines +" return: [startline, endline] function! s:mg_search_block(start_pattern, end_pattern, upper_limit_pattern) let l:winview = winsaveview() @@ -609,10 +320,9 @@ function! s:mg_search_block(start_pattern, end_pattern, upper_limit_pattern) throw "out_of_block" endif - let lines=getline(start, end) - call winrestview(l:winview) - return lines + + return [start,end] endfunction " s:mg_git_commit: commit staged stuff with message prepared in commit section @@ -626,19 +336,21 @@ endfunction " return no function! s:mg_git_commit(mode) abort if ( a:mode == 'CF' ) - silent let git_result=mg_system("git commit --amend -C HEAD") + silent let git_result=magit#utils#system("git commit --amend -C HEAD") else - let commit_section_pat_start='^'.g:magit_sections['commit_start'].'$' - let commit_section_pat_end='^'.g:magit_sections['commit_end'].'$' + let commit_section_pat_start='^'.g:magit_sections.commit_start.'$' + let commit_section_pat_end='^'.g:magit_sections.commit_end.'$' let commit_jump_line = 3 + mg_get_inline_help_line_nb('commit') - let commit_msg = mg_search_block( + let [start, end] = mg_search_block( \ [commit_section_pat_start, commit_jump_line], \ [ [commit_section_pat_end, -1] ], "") + let commit_msg = getline(start, end) let amend_flag="" if ( a:mode == 'CA' ) let amend_flag=" --amend " endif - silent! let git_result=mg_system("git commit " . amend_flag . " --file - ", commit_msg) + silent! let git_result=magit#utils#system( + \ "git commit " . amend_flag . " --file - ", commit_msg) endif if ( v:shell_error != 0 ) echoerr "Git error: " . git_result @@ -689,15 +401,17 @@ endfunction " header plus one or more hunks " return: no function! s:mg_git_apply(header, selection) - let selection = mg_flatten(a:header + a:selection) + let selection = magit#utils#flatten(a:header + a:selection) if ( selection[-1] !~ '^$' ) let selection += [ '' ] endif - silent let git_result=mg_system("git apply --no-index --cached -", selection) + let git_cmd="git apply --recount --no-index --cached -" + silent let git_result=magit#utils#system(git_cmd, selection) if ( v:shell_error != 0 ) echoerr "Git error: " . git_result + echoerr "Git cmd: " . git_cmd echoerr "Tried to aply this" - echoerr string(a:selection) + echoerr string(selection) endif endfunction @@ -712,16 +426,74 @@ function! s:mg_git_unapply(header, selection, mode) if ( a:mode == 'staged' ) let cached_flag=' --cached ' endif - let selection = mg_flatten(a:header + a:selection) + let selection = magit#utils#flatten(a:header + a:selection) if ( selection[-1] !~ '^$' ) let selection += [ '' ] endif - silent let git_result=mg_system("git apply --no-index " . cached_flag . " --reverse - ", selection) + silent let git_result=magit#utils#system( + \ "git apply --recount --no-index " . cached_flag . " --reverse - ", + \ selection) if ( v:shell_error != 0 ) echoerr "Git error: " . git_result echoerr "Tried to unaply this" - echoerr string(a:selection) + echoerr string(selection) + endif +endfunction + +" s:mg_create_diff_from_select: craft the diff to apply from a selection +" in a chunk +" remarks: it works with full lines, and can not span over multiple chunks +" param[in] select_lines: List containing all selected line numbers +" return: List containing the diff to apply, including the chunk header (must +" be applied with git apply --recount) +function! s:mg_create_diff_from_select(select_lines) + let start_select_line = a:select_lines[0] + let end_select_line = a:select_lines[-1] + let [starthunk,endhunk] = mg_select_hunk_block() + if ( start_select_line < starthunk || end_select_line > endhunk ) + throw 'out of hunk selection' endif + let section=mg_get_section() + let filename=mg_get_filename() + let hunks = s:state.get_file(section, filename).get_hunks() + for hunk in hunks + if ( hunk.header == getline(starthunk) ) + let current_hunk = hunk + break + endif + endfor + let selection = [] + call add(selection, current_hunk.header) + + let current_line = starthunk + 1 + for hunk_line in current_hunk.lines + if ( index(a:select_lines, current_line) != -1 ) + call add(selection, getline(current_line)) + elseif ( hunk_line =~ '^+.*' ) + " just ignore these lines + elseif ( hunk_line =~ '^-.*' ) + call add(selection, substitute(hunk_line, '^-\(.*\)$', ' \1', '')) + elseif ( hunk_line =~ '^ .*' ) + call add(selection, hunk_line) + else + throw 'visual selection error: ' . hunk_line + endif + let current_line += 1 + endfor + return selection +endfunction + +" s:mg_mark_lines_in_hunk: this function toggle marks for selected lines in a +" hunk. +" if a hunk contains marked lines, only these lines will be (un)staged on next +" (un)stage command +" param[in] start_select_line,end_select_line: limits of the selection +function! s:mg_mark_lines_in_hunk(start_select_line, end_select_line) + let [starthunk,endhunk] = mg_select_hunk_block() + if ( a:start_select_line < starthunk || a:end_select_line > endhunk ) + throw 'out of hunk selection' + endif + return magit#sign#toggle_signs('M', a:start_select_line, a:end_select_line) endfunction " s:mg_get_section: helper function to get the current section, according to @@ -744,6 +516,13 @@ function! s:mg_get_filename() return substitute(getline(search(g:magit_file_re, "cbnW")), g:magit_file_re, '\2', '') endfunction +" s:mg_get_hunkheader: helper function to get the current hunk header, +" according to cursor position +" return: hunk header +function! s:mg_get_hunkheader() + return getline(search(g:magit_hunk_re, "cbnW")) +endfunction + " }}} " {{{ User functions and commands @@ -774,9 +553,12 @@ function! magit#open_close_folding(...) let section=mg_get_section() " if first param is set, force visible to this value " else, toggle value - let s:mg_diff_dict[section][filename]['visible'] = - \ ( a:0 == 1 ) ? a:1 : - \ ( s:mg_diff_dict[section][filename]['visible'] == 0 ) ? 1 : 0 + let file = s:state.get_file(section, filename, 0) + if ( a:0 == 1 ) + call file.set_visible(a:1) + else + call file.toggle_visible() + endif call magit#update_buffer() endfunction @@ -788,8 +570,8 @@ endfunction " 4. fills with unstage stuff " 5. restore window state function! magit#update_buffer() - if ( @% != g:magit_unstaged_buffer_name ) - echoerr "Not in magit buffer " . g:magit_unstaged_buffer_name . " but in " . @% + if ( @% != g:magit_buffer_name ) + echoerr "Not in magit buffer " . g:magit_buffer_name . " but in " . @% return endif " FIXME: find a way to save folding state. According to help, this won't @@ -798,14 +580,30 @@ function! magit#update_buffer() " Playing with foldenable around does not help. " mkview does not help either. let l:winview = winsaveview() - silent! %d + + " remove all signs (needed as long as we wipe buffer) + call magit#sign#remove_all() + + " 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 mg_update_diff_dict() + 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() @@ -813,7 +611,7 @@ function! magit#update_buffer() call winrestview(l:winview) if ( s:magit_commit_mode != '' ) - let commit_section_pat_start='^'.g:magit_sections['commit_start'].'$' + let commit_section_pat_start='^'.g:magit_sections.commit_start.'$' silent! let section_line=search(commit_section_pat_start, "w") silent! call cursor(section_line+3+mg_get_inline_help_line_nb('commit'), 0) endif @@ -834,8 +632,8 @@ endfunction " 'v': vertical split " 'h': horizontal split " 'c': current buffer (should be used when opening vim in vimagit mode -function! magit#show_magit(display) - if ( mg_strip(system("git rev-parse --is-inside-work-tree")) != 'true' ) +function! magit#show_magit(display, ...) + if ( magit#utils#strip(system("git rev-parse --is-inside-work-tree")) != 'true' ) echoerr "Magit must be started from a git repository" return endif @@ -848,20 +646,35 @@ function! magit#show_magit(display) else throw 'parameter_error' endif + + 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 + + if ( a:0 > 0 ) + let b:magit_default_show_all_files = a:1 + endif + if ( a:0 > 1 ) + let b:magit_default_fold_level = a:2 + endif + + silent! execute "bdelete " . g:magit_buffer_name + execute "file " . g:magit_buffer_name + setlocal buftype=nofile setlocal bufhidden=delete setlocal noswapfile setlocal foldmethod=syntax - setlocal foldlevel=1 + let &l:foldlevel = b:magit_default_fold_level setlocal filetype=magit "setlocal readonly - silent! execute "bdelete " . g:magit_unstaged_buffer_name - execute "file " . g:magit_unstaged_buffer_name + call magit#utils#setbufnr(bufnr(g:magit_buffer_name)) + call magit#sign#init() execute "nnoremap " . g:magit_stage_file_mapping . " :call magit#stage_file()" - execute "nnoremap " . g:magit_stage_hunk_mapping . " :call magit#stage_hunk()" - execute "nnoremap " . g:magit_discard_hunk_mapping . " :call magit#discard_hunk()" + execute "nnoremap " . g:magit_stage_hunk_mapping . " :call magit#stage_hunk(0)" + execute "nnoremap " . g:magit_discard_hunk_mapping . " :call magit#stage_hunk(1)" execute "nnoremap " . g:magit_reload_mapping . " :call magit#update_buffer()" execute "cnoremap " . g:magit_commit_mapping_command." :call magit#commit_command('CC')" execute "nnoremap " . g:magit_commit_mapping . " :call magit#commit_command('CC')" @@ -870,6 +683,13 @@ function! magit#show_magit(display) execute "nnoremap " . g:magit_ignore_mapping . " :call magit#ignore_file()" execute "nnoremap " . g:magit_close_mapping . " :close" execute "nnoremap " . g:magit_toggle_help_mapping . " :call magit#toggle_help()" + + execute "nnoremap " . g:magit_stage_line_mapping . " :call magit#stage_vselect()" + execute "xnoremap " . g:magit_stage_hunk_mapping . " :call magit#stage_vselect()" + + execute "nnoremap " . g:magit_mark_line_mapping . " :call magit#mark_vselect()" + execute "xnoremap " . g:magit_mark_line_mapping . " :call magit#mark_vselect()" + for mapping in g:magit_folding_toggle_mapping " trick to pass '' in a mapping command without being interpreted let func_arg = ( mapping ==? "" ) ? '+' : mapping @@ -891,74 +711,59 @@ function! s:mg_select_closed_file() let list = matchlist(getline("."), g:magit_file_re) let filename = list[2] let section=mg_get_section() - if ( has_key(s:mg_diff_dict[section], filename) && - \ ( s:mg_diff_dict[section][filename]['visible'] == 0 ) ) - let selection = mg_diff_dict_get_hunks(section, filename) + + let file = s:state.get_file(section, filename) + if ( file.is_visible() == 0 || + \ file.is_dir() == 1 ) + let selection = s:state.get_file(section, filename).get_flat_hunks() return selection endif endif throw "out_of_block" endfunction -" maagit#stage_block: this function (un)stage a block, according to parameter +" magit#stage_block: this function (un)stage a block, according to parameter " INFO: in unstaged section, it stages the hunk, and in staged section, it " unstages the hunk " param[in] block_type: can be 'file' or 'hunk' " param[in] discard: boolean, if true, discard instead of (un)stage " return: no -function! magit#stage_block(block_type, discard) abort - try - let selection = mg_select_closed_file() - catch 'out_of_block' - if ( a:block_type == 'hunk') - try - let selection = mg_select_hunk_block() - catch 'out_of_block' - let selection = mg_select_file_block() - endtry - else - let selection = mg_select_file_block() - endif - endtry - +function! magit#stage_block(selection, discard) abort let section=mg_get_section() let filename=mg_get_filename() - let header = mg_diff_dict_get_header(section, filename) - + let header = s:state.get_file(section, filename).get_header() + + let file = s:state.get_file(section, filename, 0) if ( a:discard == 0 ) if ( section == 'unstaged' ) - if ( s:mg_diff_dict[section][filename]['empty'] == 1 || - \ s:mg_diff_dict[section][filename]['symlink'] != '' || - \ s:mg_diff_dict[section][filename]['binary'] == 1 ) - call mg_system('git add ' . mg_add_quotes(filename)) + if ( file.must_be_added() ) + call magit#utils#system('git add ' . + \ magit#utils#add_quotes(filename)) else - call mg_git_apply(header, selection) + call mg_git_apply(header, a:selection) endif elseif ( section == 'staged' ) - if ( s:mg_diff_dict[section][filename]['empty'] == 1 || - \ s:mg_diff_dict[section][filename]['symlink'] != '' || - \ s:mg_diff_dict[section][filename]['binary'] == 1 ) - call mg_system('git reset ' . mg_add_quotes(filename)) + if ( file.must_be_added() ) + call magit#utils#system('git reset ' . + \ magit#utils#add_quotes(filename)) else - call mg_git_unapply(header, selection, 'staged') + call mg_git_unapply(header, a:selection, 'staged') endif else echoerr "Must be in \"" . - \ g:magit_sections['staged'] . "\" or \"" . - \ g:magit_sections['unstaged'] . "\" section" + \ g:magit_sections.staged . "\" or \"" . + \ g:magit_sections.unstaged . "\" section" endif else if ( section == 'unstaged' ) - if ( s:mg_diff_dict[section][filename]['empty'] == 1 || - \ s:mg_diff_dict[section][filename]['symlink'] != '' || - \ s:mg_diff_dict[section][filename]['binary'] == 1 ) + if ( file.must_be_added() ) call delete(filename) else - call mg_git_unapply(header, selection, 'unstaged') + call mg_git_unapply(header, a:selection, 'unstaged') endif else echoerr "Must be in \"" . - \ g:magit_sections['unstaged'] . "\" section" + \ g:magit_sections.unstaged . "\" section" endif endif @@ -971,31 +776,74 @@ endfunction " unstages the file " return: no function! magit#stage_file() - return magit#stage_block('file', 0) + try + let selection = mg_select_closed_file() + catch 'out_of_block' + let [start, end] = mg_select_file_block() + let selection = getline(start, end) + endtry + return magit#stage_block(selection, 0) endfunction " -" magit#stage_hunk: this function (un)stage a hunk, from the current +" magit#stage_hunk: this function (un)stage/discard a hunk, from the current " cursor position " INFO: in unstaged section, it stages the hunk, and in staged section, it " unstages the hunk +" param[in] discard: +" - when set to 0, (un)stage +" - when set to 1, discard " return: no -function! magit#stage_hunk() - return magit#stage_block('hunk', 0) +function! magit#stage_hunk(discard) + try + let selection = mg_select_closed_file() + catch 'out_of_block' + try + let [start,end] = mg_select_hunk_block() + catch 'out_of_block' + let [start,end] = mg_select_file_block() + endtry + let marked_lines = magit#sign#find_stage_signs(start, end) + if ( empty(marked_lines) ) + let selection = getline(start, end) + else + let selection = mg_create_diff_from_select( + \ map(keys(marked_lines), 'str2nr(v:val)')) + call magit#sign#remove_signs(marked_lines) + endif + endtry + return magit#stage_block(selection, a:discard) endfunction -" magit#discard_hunk: this function discard a single hunk, from the current -" cursor position -" INFO: only works in unstaged section +" magit#stage_vselect: this function (un)stage text being sectected in Visual +" mode +" remarks: it works with full lines, and can not span over multiple chunks +" INFO: in unstaged section, it stages the file, and in staged section, it +" unstages the file " return: no -function! magit#discard_hunk() - return magit#stage_block('hunk', 1) +function! magit#stage_vselect() range + " func-range a:firstline a:lastline seems to work at least from vim 7.2 + let lines = [] + let curline = a:firstline + while ( curline <= a:lastline ) + call add(lines, curline) + let curline += 1 + endwhile + let selection = mg_create_diff_from_select(lines) + return magit#stage_block(selection, 0) +endfunction + +" magit#mark_vselect: wrapper function to mark selected lines (see +" mg_mark_lines_in_hunk) +function! magit#mark_vselect() range + return mg_mark_lines_in_hunk(a:firstline, a:lastline) endfunction " magit#ignore_file: this function add the file under cursor to .gitignore " FIXME: git diff adds some strange characters to end of line function! magit#ignore_file() abort let ignore_file=mg_get_filename() - call mg_append_file(mg_top_dir() . ".gitignore", [ ignore_file ] ) + call magit#utils#append_file(magit#utils#top_dir() . ".gitignore", + \ [ ignore_file ] ) call magit#update_buffer() endfunction diff --git a/test/addDir.vader b/test/addDir.vader new file mode 100644 index 0000000..75de58f --- /dev/null +++ b/test/addDir.vader @@ -0,0 +1,69 @@ +Include: setup.inc + +Execute (Stage untracked directory closed): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(0) + call Cursor_position() + call magit#stage_file() + call Cd_test() + let diff=Git_diff('staged') + call Expect_diff(g:test_script_dir . 'addDir/addDir_all_diff.expect', diff) + call Git_cmd("git reset") + +Execute (Stage untracked directory opened): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(1) + call Cursor_position() + call magit#stage_file() + call Cd_test() + let diff=Git_diff('staged') + call Expect_diff(g:test_script_dir . 'addDir/addDir_all_diff.expect', diff) + call Git_cmd("git reset") + +Execute (Stage untracked hidden file): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(1) + call Search_pattern(Get_filename() . '.hidden') + call Cursor_position() + call magit#stage_file() + call Cd_test() + let diff=Git_diff('staged') + call Expect_diff(g:test_script_dir . 'addDir/addDir_hidden_diff.expect', diff) + call Git_cmd("git reset") + +Execute (Stage untracked subdir file): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(1) + call Search_pattern(Get_filename() . 'newsubdir') + call Cursor_position() + call magit#stage_file() + call Cd_test() + let diff=Git_diff('staged') + call Expect_diff(g:test_script_dir . 'addDir/addDir_subdir_diff.expect', diff) + call Git_cmd("git reset") + +Execute (Stage untracked subsubfile): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(1) + call Search_pattern(Get_filename() . 'newsubdir') + call magit#open_close_folding(1) + call Search_pattern(Get_filename() . 'newsubdir/e') + call Cursor_position() + call magit#stage_file() + call Cd_test() + let diff=Git_diff('staged') + call Expect_diff(g:test_script_dir . 'addDir/addDir_subsubfile_diff.expect', diff) + call Git_cmd("git reset") + +Include: cleanup.inc + diff --git a/test/addDir/addDir_all_diff.expect b/test/addDir/addDir_all_diff.expect new file mode 100644 index 0000000..1b81b04 --- /dev/null +++ b/test/addDir/addDir_all_diff.expect @@ -0,0 +1,42 @@ +diff --git newdir/.hidden newdir/.hidden +new file mode 100644 +--- /dev/null ++++ newdir/.hidden +@@ -0,0 +1 @@ ++this is an hidden file +diff --git newdir/a newdir/a +new file mode 100644 +--- /dev/null ++++ newdir/a +@@ -0,0 +1 @@ ++a +diff --git newdir/b newdir/b +new file mode 100644 +--- /dev/null ++++ newdir/b +@@ -0,0 +1 @@ ++b +diff --git newdir/c newdir/c +new file mode 100644 +--- /dev/null ++++ newdir/c +@@ -0,0 +1 @@ ++c +diff --git newdir/newsubdir/d newdir/newsubdir/d +new file mode 100644 +--- /dev/null ++++ newdir/newsubdir/d +@@ -0,0 +1 @@ ++d +diff --git newdir/newsubdir/e newdir/newsubdir/e +new file mode 100644 +--- /dev/null ++++ newdir/newsubdir/e +@@ -0,0 +1 @@ ++e +diff --git newdir/newsubdir/f newdir/newsubdir/f +new file mode 100644 +--- /dev/null ++++ newdir/newsubdir/f +@@ -0,0 +1 @@ ++f diff --git a/test/addDir/addDir_hidden_diff.expect b/test/addDir/addDir_hidden_diff.expect new file mode 100644 index 0000000..5168c5a --- /dev/null +++ b/test/addDir/addDir_hidden_diff.expect @@ -0,0 +1,6 @@ +diff --git newdir/.hidden newdir/.hidden +new file mode 100644 +--- /dev/null ++++ newdir/.hidden +@@ -0,0 +1 @@ ++this is an hidden file diff --git a/test/addDir/addDir_subdir_diff.expect b/test/addDir/addDir_subdir_diff.expect new file mode 100644 index 0000000..f44ecbd --- /dev/null +++ b/test/addDir/addDir_subdir_diff.expect @@ -0,0 +1,18 @@ +diff --git newdir/newsubdir/d newdir/newsubdir/d +new file mode 100644 +--- /dev/null ++++ newdir/newsubdir/d +@@ -0,0 +1 @@ ++d +diff --git newdir/newsubdir/e newdir/newsubdir/e +new file mode 100644 +--- /dev/null ++++ newdir/newsubdir/e +@@ -0,0 +1 @@ ++e +diff --git newdir/newsubdir/f newdir/newsubdir/f +new file mode 100644 +--- /dev/null ++++ newdir/newsubdir/f +@@ -0,0 +1 @@ ++f diff --git a/test/addDir/addDir_subsubfile_diff.expect b/test/addDir/addDir_subsubfile_diff.expect new file mode 100644 index 0000000..a62cdbd --- /dev/null +++ b/test/addDir/addDir_subsubfile_diff.expect @@ -0,0 +1,6 @@ +diff --git newdir/newsubdir/e newdir/newsubdir/e +new file mode 100644 +--- /dev/null ++++ newdir/newsubdir/e +@@ -0,0 +1 @@ ++e diff --git a/test/addFile.vader b/test/addFile.vader index 759b2a4..d538201 100644 --- a/test/addFile.vader +++ b/test/addFile.vader @@ -74,7 +74,7 @@ Execute (Stage untracked file with magit#stage_hunk at closed file header): call Search_file('unstaged') call magit#open_close_folding(0) call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_diff('staged', Get_filename()) call Expect_diff(g:test_script_dir . 'addFile/addFile_' . Get_safe_filename() . '_file_diff.expect', diff) @@ -85,7 +85,7 @@ Execute (Unstage file with magit#stage_hunk at closed file header): call Search_file('staged') call magit#open_close_folding(0) call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_status(Get_filename()) call Expect_diff(g:test_script_dir . 'addFile/addFile_' . Get_safe_filename() . '_unstaged_status.expect', diff) @@ -96,7 +96,7 @@ Execute (Stage untracked file with magit#stage_hunk at file header): call Search_file('unstaged') call magit#open_close_folding(1) call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_diff('staged', Get_filename()) call Expect_diff(g:test_script_dir . 'addFile/addFile_' . Get_safe_filename() . '_file_diff.expect', diff) @@ -107,7 +107,7 @@ Execute (Unstage file with magit#stage_hunk at file header): call Search_file('staged') call magit#open_close_folding(1) call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_status(Get_filename()) call Expect_diff(g:test_script_dir . 'addFile/addFile_' . Get_safe_filename() . '_unstaged_status.expect', diff) diff --git a/test/addHunk.vader b/test/addHunk.vader index a966608..549df8d 100644 --- a/test/addHunk.vader +++ b/test/addHunk.vader @@ -7,7 +7,7 @@ Execute (Stage untracked file with magit#stage_hunk on start hunk): call magit#open_close_folding(1) /^@@ call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_diff('staged', Get_filename()) call Expect_diff(g:test_script_dir . 'addHunk/addHunk_' . Get_safe_filename() . '_hunk_diff.expect', diff) @@ -19,7 +19,7 @@ Execute (Unstage untracked file with magit#stage_hunk on start hunk): call magit#open_close_folding(1) /^@@ call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_status(Get_filename()) call Expect_diff(g:test_script_dir . 'addHunk/addHunk_' . Get_safe_filename() . '_unstaged_status.expect', diff) @@ -32,7 +32,7 @@ Execute (Stage untracked file with magit#stage_hunk on end hunk): /^@@ call Move_relative(+3) call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_diff('staged', Get_filename()) call Expect_diff(g:test_script_dir . 'addHunk/addHunk_' . Get_safe_filename() . '_hunk_diff.expect', diff) @@ -45,7 +45,7 @@ Execute (Untage untracked file with magit#stage_hunk on end hunk): /^@@ call Move_relative(+3) call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_status(Get_filename()) call Expect_diff(g:test_script_dir . 'addHunk/addHunk_' . Get_safe_filename() . '_unstaged_status.expect', diff) @@ -58,7 +58,7 @@ Execute (Stage untracked file with magit#stage_hunk modified hunk): /^+ call setline(line('.'), substitute(getline('.'), '^\(+.*\) .\{-\}$', '\1 vimatest', '')) call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_diff('staged', Get_filename()) call Expect_diff(g:test_script_dir . 'addHunk/addHunk_' . Get_safe_filename() . '_modified_hunk_diff.expect', diff) @@ -70,7 +70,7 @@ Execute (Unstage untracked file with magit#stage_hunk): call magit#open_close_folding(1) /^@@ call Cursor_position() - call magit#stage_hunk() + call magit#stage_hunk(0) call Cd_test() let diff=Git_status(Get_filename()) call Expect_diff(g:test_script_dir . 'addHunk/addHunk_' . Get_safe_filename() . '_unstaged_status.expect', diff) diff --git a/test/addSelect.vader b/test/addSelect.vader new file mode 100644 index 0000000..4a2e9e4 --- /dev/null +++ b/test/addSelect.vader @@ -0,0 +1,59 @@ +Include: setup.inc + +Execute (Stage untracked file with magit#stage_hunk on start hunk (this hunk will stay staged all test)): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(1) + call Search_pattern("^@@ ") + call Cursor_position() + call magit#stage_hunk(0) + call Cd_test() + let diff=Git_diff('staged', Get_filename()) + call Expect_diff(g:test_script_dir . 'addSelect/addSelect_' . Get_safe_filename() . '_1_hunk_diff.expect', diff) + +Execute (Stage selection): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(1) + %foldopen! + call Search_pattern("^+\t\tif product.cover_url is not '':$") + execute "normal! v4j:call magit#stage_vselect()\" + call Cd_test() + let diff=Git_diff('staged', Get_filename()) + call Expect_diff(g:test_script_dir . 'addSelect/addSelect_' . Get_safe_filename() . '_2_vselect_diff.expect', diff) + +Execute (Stage lines): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(1) + %foldopen! + call Search_pattern("^-\teisbn = models.CharField(max_length=13, blank=True)$") + call magit#stage_vselect() + %foldopen! + call Search_pattern("^+\teisbn = models.CharField(max_length=13, blank=True, null=True)$") + call magit#stage_vselect() + call Cd_test() + let diff=Git_diff('staged', Get_filename()) + call Expect_diff(g:test_script_dir . 'addSelect/addSelect_' . Get_safe_filename() . '_3_lines_diff.expect', diff) + +Execute (Stage marks): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(1) + %foldopen! + call Search_pattern("^+def upload_path(book, filename):$") + execute "normal! v3j:call magit#mark_vselect()\" + call Search_pattern("^-\tedition = models.CharField(max_length=200, blank=True)$") + call magit#mark_vselect() + call Search_pattern("+\tedition = models.CharField(max_length=200, blank=True, null=True)$") + call magit#mark_vselect() + call magit#stage_hunk(0) + call Cd_test() + let diff=Git_diff('staged', Get_filename()) + call Expect_diff(g:test_script_dir . 'addSelect/addSelect_' . Get_safe_filename() . '_4_marks_diff.expect', diff) + +Include: cleanup.inc diff --git a/test/addSelect/addSelect_books_models_py_1_hunk_diff.expect b/test/addSelect/addSelect_books_models_py_1_hunk_diff.expect new file mode 100644 index 0000000..4d9138a --- /dev/null +++ b/test/addSelect/addSelect_books_models_py_1_hunk_diff.expect @@ -0,0 +1,13 @@ +diff --git books/models.py books/models.py +--- books/models.py ++++ books/models.py +@@ -1,5 +1,9 @@ + from django.db import models + ++import urllib2 as urllib ++from PIL import Image ++import io ++ + import isbn_search + + class User(models.Model): diff --git a/test/addSelect/addSelect_books_models_py_2_vselect_diff.expect b/test/addSelect/addSelect_books_models_py_2_vselect_diff.expect new file mode 100644 index 0000000..313e368 --- /dev/null +++ b/test/addSelect/addSelect_books_models_py_2_vselect_diff.expect @@ -0,0 +1,24 @@ +diff --git books/models.py books/models.py +--- books/models.py ++++ books/models.py +@@ -1,5 +1,9 @@ + from django.db import models + ++import urllib2 as urllib ++from PIL import Image ++import io ++ + import isbn_search + + class User(models.Model): +@@ -22,6 +26,10 @@ class BookManager(models.Manager): + title = product.title, + edition = product.edition + ) ++ if product.cover_url is not '': ++ cover_img_bin = urllib.urlopen(product.cover_url) ++ cover_img_file = Image.open(io.BytesIO(cover_img_bin.read())) ++ book.cover_img.save("cover.jpg", cover_img_file) + return book + + class Book(models.Model): diff --git a/test/addSelect/addSelect_books_models_py_3_lines_diff.expect b/test/addSelect/addSelect_books_models_py_3_lines_diff.expect new file mode 100644 index 0000000..0698fb6 --- /dev/null +++ b/test/addSelect/addSelect_books_models_py_3_lines_diff.expect @@ -0,0 +1,30 @@ +diff --git books/models.py books/models.py +--- books/models.py ++++ books/models.py +@@ -1,5 +1,9 @@ + from django.db import models + ++import urllib2 as urllib ++from PIL import Image ++import io ++ + import isbn_search + + class User(models.Model): +@@ -22,11 +26,15 @@ class BookManager(models.Manager): + title = product.title, + edition = product.edition + ) ++ if product.cover_url is not '': ++ cover_img_bin = urllib.urlopen(product.cover_url) ++ cover_img_file = Image.open(io.BytesIO(cover_img_bin.read())) ++ book.cover_img.save("cover.jpg", cover_img_file) + return book + + class Book(models.Model): + isbn = models.CharField(max_length=10, unique=True) +- eisbn = models.CharField(max_length=13, blank=True) ++ eisbn = models.CharField(max_length=13, blank=True, null=True) + title = models.CharField(max_length=200) + author = models.CharField(max_length=200) + edition = models.CharField(max_length=200, blank=True) diff --git a/test/addSelect/addSelect_books_models_py_4_marks_diff.expect b/test/addSelect/addSelect_books_models_py_4_marks_diff.expect new file mode 100644 index 0000000..e4dfb93 --- /dev/null +++ b/test/addSelect/addSelect_books_models_py_4_marks_diff.expect @@ -0,0 +1,38 @@ +diff --git books/models.py books/models.py +--- books/models.py ++++ books/models.py +@@ -1,5 +1,9 @@ + from django.db import models + ++import urllib2 as urllib ++from PIL import Image ++import io ++ + import isbn_search + + class User(models.Model): +@@ -22,15 +26,22 @@ class BookManager(models.Manager): + title = product.title, + edition = product.edition + ) ++ if product.cover_url is not '': ++ cover_img_bin = urllib.urlopen(product.cover_url) ++ cover_img_file = Image.open(io.BytesIO(cover_img_bin.read())) ++ book.cover_img.save("cover.jpg", cover_img_file) + return book + ++def upload_path(book, filename): ++ return 'covers/%s/%s' % (book.isbn, filename) ++ + class Book(models.Model): + isbn = models.CharField(max_length=10, unique=True) +- eisbn = models.CharField(max_length=13, blank=True) ++ eisbn = models.CharField(max_length=13, blank=True, null=True) + title = models.CharField(max_length=200) + author = models.CharField(max_length=200) +- edition = models.CharField(max_length=200, blank=True) + cover = models.ImageField(upload_to='covers', blank=True) ++ edition = models.CharField(max_length=200, blank=True, null=True) + def __unicode__(self): + return u'[%s] "%s" by %s' % (self.isbn, self.title, self.author,) + diff --git a/test/addSubmodule.vader b/test/addSubmodule.vader new file mode 100644 index 0000000..3b52b06 --- /dev/null +++ b/test/addSubmodule.vader @@ -0,0 +1,28 @@ +Include: setup.inc + +Execute (Stage untracked directory closed): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(0) + call Cursor_position() + call magit#stage_file() + call Cd_test() + let diff=Git_status(Get_filename()) + call Expect_diff(g:test_script_dir . 'addSubmodule/addSubmodule_status.expect', diff) + call Git_cmd("git reset") + +Execute (Stage untracked directory opened): + call Cd_test_sub() + Magit + call Search_file('unstaged') + call magit#open_close_folding(1) + call Move_relative(+4) + call Cursor_position() + call magit#stage_file() + call Cd_test() + let diff=Git_status(Get_filename()) + call Expect_diff(g:test_script_dir . 'addSubmodule/addSubmodule_status.expect', diff) + call Git_cmd("git reset") + +Include: cleanup.inc diff --git a/test/addSubmodule/addSubmodule_status.expect b/test/addSubmodule/addSubmodule_status.expect new file mode 100644 index 0000000..99fc911 --- /dev/null +++ b/test/addSubmodule/addSubmodule_status.expect @@ -0,0 +1 @@ +M subdjooks diff --git a/test/ignoreFile.vader b/test/ignoreFile.vader index c0dc1f0..2475e91 100644 --- a/test/ignoreFile.vader +++ b/test/ignoreFile.vader @@ -15,7 +15,7 @@ Execute (Ignore untracked file at closed file header): call search('^modified: .gitignore$') call magit#open_close_folding(0) call Cursor_position() - call magit#discard_hunk() + call magit#stage_hunk(1) call magit#update_buffer() Assert (Search_file('unstaged') != 0), $VIMAGIT_TEST_FILENAME . 'should not be ignored anymore' @@ -34,7 +34,7 @@ Execute (Ignore untracked file at opened file header): call search('^modified: .gitignore$') call magit#open_close_folding(1) call Cursor_position() - call magit#discard_hunk() + call magit#stage_hunk(1) call magit#update_buffer() Assert (Search_file('unstaged') != 0), $VIMAGIT_TEST_FILENAME . 'should not be ignored anymore' @@ -55,7 +55,7 @@ Execute (Ignore untracked file at hunk position): call magit#open_close_folding(1) call Move_relative(+1) call Cursor_position() - call magit#discard_hunk() + call magit#stage_hunk(1) call magit#update_buffer() Assert (Search_file('unstaged') != 0), $VIMAGIT_TEST_FILENAME . 'should not be ignored anymore' diff --git a/test/run.sh b/test/run.sh index 4054ace..ded972c 100755 --- a/test/run.sh +++ b/test/run.sh @@ -26,7 +26,8 @@ fi pushd $TEST_PATH git config --local user.email 'tester@vimagit.org' git config --local user.name 'vimagit tester' -export TEST_HEAD_SHA1='8e589e4' +export TEST_HEAD_SHA1='origin/vimagit_test-1.4' +git submodule update git show $TEST_HEAD_SHA1 --stat git reset $TEST_HEAD_SHA1~1 && git status --porcelain && git reset --hard $TEST_HEAD_SHA1 popd @@ -39,6 +40,9 @@ else VIM=vim fi +echo 'Git version' +git --version + echo 'Vim version' $VIM --version @@ -74,7 +78,7 @@ for script in ${!test_scripts[@]}; do set rtp+=$VADER_PATH filetype plugin indent on syntax enable -EOF) -c "Vader! $VIMAGIT_PATH/test/$script" +EOF) -c "Vader! $VIMAGIT_PATH/test/$script 2> >(sed -n '/^Starting Vader/,$p')" done done diff --git a/test/setup.inc b/test/setup.inc index 3b1816e..bb56adc 100644 --- a/test/setup.inc +++ b/test/setup.inc @@ -1,6 +1,6 @@ Execute (setup): source $VIMAGIT_PATH/test/utils.vim call Cd_test() - call system("git reset --mixed " . $TEST_HEAD_SHA1 . "~1") + call system("git reset " . $TEST_HEAD_SHA1 . "~1") call Git_verbose_log(system("git status --porcelain")) call Cd_test_sub() diff --git a/test/test.config b/test/test.config index 4178717..e137682 100644 --- a/test/test.config +++ b/test/test.config @@ -2,6 +2,9 @@ declare -a test_paths=(./ ./books/templates/) declare -A test_scripts=( [addFile.vader]='bootstrap;books/models.py;bootstrap.lnk;empty_file;bootstrap\ with\ spaces;bootstrap\ with\ spaces.lnk;empty_file\ with\ spaces' [addHunk.vader]='bootstrap;books/models.py' + [addSelect.vader]='books/models.py' [renameFile.vader]='manage.py|manage\ with\ spaces.py;djooks/settings\ with\ spaces.py|djooks/settings_without_spaces.py' [ignoreFile.vader]='bootstrap' + [addDir.vader]='newdir\/' +#[addSubmodule.vader]='subdjooks' ) diff --git a/test/utils.vim b/test/utils.vim index 975b4fa..78cc63d 100644 --- a/test/utils.vim +++ b/test/utils.vim @@ -77,10 +77,16 @@ function! Git_add_quotes(filename) endfunction " helper function to get the diff of a file, in staged or unstaged mode -function! Git_diff(state, file) +function! Git_diff(state, ...) let staged_flag = ( a:state == 'staged' ) ? ' --staged ' : '' + if ( a:0 == 1 ) + let file = " -- " . Git_add_quotes(a:1) + else + let file = "" + endif + let diff_cmd="git diff --no-color --no-ext-diff --src-prefix='' --dst-prefix='' " . - \ staged_flag . " -- " . Git_add_quotes(a:file) . + \ staged_flag . file . \ " | \\grep -v " . g:index_regex return Git_cmd(diff_cmd) endfunction @@ -150,10 +156,15 @@ function! Search_file(mode, ...) call Git_verbose_log('Search mode: "' . a:mode . '" => ' . getline('.')) let pattern='^.*: ' . call('Get_filename', a:000) . '\%( -> .*\)\?$' let ret = search(pattern) - call Git_verbose_log('Search: "' . pattern . '" => ' . getline('.')) + call Git_verbose_log('Search: "' . pattern . '" => ' . getline('.') . ' @line' . line('.')) return ret endfunction +function! Search_pattern(pattern) + let ret = search(a:pattern) + call Git_verbose_log('Search: "' . a:pattern . '" => ' . getline('.') . ' @line' . line('.')) +endfunction + " get a safe to use string of filename we curently test (for golden files) function! Get_safe_filename(...) return substitute(call('Get_filename', a:000), '[/. ]', '_', 'g')