diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4713993..20deffc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,45 +8,26 @@ jobs: matrix: include: - vim_type: "Vim" - vim_version: "v8.2.2277" - vspec_vim: "vim" + vim_version: "v8.2.4212" + lua_version: "luajit-2.1.0-beta3" + nvim: "false" - vim_type: "Neovim" vim_version: "stable" - vspec_vim: "nvim" + nvim: "true" steps: - name: "checkout" uses: "actions/checkout@v2" with: fetch-depth: 5 + - name: "install lua" + uses: "leafo/gh-actions-lua@v8.0.0" + with: + luaVersion: "${{ matrix.lua_version }}" + if: "${{ matrix.lua_version }}" - name: "install ${{ matrix.vim_type }}" uses: "thinca/action-setup-vim@v1" with: vim_version: "${{ matrix.vim_version }}" vim_type: "${{ matrix.vim_type }}" - - name: "install ruby" - uses: "ruby/setup-ruby@v1" - with: - ruby-version: "3.0" - bundler-cache: true - name: "run tests" - run: | - ${{ matrix.vspec_vim }} --version - script -qec " VSPEC_VIM=${{ matrix.vspec_vim }} rake test" - lint: - runs-on: "ubuntu-18.04" - steps: - - name: "checkout" - uses: "actions/checkout@v2" - - uses: actions/setup-python@v2 - with: - python-version: "3.x" - - name: "run vint" - # Must remove syntax before linting - # https://github.com/vim-jp/vim-vimlparser/issues/98 - # Must remove tests before linting - # https://github.com/Vimjas/vint/issues/330 - run: | - sudo python3 -m pip install vim-vint - sed -i 's/^syntax .*//' syntax/floggraph.vim - rm -rf t/ - vint --color . + run: "NVIM=${{ matrix.nvim }} ./t/run.sh" diff --git a/.gitignore b/.gitignore index 61bf44b..d59ae0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ tags -.vim-flavor +.test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e02abf1..a6683b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,25 @@ # Contributing The idea behind this plugin is to provide a fully featured Vim branch viewer as light as possible. -Take advantage of Vim and fugitive's builtin features instead of recreating them. +Take advantage of Vim and Fugitive's builtin features instead of recreating them whenever possible. Let users customize how they use Flog so they can use their personal workflows and preferred interface. ## Running Tests -Ensure you have `ruby` installed and run `rake test`. -Tests are located at `t/*.vim` and use [vim-flavor](https://github.com/kana/vim-flavor). +**Vim**: + +``` +./t/run.sh +``` + +**Neovim**: + +``` +NVIM=true ./t/run.sh +``` ## Pull Request Process 1. Ensure changes to the code are simple, iterative, and consistent. -2. Run [vint](https://github.com/Kuniwak/vint) in the root directory and make any necessary changes. -3. Keep documentation easy to read, concise, and up-to-date. +2. Keep documentation easy to read, concise, and up-to-date. +3. Add/update tests if appropriate. diff --git a/EXAMPLES.md b/EXAMPLES.md index b63cff4..9bc8b34 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -2,45 +2,50 @@ ## Checking Out a Branch -1. Launch the graph with `:Flog` (if this runs slowly for you, see [FAQ](FAQ.md)). -2. Make sure your commit is in the graph by pressing `a` to toggle showing all commits. -3. Navigate to your branch. There are a few ways to do this: - - Use builtin VIM navigation like `/`, `j`, `k`, etc. +1. Open the git branch graph with `:Flog` (if this runs slowly for you, see [FAQ](FAQ.md)). +2. Make sure your commit is in the git branch graph by pressing `a` to toggle showing all commits. +3. Navigate to your branch. There are a couple ways to do this: + - Use builtin Vim navigation like `/`, `j`, `k`, etc. - Use `]r`/`[r` to jump between commits with refs. - - Use `:Flogjump` to jump towards the commit with completion. 4. Checkout the branch. There are also a few ways to do this: - - Use `:Floggit checkout `. This will complete the commit name. - - Use the `git` mapping to prepopulate the command line with `:Floggit`, or use `co` for `:Floggit checkout`. - Use `cob` to checkout the first local branch name, or remote branch if it is not available. - - Use `cot` to checkout the first branch name, setting it up to be tracked locally if it is a remote branch. + - Use `col` to checkout the first branch name, setting it up to be tracked locally if it is a remote branch. + - Use `:Floggit checkout `. + - Use the `git` mapping to prepopulate the command line with `:Floggit`, or use `co` for `:Floggit checkout`. + - Using `:Floggit` lets you use completion for: + - Options for git commands. + - Git objects. + - Contextual Flog items, such as branch names on the current line. ## Adding Default Arguments -Put this inside of your `.vimrc` to always launch Flog with the `-all` and `-max-count=2000` options: +Put this inside of your `.vimrc` to always launch Flog with the `-no-merges` and `-max-count=2000` options: ```vim -let g:flog_default_arguments = { +let g:flog_default_opts = { \ 'max_count': 2000, - \ 'all': 1, + \ 'merges': 0, \ } ``` -You can use `:Flogsetargs` after the graph has launched to override these options: +You can use `:Flogsetargs` after the git branch graph has launched to override these options: ``` # Clear the max count Flogsetargs -max-count= # Increase the max count to 3000 Flogsetargs -max-count=3000 +# Remove -no-merges +Flogsetargs -merges # Clear out options Flogsetargs! ``` -If you don't want options to be cleared when you run `:Flogsetargs!` you can use `g:flog_permanent_default_arguments`. +If you don't want options to be cleared when you run `:Flogsetargs!` you can use `g:flog_permanent_default_opts`. For example, if you want to always use the short date format: ```vim -let g:flog_permanent_default_arguments = { +let g:flog_permanent_default_opts = { \ 'date': 'short', \ } ``` @@ -48,36 +53,40 @@ let g:flog_permanent_default_arguments = { ## Diffing Commits There are several different ways to diff commits after launching Flog: - - Press `dd` in normal mode to diff the commit under the cursor. - - Visually select the commits and use `:Floggit diff ` to complete the commits at the beginning and end of the selection. + - Press `dd` in normal mode to diff the commit under the cursor with `HEAD`. + - Visually select the commits and use `:Floggit -s diff ` to complete the commits at the beginning and end of the selection. - Press `dd` in visual mode to diff the commits at the beginning and end of the selection + - Press `d!` to diff the commit at the cursor and the commit that was previously opened with ``. ## Extension Example: Switch Diff Order -Instead of trying to provide settings for everything, Flog provides utility functions for customization. +Flog has functions that allow you to easily define your own mappings and commands. This example shows how to switch the order of commits when diffing with `dd`. Put this code inside of your `.vimrc`: ```vim -augroup flog - autocmd FileType floggraph nno dd :call flog#run_tmp_command('vertical belowright Git diff HEAD %h') - autocmd FileType floggraph vno dd :call flog#run_tmp_command("vertical belowright Git diff %(h'>) %(h'<)") +augroup MyFlogSettings + autocmd FileType floggraph nno dd :exec flog#Format('vertical belowright Floggit -s -t diff HEAD %h') + autocmd FileType floggraph vno dd :exec flog#Format("vertical belowright Floggit -s -t diff %(h'>) %(h'<)") augroup END ``` -`flog#run_tmp_command` tells flog to run the command and treat any windows it opens as temporary. -You can also use `flog#run_command`, which runs a command using the same syntax without temporary windows. +`Floggit` runs a command using Fugitive's `Git` command. +The `-s` flag prevents the Flog buffer from updating after running the command. +The `-t` flag treats any windows it opens as temporary side windows. -This function can use different special format specifiers, similar to `printf()`. -In this case, `%h` will resolve to the hash on the current line, and `%(h'>) %(h'<)` will resolve to the hashes at the end and beginning of the visual selection. +The `flog#Format()` function uses special format specifier items, similar to `printf()`, to get contextual information from Flog. + +The `%h` format specifier item used here will resolve to the hash on the current line. +`%(h'>) %(h'<)` will resolve to the hashes at the end and beginning of the visual selection. When diffing with `dd`, Flog will now show a diff from bottom-to-top, instead of top-to-bottom. -This is because `%(h'<)` and `%(h'>)` have been swapped from the default command. +This is because `HEAD`/`%h` have been swapped in normal mode from the default command, and `%(h'<)`/`%(h'>)` have been swapped in visual mode. See `:help flog-command-format` for more format specifiers. See `:help flog-functions` for more details about calling command functions. -You can also view [the floggraph filetype script](https://github.com/rbong/vim-flog/blob/master/ftplugin/floggraph.vim), which effectively serves as further examples of Flog's utility functions. +You can also view [the floggraph filetype script](https://github.com/rbong/vim-flog/blob/master/ftplugin/floggraph.vim), which contains more examples. Finally, if you would like to view user-created commands, check out the [Wiki](https://github.com/rbong/vim-flog/wiki/Custom-Commands). ## Additional Examples @@ -91,5 +100,5 @@ Here are some brief ideas. - You can start/manage a bisect with `:Floggit` commands, taking advantage of completion, and toggle seeing the current commits in the bisect with `gb`. - You can view the history of a file next to the file itself with `:Flogsplit -path=%`. - You can view the history for a particular range of lines in a file by visually selecting it and then typing `:Flog`. - This will display an inline patch, which you can trigger with `gp`. -- If you haven't already, look through `:help flog`. There are many commands that still haven't been covered here. + - This will display an inline patch, which you can trigger with `gp`. +- If you haven't already, look through `:help flog`. There is much that still hasn't been covered here. diff --git a/FAQ.md b/FAQ.md index 46224fe..12797db 100644 --- a/FAQ.md +++ b/FAQ.md @@ -2,102 +2,80 @@ ## How do I get Flog to run faster? -There are several ways to get Flog to run faster depending on what your exact issue is. +The answer depends on your issue. -**Specifying the max count** +**Flog is slow the first time it runs for a repo** -You may want to specify `-max-count=`, or use `let g:flog_default_arguments = { 'max_count': }`. +The first time Flog runs for a repo, it runs `git commit-graph write`. +This ultimately makes it run faster. -This restricts the log to displaying a certain number of commits. -This will increase the speed at which Flog can redraw the commit graph, generally reducing lag. - -If you need to jump forward/backwards in history by ``, use `[[`/`]]` - -**Pre-calculating the commit graph** - -In very large repositories, the commit graph can take a long time to sort when you use `git log --graph` or run Flog, even with max count specified. -In these cases you can pre-calculate the commit graph. - -If you are running Git 2.24 or greater, it is enabled by default. -Otherwise it can be enabled via: +Disable this feature: ``` -git config --global core.commitGraph true -git config --global gc.writeCommitGraph true +let g:flog_write_commit_graph = 0 ``` -After that, navigate to your repository and run `git commit-graph write`. - -This command may still take a long time to run, but once it has been generated, `git log --graph` and Flog will run much faster. -If you don't plan to view a large number of commits that aren't reachable, you can use `git commit-graph write --reachable` to speed up this process. +Set args (defaults shown): -You may want to re-run this command regularly when there are enough new commits. - -**Disabling graph mode** - -If you want to skip generating a graph and use Flog just as a log viewer, you can pass `-no-graph` to Flog or use the `gx` binding to toggle the graph. -This is equivalent to `git log --no-graph`. - -If this is still too slow, it might be because Flog has to wait until the command completes to write output to the buffer. -In these cases, you may want to resort to just using `git log` in the terminal. +``` +let g:flog_write_commit_graph_args = '--reachable --progress' +``` -**Flog is still too slow** +**Flog gets slower over time for repos** -Flog, unlike other branch viewers like `gitk`, is just a wrapper around `git log`. -It just reads static output from the command after it finishes and writes it to a buffer. -By contrast, `gitk` reads raw commit data, calculates the graph structure itself commit-by-commit, and updates the display, all without hanging. +The commit graph will eventually become out of date. -This may change in the future, so check back. +You can update it by running: -If you have any feedback about Flog's speed or any of the suggestions above, please see [this ongoing issue](https://github.com/rbong/vim-flog/issues/26). +``` +git commit-graph write --reachable --progress +``` -## How do I get Flog to look nicer? +**Flog takes a long time to load for many commits** -Flog struggles with highlighting since Vim is not built to highlight vertical columns. +By default, Flog will shows 5,000 commits. -To use the same highlighting that `git log --graph` would use in the shell, -[download the AnsiEsc.vim plugin](https://github.com/vim-scripts/AnsiEsc.vim), -then add `let g:flog_use_ansi_esc = 1` to your `.vimrc`. +Launch with less commits: -Note that using `AnsiEsc.vim` with Flog comes with a performance hit. +``` +:Flog -max-count=2000 +``` -Another option is to use a custom command to replace `git log --graph`. -Some users prefer the look of [git-forest](https://github.com/rbong/git-scripts/blob/master/git-forest), -pictured below. +Launch with less commits by default: -![git-forest](img/git-forest.png) +``` +let g:flog_permanent_default_opts = { 'max_count': 2000 } +``` -To use `git-forest` as a custom log command, -[download it from here](https://github.com/rbong/git-scripts/blob/master/git-forest), -and add it to your path, then add this to your `.vimrc`. +**Flog takes a long time to load for complex git branch graphs** -```vim -let g:flog_build_log_command_fn = 'flog#build_git_forest_log_command' -``` +Toggle the graph with `gx` or launch with `:Flog -no-graph`. -## Why not just use the `git log --graph` command? +**Other issues** -To interact with commits. +Please [post an issue](https://github.com/rbong/vim-flog/issues/). -## Why have a branch viewer inside of Vim? +## What are the differences with other branch viewers? -This allows seamlessly switching between navigating the commit history, running git commands, and editing files checked into git. +[gv.vim](https://github.com/junegunn/gv.vim) is an ultra-light branch viewer. -It also prevents having to learn another git interface on top of [fugitive](https://github.com/tpope/vim-fugitive). +[gitv](https://github.com/gregsexton/gitv) is a fully featured branch viewer. -If you want to know everything you can do with fugitive, I recommend [the Vimcasts fugitive series](http://vimcasts.org/blog/2011/05/the-fugitive-series/). +Flog is faster than gitv. +Flog is slower than gv.vim, but in many cases only marginally. -## What are the differences with other branch viewers? +gv.vim and gitv rely on the output of `git log --graph`. +Flog draws the git branch graph itself. +This allows for branch highlighting and beautiful git branch graphs. -[gv.vim](https://github.com/junegunn/gv.vim) is an ultra-light branch viewer, whereas Flog is fully featured. -Flog allows updating the graph, running commands, and customization, where gv does not. +Flog is more customizable and flexible than gitv. +gv.vim does not have any customization or flexibility by design. -[gitv](https://github.com/gregsexton/gitv) is another fully featured branch viewer. -Flog is a next generation branch viewer that learns a lot of lessons from gitv. -It has a better defined argument system, more robust window management, more stable update system, has the ability to run more commands in the graph easier, has cleaner mappings, and supports any log format. +Flog has features which have no equivalent in either of the other branch viewers. +This includes commit marks, some navigation mappings, and contextually aware command completion. ## How can I learn how to use flog? See `:help flog` for all commands and options. -See [the examples](EXAMPLES.md) for detailed walkthroughs of different operations using flog. -Please [post an issue](https://github.com/rbong/vim-flog/issues) if you have any questions on how to do anything. +See [examples](EXAMPLES.md) for detailed walkthroughs. +Please [post an issue](https://github.com/rbong/vim-flog/issues) if you have any questions. diff --git a/Flavorfile b/Flavorfile deleted file mode 100644 index 7636db0..0000000 --- a/Flavorfile +++ /dev/null @@ -1 +0,0 @@ -flavor 'tpope/vim-fugitive' diff --git a/Flavorfile.lock b/Flavorfile.lock deleted file mode 100644 index 7c783e7..0000000 --- a/Flavorfile.lock +++ /dev/null @@ -1,2 +0,0 @@ -kana/vim-vspec (v1.9.2) -tpope/vim-fugitive (v3.4) diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 862399b..0000000 --- a/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -source 'https://rubygems.org' - -gem "vim-flavor", "~> 4.0" - -gem "rake", "~> 13.0" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index e292fee..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,23 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - parslet (2.0.0) - pastel (0.8.0) - tty-color (~> 0.5) - rake (13.0.6) - thor (1.2.1) - tty-color (0.6.0) - vim-flavor (4.0.2) - parslet (>= 1.8, < 3.0) - pastel (~> 0.7) - thor (>= 0.20, < 2.0) - -PLATFORMS - ruby - -DEPENDENCIES - rake (~> 13.0) - vim-flavor (~> 4.0) - -BUNDLED WITH - 2.2.25 diff --git a/README.md b/README.md index 89331c5..4e9d5fb 100644 --- a/README.md +++ b/README.md @@ -2,59 +2,60 @@ [![test status](https://github.com/rbong/vim-flog/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/rbong/vim-flog/actions) -Flog is a lightweight and powerful git branch viewer that integrates with -[fugitive](https://github.com/tpope/vim-fugitive). +Flog is a fast, beautiful, and powerful git branch viewer for Vim. ![flog in action](img/screen-graph.png) +## Prerequisites + +In Vim 8/9, [LuaJIT 2.1](https://luajit.org/download.html) must be installed. + +On systems without LuaJIT available, you may also use [Lua](https://www.lua.org/) 5.1, +however this is less performant. + +Neovim is supported natively. + ## Installation -Using [Plug](https://github.com/junegunn/vim-plug) add the following to your `.vimrc`: +If you use [Plug](https://github.com/junegunn/vim-plug), add the following to your `.vimrc`: ```vim Plug 'tpope/vim-fugitive' Plug 'rbong/vim-flog' ``` -See `:help plug-example` for more information. -If you do not use plug, see your plugin manager of choice's documentation. - -Requires vim version 8 or greater. -Neovim is also supported. - ## Using Flog -Open the commit graph with `:Flog` or `:Flogsplit`. +Open the git branch graph with `:Flog` or `:Flogsplit`. Many options can be passed in, complete with `` completion. Open commits in temporary windows once you've opened Flog using ``. Jump between commits with `` and ``. -Refresh the graph with `u`. +Refresh the git branch graph with `u`. Toggle viewing all branches with `a`. -Toggle bisect mode with `gb`. Toggle displaying no merges with `gm`. Toggle viewing the reflog with `gr`. +Toggle bisect mode with `gb`. Quit with `gq`. -Many of the bindings that work in fugitive in `:Gstatus` windows will work in Flog. +See more mappings with `g?`. -To see more bindings or get a refresher, press `g?`. +Many of the mappings that work in the Fugitive `:Git` status window will work in Flog. -Run `:Git` commands in a split next to the graph using `:Floggit -p`. +Run `:Git` commands in a split next to the git branch graph using `:Floggit -p`. Command line completion is provided to do any git command with the commits and refs under the cursor. -You can do a lot more with Flog. -Flog can be heavily customized, and comes with utility functions for defining your own commands. -See the [examples](EXAMPLES.md) for more details. +Flog can be heavily customized with functions. +See [examples](EXAMPLES.md) for details. ## Getting Help -If you have questions, requests, or bugs, see -[the issue tracker](https://github.com/rbong/vim-flog/issues) and `:help flog`. +See [the issue tracker](https://github.com/rbong/vim-flog/issues) and `:help flog`. + +See [fugitive](https://github.com/tpope/vim-fugitive) for help with fugitive. -Please see [fugitive](https://github.com/tpope/vim-fugitive) for help with Fugitive commands. -See `git log --help` for any problems specific to `git log`. +See `git log --help` for help with `git log`. More info: - [FAQ](FAQ.md) diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 0103698..0000000 --- a/Rakefile +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env rake - -task :test do - sh 'bundle exec vim-flavor test' -end diff --git a/autoload/flog.vim b/autoload/flog.vim index 43f6080..d4c44bb 100644 --- a/autoload/flog.vim +++ b/autoload/flog.vim @@ -1,2282 +1,49 @@ -" Utilities {{{ +" +" This file contains public Flog API functions. +" -function! flog#instance() abort - let l:instance = g:flog_instance_counter - let g:flog_instance_counter += 1 - return l:instance -endfunction - -function! flog#get_all_window_ids() abort - let l:tabs = gettabinfo() - let l:windows = [] - for l:tab in l:tabs - let l:windows += l:tab.windows - endfor - return l:windows -endfunction - -function! flog#exclude(list, filters) abort - return filter(a:list, 'index(a:filters, v:val) < 0') -endfunction - -function! flog#ellipsize(string, ...) abort - let l:max_len = a:0 >= 1 ? min(a:1, 4) : 15 - let l:dir = a:0 >= 2 ? a:2 : 0 - - if len(a:string) > l:max_len - if l:dir == 0 - return a:string[: l:max_len - 4] . '...' - else - return '...' . a:string[l:max_len - 3 :] - endif - else - return a:string - endif -endfunction - -function! flog#unescape_arg(arg) abort - let l:arg = '' - let l:is_escaped = 0 - - for l:char in split(a:arg, '\zs') - if l:char ==# '\' && !l:is_escaped - let l:is_escaped = 1 - else - let l:arg .= l:char - let l:is_escaped = 0 - endif - endfor - - return l:arg -endfunction - -function! flog#resolve_path(path, relative_dir) abort - let l:full_path = fnamemodify(a:path, ':p') - if stridx(l:full_path, a:relative_dir) == 0 - return l:full_path[len(a:relative_dir) + 1:] - endif - return a:path -endfunction - -function! flog#split_limit(limit) abort - let [l:match, l:start, l:end] = matchstrpos(a:limit, '^.\{1}:\zs') - if l:start < 0 - return [a:limit, ''] - endif - return [a:limit[: l:start - 1], a:limit[l:start :]] -endfunction - -function! flog#get_sort_type(name) abort - return filter(copy(g:flog_sort_types), 'v:val.name ==# ' . string(a:name))[0] -endfunction - -function! flog#get(dict, key, ...) abort - if type(a:dict) != v:t_dict - return v:null - endif - let l:default = get(a:, 1, v:null) - return get(a:dict, a:key, l:default) -endfunction - -" }}} - -" Deprecation helpers {{{ - -function! flog#show_deprecation_warning(deprecated_usage, new_usage) abort - echoerr printf('Deprecated: %s', a:deprecated_usage) - echoerr printf('New usage: %s', a:new_usage) - let g:flog_shown_deprecation_warnings[a:deprecated_usage] = 1 -endfunction - -function! flog#did_show_deprecation_warning(deprecated_usage) abort - return has_key(g:flog_shown_deprecation_warnings, a:deprecated_usage) -endfunction - -function! flog#deprecate_default_mapping(mapping, new_mapping) abort - let l:deprecated_usage = a:mapping - if !flog#did_show_deprecation_warning(l:deprecated_usage) - let l:new_usage = a:new_mapping - return flog#show_deprecation_warning(l:deprecated_usage, l:new_usage) - endif -endfunction - -function! flog#deprecate_plugin_mapping(mapping, new_mapping, ...) abort - let l:deprecated_usage = a:mapping - if hasmapto(a:mapping) && !flog#did_show_deprecation_warning(l:deprecated_usage) - let l:new_mapping_type = get(a:, 1, '{nmap|vmap}') - let l:new_mapping_value = get(a:, 2, '...') - let l:new_usage = printf('%s %s %s', l:new_mapping_type, l:new_mapping_value, a:new_mapping) - return flog#show_deprecation_warning(l:deprecated_usage, l:new_usage) - endif -endfunction - -function! flog#deprecate_setting(setting, new_setting, ...) abort - let l:deprecated_usage = a:setting - if exists(a:setting) && !flog#did_show_deprecation_warning(l:deprecated_usage) - let l:new_setting_value = get(a:, 1, '...') - let l:new_usage = printf('let %s = %s', a:new_setting, l:new_setting_value) - return flog#show_deprecation_warning(l:deprecated_usage, l:new_usage) - endif -endfunction - -function! flog#deprecate_function(func, new_func, ...) abort - let l:deprecated_usage = printf('%s()', a:func) - let l:new_func_args = get(a:, 1, '...') - let l:new_usage = printf('call %s(%s)', a:new_func, l:new_func_args) - - if !flog#did_show_deprecation_warning(l:deprecated_usage) - let l:new_func_args = get(a:, 1, '...') - let l:new_usage = printf('call %s(%s)', a:new_func, l:new_func_args) - call flog#show_deprecation_warning(l:deprecated_usage, l:new_usage) - endif -endfunction - -function! flog#deprecate_autocmd(autocmd, new_autocmd) abort - let l:deprecated_usage = a:autocmd - if exists('#' . a:autocmd) && !flog#did_show_deprecation_warning(l:deprecated_usage) - let l:new_usage = printf('autocmd %s ...', a:new_autocmd) - call flog#show_deprecation_warning(l:deprecated_usage, l:new_usage) - endif -endfunction - -function! flog#deprecate_command(command, new_command, ...) abort - let l:deprecated_usage = a:command - if !flog#did_show_deprecation_warning(l:deprecated_usage) - let l:new_command_args = get(a:, 1, '...') - let l:new_usage = printf('%s %s', a:new_command, l:new_command_args) - call flog#show_deprecation_warning(l:deprecated_usage, l:new_usage) - endif -endfunction - -" }}} - -" Shell interface {{{ - -function! flog#systemlist(command) abort - let l:output = systemlist(a:command) - if v:shell_error - echoerr join(l:output, "\n") - throw g:flog_shell_error - endif - return l:output -endfunction - -function! flog#shellescapelist(list) abort - return map(copy(a:list), 'shellescape(v:val)') -endfunction - -" }}} - -" Fugitive interface {{{ - -function! flog#is_fugitive_buffer() abort - return FugitiveIsGitDir() -endfunction - -function! flog#resolve_fugitive_path_arg(path) abort - return flog#resolve_path(a:path, FugitiveFind(':/')) -endfunction - -function! flog#get_fugitive_git_command() abort - return FugitiveShellCommand() -endfunction - -function! flog#get_initial_workdir() abort - return FugitiveFind(':/') -endfunction - -function! flog#get_fugitive_git_dir() abort - return FugitiveGitDir() -endfunction - -function! flog#trigger_fugitive_git_detection() abort - let l:workdir = flog#get_state().workdir - call FugitiveDetect(l:workdir) -endfunction - -" }}} - -" Argument handling {{{ - -" Argument parsing {{{ - -function! flog#get_internal_default_args() abort - let l:defaults = { - \ 'raw_args': v:null, - \ 'format': '%Cblue%ad%Creset %C(yellow)[%h]%Creset %Cgreen{%an}%Creset%Cred%d%Creset %s', - \ 'date': 'iso8601', - \ 'all': v:false, - \ 'bisect': v:false, - \ 'no_merges': v:false, - \ 'reflog': v:false, - \ 'reverse': v:false, - \ 'no_graph': v:false, - \ 'no_patch': v:false, - \ 'skip': v:null, - \ 'sort': v:null, - \ 'max_count': v:null, - \ 'open_cmd': 'tabedit', - \ 'search': v:null, - \ 'patch_search': v:null, - \ 'author': v:null, - \ 'limit': v:null, - \ 'rev': [], - \ 'path': [] - \ } - - " read the user immutable defaults - if exists('g:flog_permanent_default_arguments') - for [l:key, l:value] in items(g:flog_permanent_default_arguments) - if has_key(l:defaults, l:key) - let l:defaults[l:key] = l:value - else - echoerr 'Warning: unrecognized immutable argument ' . l:key - endif - endfor - endif - - return l:defaults -endfunction - -function! flog#get_default_args() abort - let l:new_settings = '{g:flog_default_arguments|g:flog_permanent_default_arguments}' - call flog#deprecate_setting('g:flog_default_format', l:new_settings, '{ "format": ... }') - call flog#deprecate_setting('g:flog_default_date_format', l:new_settings, '{ "date": ... }') - - let l:defaults = flog#get_internal_default_args() - - " read the user argument defaults - if exists('g:flog_default_arguments') - for [l:key, l:value] in items(g:flog_default_arguments) - if has_key(l:defaults, l:key) - let l:defaults[l:key] = l:value - else - echoerr 'Warning: unrecognized default argument ' . l:key - endif - endfor - endif - - return l:defaults -endfunction - -function! flog#parse_arg_opt(arg) abort - let l:opt = matchstr(a:arg, '=\zs.*') - return l:opt -endfunction - -function! flog#parse_limit_opt(arg) abort - let l:arg = flog#parse_arg_opt(a:arg) - let [l:limit, l:path] = flog#split_limit(l:arg) - if l:path ==# '' - return l:arg - endif - return l:limit . fnameescape(flog#resolve_fugitive_path_arg(l:path)) -endfunction - -function! flog#parse_path_opt(arg) abort - return [fnameescape(flog#resolve_fugitive_path_arg(expand(flog#parse_arg_opt(a:arg))))] -endfunction - -function! flog#parse_set_args(args, current_args, defaults) abort - let l:has_set_path = v:false - - let l:has_set_rev = v:false - - let l:has_set_raw_args = v:false - let l:got_raw_args_token = v:false - let l:raw_args = [] - - for l:arg in a:args - if l:got_raw_args_token - let l:has_set_raw_args = v:true - let l:raw_args += [l:arg] - elseif l:arg ==# '--' - let l:got_raw_args_token = v:true - elseif l:arg =~# '^-format=.\+' - let a:current_args.format = flog#parse_arg_opt(l:arg) - elseif l:arg ==# '-format=' - let a:current_args.format = a:defaults.format - elseif l:arg =~# '^-date=.\+' - let a:current_args.date = flog#parse_arg_opt(l:arg) - elseif l:arg ==# '-date=' - let a:current_args.date = a:defaults.date - elseif l:arg =~# '^-raw-args=.\+' - let l:has_set_raw_args = v:true - let l:raw_args += [flog#parse_arg_opt(l:arg)] - elseif l:arg ==# '-raw-args=' - let l:has_set_raw_args = v:false - let a:current_args.raw_args = a:defaults.raw_args - elseif l:arg ==# '-all' - let a:current_args.all = v:true - elseif l:arg ==# '-bisect' - let a:current_args.bisect = v:true - elseif l:arg ==# '-no-merges' - let a:current_args.no_merges = v:true - elseif l:arg ==# '-reflog' - let a:current_args.reflog = v:true - elseif l:arg ==# '-reverse' - let a:current_args.reverse = v:true - elseif l:arg ==# '-no-graph' - let a:current_args.no_graph = v:true - elseif l:arg ==# '-no-patch' - let a:current_args.no_patch = v:true - elseif l:arg =~# '^-skip=\d\+' - let a:current_args.skip = flog#parse_arg_opt(l:arg) - elseif l:arg ==# '-skip=' - let a:current_args.skip = a:defaults.skip - elseif l:arg =~# '^-sort=.\+' - let a:current_args.sort = flog#parse_arg_opt(l:arg) - elseif l:arg ==# '-sort=' - let a:current_args.sort = a:defaults.sort - elseif l:arg =~# '^-max-count=\d\+' - let a:current_args.max_count = flog#parse_arg_opt(l:arg) - elseif l:arg ==# '-max-count=' - let a:current_args.max_count = a:defaults.max_count - elseif l:arg =~# '^-open-cmd=.\+' - let a:current_args.open_cmd = flog#parse_arg_opt(l:arg) - elseif l:arg ==# '-open-cmd=' - let a:current_args.open_cmd = a:defaults.open_cmd - elseif l:arg =~# '^-search=.\+' - let a:current_args.search = flog#parse_arg_opt(l:arg) - elseif l:arg ==# '-search=' - let a:current_args.search = a:defaults.search - elseif l:arg =~# '^-patch-search=.\+' - let a:current_args.patch_search = flog#parse_arg_opt(l:arg) - elseif l:arg ==# '-patch-search=' - let a:current_args.patch_search = a:defaults.patch_search - elseif l:arg =~# '^-author=.\+' - let a:current_args.author = flog#parse_arg_opt(l:arg) - elseif l:arg ==# '-author=' - let a:current_args.author = a:defaults.author - elseif l:arg =~# '^-limit=.\+' - let a:current_args.limit = flog#parse_limit_opt(l:arg) - elseif l:arg ==# '-limit=' - let a:current_args.limit = a:defaults.limit - elseif l:arg =~# '^-rev=.\+' - if !l:has_set_rev - let a:current_args.rev = [] - let l:has_set_rev = v:true - endif - let a:current_args.rev += [flog#parse_arg_opt(l:arg)] - elseif l:arg ==# '-rev=' - let l:has_set_rev = v:false - let a:current_args.rev = a:defaults.rev - elseif l:arg =~# '^-path=.\+' - if !l:has_set_path - let a:current_args.path = [] - let l:has_set_path = v:true - endif - let a:current_args.path += flog#parse_path_opt(l:arg) - elseif l:arg ==# '-path=' - let a:current_args.path = a:defaults.path - let l:has_set_path = v:false - else - echoerr 'error parsing argument ' . l:arg - throw g:flog_unsupported_argument - endif - endfor - - if l:has_set_raw_args - let a:current_args.raw_args = join(l:raw_args, ' ') - endif - - return a:current_args -endfunction - -function! flog#parse_args(args) abort - return flog#parse_set_args(a:args, flog#get_default_args(), flog#get_internal_default_args()) -endfunction - -" }}} - -" Argument completion {{{ - -" Argument completion utilities {{{ - -function! flog#filter_completions(arg_lead, completions) abort - let l:lead = escape(a:arg_lead, '\\') - return filter(a:completions, 'v:val =~# "^" . l:lead') -endfunction - -function! flog#escape_completions(lead, completions) abort - return map(a:completions, "a:lead . substitute(v:val, ' ', '\\\\ ', '')") -endfunction - -function! flog#shellescape_completions(completions) abort - return map(a:completions, 'fnameescape(v:val)') -endfunction - -function! flog#split_single_completable_arg(arg) abort - let l:start_pattern = '^\([^=]*=\)\?' - let l:start = matchstr(a:arg, l:start_pattern) - let l:rest = matchstr(a:arg, l:start_pattern . '\zs.*') - - return [l:start, l:rest] -endfunction - -function! flog#split_completable_arg(arg) abort - let [l:start, l:rest ] = flog#split_single_completable_arg(a:arg) - - let l:split = split(l:rest, '\\ ', v:true) - - let l:trimmed = l:split[:-2] - - if l:split != [] - let l:last = l:split[-1] - else - let l:last = '' - endif - - let l:lead = l:start . join(l:trimmed, '\ ') - if len(l:trimmed) > 0 - let l:lead .= '\ ' - endif - - return [l:lead, l:last] -endfunction - -function! flog#find_arg_command(split_args) abort - let l:i = 1 - while l:i < len(a:split_args) - let l:arg = a:split_args[l:i] - if len(l:arg) && l:arg[0] !=# '-' - return [l:i, l:arg] - endif - let l:i += 1 - endwhile - return [-1, ''] -endfunction - -" }}} - -" Argument commands {{{ - -function! flog#get_git_commands() abort - return flog#systemlist(flog#get_fugitive_git_command() . ' --list-cmds=list-mainporcelain,others,nohelpers,alias,list-complete,config') -endfunction - -function! flog#get_git_options(split_args, command_index) abort - let l:options = [] - - let l:command = a:split_args[a:command_index] - let l:command_spec = get(g:flog_git_command_spec, l:command, {}) - - if has_key(l:command_spec, 'subcommands') && a:command_index >= len(a:split_args) - 2 - let l:options += l:command_spec.subcommands - endif - - if has_key(l:command_spec, 'options') - let l:options += l:command_spec.options - endif - - return l:options -endfunction - -function! flog#get_remotes() abort - return flog#systemlist(flog#get_fugitive_git_command() . ' remote') -endfunction - -function! flog#get_refs() abort - let l:command = flog#get_fugitive_git_command() - \ . ' rev-parse --symbolic --branches --tags --remotes' - return flog#systemlist(l:command) + ['HEAD', 'FETCH_HEAD', 'MERGE_HEAD', 'ORIG_HEAD'] -endfunction - -function! flog#get_authors() abort - let l:command = flog#get_fugitive_git_command() - \ . ' shortlog --all --no-merges -s -n' - " Filter author commit numbers before returning - return map( - \ flog#systemlist(l:command), - \ 'substitute(v:val, "^\\s*\\d\\+\\s*", "", "")') -endfunction - -" }}} - -" Git command argument completion {{{ - -function! flog#complete_line(arg_lead, cmd_line, cursor_pos) abort - let l:line = line('.') - let l:firstline = line("'<") - let l:lastline = line("'>") - - if (l:line != l:firstline && l:line != l:lastline) || l:firstline == l:lastline - " complete for only the current line - let l:commit = flog#get_commit_at_line() - if type(l:commit) != v:t_dict - return [] - endif - let l:completions = [l:commit.short_commit_hash] - \ + flog#get_remotes() + l:commit.ref_name_list - else - " complete for a range - let l:commit = flog#get_commit_selection(l:firstline, l:lastline) - if type(l:commit) != v:t_list - return [] - endif - let l:first_commit = l:commit[0] - let l:last_commit = l:commit[1] - if l:first_commit == l:last_commit - let l:completions = [l:first_commit.short_commit_hash] + l:commit.ref_name_list - else - let l:first_hash = l:first_commit.short_commit_hash - let l:last_hash = l:last_commit.short_commit_hash - let l:completions = [l:first_hash, l:last_hash] - \ + flog#get_remotes() - \ + l:first_commit.ref_name_list + l:last_commit.ref_name_list - \ + [ - \ l:last_hash . '..' . l:first_hash, - \ l:last_hash . '^..' . l:first_hash - \ ] - endif - endif - - return flog#filter_completions(a:arg_lead, l:completions) -endfunction - -function! flog#complete_git(arg_lead, cmd_line, cursor_pos) abort - call flog#deprecate_setting('g:flog_git_commands', '(None)') - call flog#deprecate_setting('g:flog_git_subcommands', 'g:flog_git_command_spec', '{ "command": { "subcommands": [...] } }') - - let l:is_flog = flog#has_state() - if l:is_flog - let l:state = flog#get_state() - endif - - let l:is_fugitive = flog#is_fugitive_buffer() - - let l:split_args = split(a:cmd_line, '\s', v:true) - let [l:command_index, l:command] = flog#find_arg_command(l:split_args) - - " complete commands - if l:command ==# '' || l:command_index == len(l:split_args) - 1 - return flog#filter_completions(a:arg_lead, flog#get_git_commands()) - endif - - if l:is_flog - " complete line info - let l:completions = flog#complete_line(a:arg_lead, a:cmd_line, a:cursor_pos) - - " complete limit - if l:state.limit - let [l:limit, l:limit_path] = flog#split_limit(l:state.limit) - let l:completions += flog#filter_completions(a:arg_lead, [l:limit_path]) - endif - - " complete path - let l:completions += flog#exclude(flog#filter_completions(a:arg_lead, l:state.path), l:completions) - else - let l:completions = [] - endif - - " complete options - let l:completions += flog#filter_completions(a:arg_lead, flog#get_git_options(l:split_args, l:command_index)) - - if l:is_fugitive - " complete all possible refs - let l:completed_refs = flog#filter_completions(a:arg_lead, flog#get_refs()) - let l:completions += flog#exclude(l:completed_refs, l:completions) - - " complete all filenames - let l:completions += flog#exclude(getcompletion(a:arg_lead, 'file'), l:completions) - endif - - return flog#shellescape_completions(l:completions) -endfunction - -" }}} - -" Flog command argument commpletion {{{ - -function! flog#complete_format(arg_lead) abort - " build patterns - let l:completable_pattern = g:flog_eat_specifier_pattern - \ . '\zs%' . g:flog_completable_specifier_pattern . '\?$' - let l:noncompletable_pattern = g:flog_eat_specifier_pattern - \ . '\zs%' . g:flog_noncompletable_specifier_pattern . '$' - - " test the arg lead - if a:arg_lead =~# l:noncompletable_pattern - " format ends with an incompletable pattern - return [] - elseif a:arg_lead =~# l:completable_pattern - " format ends with a completable pattern - let l:lead = substitute(a:arg_lead, l:completable_pattern, '', '') - let l:completions = map(copy(g:flog_completion_specifiers), 'l:lead . v:val') - return flog#filter_completions(a:arg_lead, copy(l:completions)) - else - " format does not end with any special atom - return [a:arg_lead . '%'] - endif -endfunction - -function! flog#complete_date(arg_lead) abort - let [l:lead, l:path] = flog#split_single_completable_arg(a:arg_lead) - let l:completions = map(copy(g:flog_date_formats), 'l:lead . v:val') - return flog#filter_completions(a:arg_lead, l:completions) -endfunction - -function! flog#complete_open_cmd(arg_lead) abort - " get the lead without the last command - let [l:lead, l:last] = flog#split_completable_arg(a:arg_lead) - - " build the list of possible completions - let l:completions = [] - let l:completions += map(copy(g:flog_open_cmd_modifiers), 'l:lead . v:val') - let l:completions += map(copy(g:flog_open_cmds), 'l:lead . v:val') - - return flog#filter_completions(a:arg_lead, l:completions) -endfunction - -function! flog#complete_limit(arg_lead) abort - let [l:lead, l:last] = flog#split_completable_arg(a:arg_lead) - - let [l:limit, l:path] = flog#split_limit(l:last) - if l:limit !~# '^.\{1}:$' - return [] - endif - - let l:files = getcompletion(flog#unescape_arg(l:path), 'file') - let l:completions = flog#escape_completions(l:lead . l:limit, l:files) - - return flog#filter_completions(a:arg_lead, l:completions) -endfunction - -function! flog#complete_rev(arg_lead) abort - if !flog#is_fugitive_buffer() - return [] - endif - let [l:lead, l:last] = flog#split_single_completable_arg(a:arg_lead) - let l:refs = flog#get_refs() - return flog#filter_completions(a:arg_lead, map(l:refs, 'l:lead . v:val')) -endfunction - -function! flog#complete_path(arg_lead) abort - let [l:lead, l:path] = flog#split_single_completable_arg(a:arg_lead) - - let l:files = getcompletion(flog#unescape_arg(l:path), 'file') - let l:completions = flog#escape_completions(l:lead, l:files) - - return flog#filter_completions(a:arg_lead, l:completions) -endfunction - -function! flog#complete_author(arg_lead) abort - let [l:lead, l:name] = flog#split_single_completable_arg(a:arg_lead) - let l:authors = flog#escape_completions(l:lead, flog#get_authors()) - return flog#filter_completions(a:arg_lead, l:authors) -endfunction - -function! flog#complete_sort(arg_lead) abort - let [l:lead, l:name] = flog#split_single_completable_arg(a:arg_lead) - let l:sort_types = flog#escape_completions(l:lead, map(copy(g:flog_sort_types), 'v:val.name')) - return flog#filter_completions(a:arg_lead, l:sort_types) -endfunction - -function! flog#complete(arg_lead, cmd_line, cursor_pos) abort - if a:cmd_line[:a:cursor_pos] =~# ' -- ' - return [] - endif - - if a:arg_lead ==# '' - return flog#filter_completions(a:arg_lead, copy(g:flog_default_completion)) - elseif a:arg_lead =~# '^-format=' - return flog#complete_format(a:arg_lead) - elseif a:arg_lead =~# '^-date=' - return flog#complete_date(a:arg_lead) - elseif a:arg_lead =~# '^-open-cmd=' - return flog#complete_open_cmd(a:arg_lead) - elseif a:arg_lead =~# '^-\(patch-\)\?search=' - return [] - elseif a:arg_lead =~# '^-author=' - return flog#complete_author(a:arg_lead) - elseif a:arg_lead =~# '^-limit=' - return flog#complete_limit(a:arg_lead) - elseif a:arg_lead =~# '^-rev=' - return flog#complete_rev(a:arg_lead) - elseif a:arg_lead =~# '^-path=' - return flog#complete_path(a:arg_lead) - elseif a:arg_lead =~# '^-sort=' - return flog#complete_sort(a:arg_lead) - endif - return flog#filter_completions(a:arg_lead, copy(g:flog_default_completion)) -endfunction - -function! flog#complete_jump(arg_lead, cmd_line, cursor_pos) abort - let l:state = flog#get_state() - return flog#shellescape_completions(flog#complete_rev(a:arg_lead)) -endfunction - -" }}} - -" }}} - -" }}} - -" State management {{{ - -function! flog#get_initial_state(parsed_args, original_file) abort - return extend(copy(a:parsed_args), { - \ 'instance': flog#instance(), - \ 'workdir': flog#get_initial_workdir(), - \ 'original_file': a:original_file, - \ 'graph_window_id': v:null, - \ 'tmp_cmd_window_ids': [], - \ 'last_cmd_window_ids': [], - \ 'previous_log_command': v:null, - \ 'line_commits': [], - \ 'commit_refs': [], - \ 'line_commit_refs': [], - \ 'ref_line_lookup': {}, - \ 'commit_line_lookup': {}, - \ 'commit_marks': {}, - \ 'ansi_esc_called': v:false, - \ }) -endfunction - -function! flog#set_buffer_state(state) abort - let b:flog_state = a:state -endfunction - -function! flog#has_state() abort - return exists('b:flog_state') -endfunction - -function! flog#get_state() abort - if !flog#has_state() - throw g:flog_missing_state - endif - return b:flog_state -endfunction - -function! flog#get_resolved_graph_options() abort - let l:opts = copy(flog#get_state()) - - let l:opts.bisect = l:opts.bisect && !l:opts.limit - let l:opts.reflog = l:opts.reflog && !l:opts.limit - - return l:opts -endfunction - -" }}} - -" Log command management {{{ - -function! flog#create_log_format() abort - call flog#deprecate_setting('g:flog_format_separator', '(None)') - call flog#deprecate_setting('g:flog_format_end', '(None)') - call flog#deprecate_setting('g:flog_display_commit_start', '(None)') - call flog#deprecate_setting('g:flog_display_commit_end', '(None)') - call flog#deprecate_setting('g:flog_format_specifiers', '(None)') - call flog#deprecate_setting('g:flog_log_data_format_specifiers', '(None)') - - let l:state = flog#get_state() - - " start format - let l:format = 'format:' - let l:format .= g:flog_format_start - let l:format .= l:state.format - - " add flog data - let l:format .= g:flog_data_start - " short hash and ref names - let l:format .= '%h %D' - - " perform string formatting to avoid shell interpolation - return shellescape(l:format) -endfunction - -function! flog#parse_log_output(output) abort - let l:output_len = len(a:output) - if l:output_len == 0 - return [[], []] - endif - - " Final filtered visual output - let l:o = [] - " Output index - let l:i = 0 - " Commits - let l:cs = [] - " Number of commits - let l:cn = 0 - " Current commit - let l:c = { 'display_len': 0 } - " Has found data - let l:d = 0 - " Format start length - let l:lf = len(g:flog_format_start) - " Data start length - let l:ld = len(g:flog_data_start) - - while l:i < l:output_len - " Get current line - let l:line = a:output[l:i] - - " Data found or no commit parsed, check for format start - if l:d || !l:cn - " Find format start if any - let l:j = stridx(l:line, g:flog_format_start) - - " Start of format found, start new commit - if l:j >= 0 - " Start new commit if at least one parsed - if l:cn - " Add to list of commits - call add(l:cs, l:c) - - " Set new current commit - let l:c = { 'display_len': 0 } - endif - - " Remove format start from line - let l:line = strpart(l:line, 0, l:j) - \ . strpart(l:line, l:j + l:lf) - - " Reset data found - let l:d = 0 - endif - endif - - " No commit data, try to find it - if !l:d - " Find start of data - let l:k = stridx(l:line, g:flog_data_start) - - " Start of data found - if l:k >= 0 - " Find hash end - let l:hend = stridx(l:line, ' ', l:k + l:ld) - - if l:hend > 0 - " Set hash - let l:c.short_commit_hash = strpart(l:line, - \ l:k + l:ld, l:hend - l:k - l:ld) - " Set ref names - let l:c.ref_names_unwrapped = strpart(l:line, l:hend + 1) - let l:c.ref_name_list = split( - \ l:c.ref_names_unwrapped, '\( -> \|, \|tag: \)\+') - else - " No space found, no refs - - " Set hash - let l:c.short_commit_hash = strpart(l:line, l:k + l:ld) - " Set ref names - let l:c.ref_names_unwrapped = '' - let l:c.ref_name_list = [] - endif - - " Set data found - let l:d = 1 - " Increment number of commits - let l:cn += 1 - - " Remove data from line - let l:line = strpart(l:line, 0, l:k) - endif - endif - - " Append line to output - call add(l:o, l:line) - let l:c.display_len += 1 - - let l:i += 1 - endwhile - - " Add last commit if any - if l:cn - call add(l:cs, l:c) - endif - - return [l:o, l:cs] -endfunction - -function! flog#build_log_paths() abort - let l:state = flog#get_state() - if len(l:state.path) == 0 - return '' - endif - let l:paths = map(l:state.path, 'fnamemodify(v:val, ":.")') - return join(l:paths, ' ') -endfunction - -function! flog#build_log_args() abort - let l:opts = flog#get_resolved_graph_options() - - let l:args = '' - - if !l:opts.no_graph - let l:args .= ' --graph' - endif - let l:args .= ' --no-color' - let l:args .= ' --pretty=' . flog#create_log_format() - let l:args .= ' --date=' . shellescape(l:opts.date) - if l:opts.all && !l:opts.limit - let l:args .= ' --all' - endif - if l:opts.bisect - let l:args .= ' --bisect' - endif - if l:opts.no_merges - let l:args .= ' --no-merges' - endif - if l:opts.reflog - let l:args .= ' --reflog' - endif - if l:opts.reverse - let l:args .= ' --reverse' - endif - if l:opts.no_patch - let l:args .= ' --no-patch' - endif - if l:opts.skip != v:null - let l:args .= ' --skip=' . shellescape(l:opts.skip) - endif - if l:opts.sort != v:null - let l:sort_type = flog#get_sort_type(l:opts.sort) - let l:args .= ' ' . l:sort_type.args - endif - if l:opts.max_count != v:null - let l:args .= ' --max-count=' . shellescape(l:opts.max_count) - endif - if l:opts.search != v:null - let l:search = shellescape(l:opts.search) - let l:args .= ' --grep=' . l:search - endif - if l:opts.patch_search != v:null - let l:patch_search = shellescape(l:opts.patch_search) - let l:args .= ' -G' . l:patch_search - endif - if l:opts.author != v:null - let l:args .= ' --author=' . shellescape(l:opts.author) - endif - if l:opts.limit != v:null - let l:limit = shellescape(l:opts.limit) - let l:args .= ' -L' . l:limit - endif - if l:opts.raw_args != v:null - let l:args .= ' ' . l:opts.raw_args - endif - if get(g:, 'flog_use_ansi_esc') - let l:args .= ' --color' - endif - if len(l:opts.rev) >= 1 - if l:opts.limit - let l:rev = shellescape(l:opts.rev[0]) - else - let l:rev = join(flog#shellescapelist(l:opts.rev), ' ') - endif - let l:args .= ' ' . l:rev - endif - - return l:args -endfunction - -function! flog#build_log_command() abort - let l:command = flog#get_fugitive_git_command() - let l:command .= ' log' - let l:command .= flog#build_log_args() - let l:command .= ' -- ' - let l:command .= flog#build_log_paths() - - return l:command -endfunction - -function! flog#build_git_forest_log_command() abort - let l:state = flog#get_state() - - if l:state.no_graph - return flog#build_log_command() - endif - - let l:command = 'export GIT_DIR=' - let l:command .= shellescape(flog#get_fugitive_git_dir()) - let l:command .= ' NO_PRINT_REFS=true' - let l:command .= '; ' - - let l:command .= 'git-forest ' - let l:command .= substitute(flog#build_log_args(), ' --graph', '', '') - let l:command .= ' -- ' - let l:command .= flog#build_log_paths() - - return l:command -endfunction - -" }}} - -" Commit operations {{{ - -function! flog#get_commit_at_line(...) abort - let l:line = get(a:, 1, '.') - if type(l:line) == v:t_string - let l:line = line(l:line) - endif - return get(flog#get_state().line_commits, l:line - 1, v:null) -endfunction - -function! flog#get_commit_at_ref(ref) abort - let l:state = flog#get_state() - if !has_key(l:state.ref_line_lookup, a:ref) - return v:null - endif - return flog#get_commit_at_line(l:state.ref_line_lookup[a:ref]) -endfunction - -function! flog#get_commit_at_ref_spec(ref_spec) abort - let l:state = flog#get_state() - - let l:command = flog#get_fugitive_git_command() - \ . ' rev-parse --short ' . shellescape(a:ref_spec) - let l:hash = flog#systemlist(l:command)[0] - - if !has_key(l:state.commit_line_lookup, l:hash) - return v:null - endif - - return flog#get_commit_at_line(l:state.commit_line_lookup[l:hash]) -endfunction - -function! flog#get_commit_selection(...) abort - let l:firstline = get(a:, 1, v:null) - let l:lastline = get(a:, 2, v:null) - let l:should_swap = get(a:, 3, 0) - - if type(l:firstline) != v:t_string && type(l:firstline) != v:t_number - let l:firstline = "'<" - endif - - if type(l:lastline) != v:t_string && type(l:lastline) != v:t_number - let l:lastline = "'>" - endif - - let l:first_commit = flog#get_commit_at_line(l:firstline) - - if type(l:first_commit) != v:t_dict - return v:null - endif - - let l:last_commit = flog#get_commit_at_line(l:lastline) - - if type(l:last_commit) != v:t_dict - return v:null - endif - - return l:should_swap ? [l:last_commit, l:first_commit] : [l:first_commit, l:last_commit] -endfunction - -" Commit navigation {{{ - -function! flog#jump_commits(commits) abort - let l:state = flog#get_state() - - let l:current_commit = flog#get_commit_at_line() - if type(l:current_commit) != v:t_dict - return - endif - - let l:index = index(l:state.commits, l:current_commit) + a:commits - let l:index = min([max([l:index, 0]), len(l:state.commits) - 1]) - - let l:line = index(l:state.line_commits, l:state.commits[l:index]) + 1 - - if l:line >= 0 - exec l:line - endif -endfunction - -function! flog#next_commit() abort - call flog#set_jump_mark() - call flog#jump_commits(v:count1) -endfunction - -function! flog#previous_commit() abort - call flog#set_jump_mark() - call flog#jump_commits(-v:count1) -endfunction - -" }}} - -function! flog#copy_commits(...) range abort - let l:by_line = get(a:, 1, v:false) - let l:state = flog#get_state() - - let l:commits = flog#get_commit_selection(a:firstline, a:lastline) - - if type(l:commits) != v:t_list - return 0 - endif - - let [l:first_commit, l:last_commit] = l:commits - - let l:first_index = index(l:state.commits, l:first_commit) - - if l:by_line - let l:last_index = index(l:state.commits, l:last_commit) - else - let l:last_index = l:first_index + a:lastline - a:firstline - endif - - let l:commits = l:state.commits[l:first_index : l:last_index] - let l:commits = map(l:commits, 'v:val.short_commit_hash') - - return setreg(v:register, join(l:commits, ' ')) -endfunction - -" }}} - -" Ref operations {{{ - -function! flog#get_ref_at_line(...) abort - let l:line = get(a:, 1, '.') - if type(l:line) == v:t_string - let l:line = line(l:line) - endif - let l:state = flog#get_state() - return get(l:state.line_commit_refs, l:line - 1, v:null) -endfunction - -function! flog#jump_refs(refs) abort - let l:state = flog#get_state() - - if l:state.commit_refs == [] - return - endif - - let l:current_ref = flog#get_ref_at_line() - let l:current_commit = flog#get_commit_at_line() - if type(l:current_commit) != v:t_dict - return - endif - - let l:refs = a:refs - if l:refs < 0 && l:current_commit.ref_names_unwrapped ==# '' - let l:refs += 1 - endif - - if type(l:current_ref) != v:t_list - let l:index = -1 - else - let l:index = index(l:state.commit_refs, l:current_ref) - endif - let l:index = max([0, l:index + l:refs]) - if l:index >= len(l:state.commit_refs) - return - endif - - let l:line = index(l:state.line_commit_refs, l:state.commit_refs[l:index]) + 1 - - if l:line >= 0 - exec l:line - endif -endfunction - -function! flog#jump_to_ref(ref) abort - let l:state = flog#get_state() - if !has_key(l:state.ref_line_lookup, a:ref) - return - endif - exec l:state.ref_line_lookup[a:ref] -endfunction - -function! flog#jump_to_commit(hash) abort - let l:state = flog#get_state() - if !has_key(l:state.commit_line_lookup, a:hash) - return - endif - exec l:state.commit_line_lookup[a:hash] -endfunction - -function! flog#next_ref() abort - call flog#set_jump_mark() - call flog#jump_refs(v:count1) -endfunction - -function! flog#previous_ref() abort - call flog#set_jump_mark() - call flog#jump_refs(-v:count1) -endfunction - -" }}} - -" Buffer management {{{ - -" Graph buffer {{{ - -" Graph buffer population {{{ - -function! flog#modify_graph_buffer_contents(content) abort - let l:state = flog#get_state() - - let l:cursor_pos = line('.') - - silent setlocal modifiable - silent setlocal noreadonly - 1,$ d - call setline(1, a:content) - call flog#graph_buffer_settings() - - exec l:cursor_pos - let l:state.line_commits = [] -endfunction - -function! flog#set_graph_buffer_commits(commits) abort - let l:state = flog#get_state() - - let l:state.line_commits = [] - - let l:state.commit_refs = [] - let l:state.line_commit_refs = [] - let l:state.ref_line_lookup = {} - let l:state.commit_line_lookup = {} - let l:state.ref_commit_lookup = {} - - let l:cr = v:null - - let l:scr = l:state.commit_refs - let l:srl = l:state.ref_line_lookup - let l:scl = l:state.commit_line_lookup - let l:slc = l:state.line_commits - let l:slr = l:state.line_commit_refs - - for l:c in a:commits - if l:c.ref_name_list !=# [] - let l:cr = l:c.ref_name_list - let l:scr += [l:cr] - for l:r in l:cr - let l:srl[l:r] = len(l:slc) + 1 - endfor - endif - - let l:scl[l:c.short_commit_hash] = len(slc) + 1 - - let l:slc += repeat([l:c], l:c.display_len) - let l:slr += repeat([l:cr], l:c.display_len) - endfor -endfunction - -function! flog#set_graph_buffer_title() abort - let l:opts = flog#get_resolved_graph_options() - - let l:title = 'flog-' . l:opts.instance - if l:opts.all && !l:opts.limit - let l:title .= ' [all]' - endif - if l:opts.bisect - let l:title .= ' [bisect]' - endif - if l:opts.no_merges - let l:title .= ' [no_merges]' - endif - if l:opts.reflog - let l:title .= ' [reflog]' - endif - if l:opts.reverse - let l:title .= ' [reverse]' - endif - if l:opts.no_graph - let l:title .= ' [no_graph]' - endif - if l:opts.no_patch - let l:title .= ' [no_patch]' - endif - if l:opts.skip != v:null - let l:title .= ' [skip=' . l:opts.skip . ']' - endif - if l:opts.sort != v:null - let l:title .= ' [sort=' . l:opts.sort . ']' - endif - if l:opts.max_count != v:null - let l:title .= ' [max_count=' . l:opts.max_count . ']' - endif - if l:opts.search != v:null - let l:title .= ' [search=' . flog#ellipsize(l:opts.search) . ']' - endif - if l:opts.patch_search != v:null - let l:title .= ' [patch_search=' . flog#ellipsize(l:opts.patch_search) . ']' - endif - if l:opts.author != v:null - let l:title .= ' [author=' . l:opts.author . ']' - endif - if l:opts.limit != v:null - let l:title .= ' [limit=' . flog#ellipsize(l:opts.limit) . ']' - endif - if len(l:opts.rev) == 1 - let l:title .= ' [rev=' . flog#ellipsize(l:opts.rev[0]) . ']' - endif - if len(l:opts.rev) > 1 - let l:title .= ' [rev=...]' - endif - if len(l:opts.path) == 1 - let l:title .= ' [path=' . flog#ellipsize(fnamemodify(l:opts.path[0], ':t')) . ']' - elseif len(l:opts.path) > 1 - let l:title .= ' [path=...]' - endif - - exec 'silent file '. l:title - - return l:title -endfunction - -function! flog#set_graph_buffer_color() abort - if get(g:, 'flog_use_ansi_esc') - let l:state = flog#get_state() - if !l:state.ansi_esc_called - AnsiEsc - let l:state.ansi_esc_called = 1 - else - AnsiEsc! - endif - endif -endfunction - -function! flog#get_graph_cursor() abort - let l:state = flog#get_state() - if l:state.line_commits != [] - return flog#get_commit_at_line() - endif - return v:null -endfunction - -function! flog#restore_graph_cursor(cursor) abort - if type(a:cursor) != v:t_dict - return - endif - - let l:state = flog#get_state() - - if len(l:state.commits) == 0 - return - endif - - let l:short_commit_hash = a:cursor.short_commit_hash - - let l:commit = flog#get_commit_at_line() - if type(l:commit) != v:t_dict - return - endif - if l:short_commit_hash ==# l:commit.short_commit_hash - return - endif - - return flog#jump_to_commit(l:short_commit_hash) -endfunction - -function! flog#populate_graph_buffer() abort - let l:state = flog#get_state() - let b:flog_status_summary = flog#get_status_summary() - - let l:cursor = flog#get_graph_cursor() - - let l:build_log_command_fn = get(g:, 'flog_build_log_command_fn', 'flog#build_log_command') - let l:command = call(l:build_log_command_fn, []) - let l:state.previous_log_command = l:command - - let l:output = flog#systemlist(l:command) - let [l:final_output, l:commits] = flog#parse_log_output(l:output) - - call flog#modify_graph_buffer_contents(l:final_output) - call flog#set_graph_buffer_commits(l:commits) - call flog#set_graph_buffer_title() - call flog#set_graph_buffer_color() - - let l:state.commits = l:commits - - call flog#restore_graph_cursor(l:cursor) -endfunction - -function! flog#graph_buffer_settings() abort - exec 'lcd ' . flog#get_state().workdir - set filetype=floggraph -endfunction - -function! flog#initialize_graph_buffer(state) abort - call flog#set_buffer_state(a:state) - call flog#trigger_fugitive_git_detection() - call flog#graph_buffer_settings() - call flog#populate_graph_buffer() -endfunction - -" }}} - -" Graph buffer settings {{{ - -function! flog#update_options(args, force) abort - let l:state = flog#get_state() - let l:defaults = flog#get_internal_default_args() - - if a:force - call extend(l:state, l:defaults) - endif - - call flog#parse_set_args(a:args, l:state, l:defaults) - - call flog#populate_graph_buffer() -endfunction - -function! flog#toggle_all_refs_option() abort - let l:state = flog#get_state() - let l:state.all = l:state.all ? v:false : v:true - call flog#populate_graph_buffer() -endfunction - -function! flog#toggle_bisect_option() abort - let l:state = flog#get_state() - let l:state.bisect = l:state.bisect ? v:false : v:true - call flog#populate_graph_buffer() -endfunction - -function! flog#toggle_no_merges_option() abort - let l:state = flog#get_state() - let l:state.no_merges = l:state.no_merges ? v:false : v:true - call flog#populate_graph_buffer() -endfunction - -function! flog#toggle_reflog_option() abort - let l:state = flog#get_state() - let l:state.reflog = l:state.reflog ? v:false : v:true - call flog#populate_graph_buffer() -endfunction - -function! flog#toggle_reverse_option() abort - let l:state = flog#get_state() - let l:state.reverse = l:state.reverse ? v:false : v:true - call flog#populate_graph_buffer() -endfunction - -function! flog#toggle_no_graph_option() abort - let l:state = flog#get_state() - let l:state.no_graph = l:state.no_graph ? v:false : v:true - call flog#populate_graph_buffer() -endfunction - -function! flog#toggle_no_patch_option() abort - let l:state = flog#get_state() - let l:state.no_patch = l:state.no_patch ? v:false : v:true - call flog#populate_graph_buffer() -endfunction - -function! flog#set_skip_option(skip) abort - let l:state = flog#get_state() - let l:state.skip = a:skip - call flog#populate_graph_buffer() -endfunction - -function! flog#change_skip_by_max_count(multiplier) abort - let l:state = flog#get_state() - if a:multiplier == 0 || l:state.max_count == v:null - return - endif - if l:state.skip == v:null - let l:state.skip = 0 - endif - let l:state.skip = max([0, l:state.skip + l:state.max_count * a:multiplier]) - call flog#populate_graph_buffer() -endfunction - -function! flog#set_sort_option(sort) abort - let l:state = flog#get_state() - let l:state.sort = a:sort - call flog#populate_graph_buffer() -endfunction - -function! flog#cycle_sort_option() abort - let l:state = flog#get_state() - - if l:state.sort == v:null - let l:state.sort = g:flog_sort_types[0].name - else - let l:sort_type = flog#get_sort_type(l:state.sort) - let l:sort_index = index(g:flog_sort_types, l:sort_type) - if l:sort_index == len(g:flog_sort_types) - 1 - let l:state.sort = g:flog_sort_types[0].name - else - let l:state.sort = g:flog_sort_types[l:sort_index + 1].name - endif - endif - - call flog#populate_graph_buffer() -endfunction - -" }}} - -" Graph buffer update hook {{{ - -function! flog#clear_graph_update_hook() abort - augroup FlogGraphUpdate - autocmd! * - augroup END -endfunction - -function! flog#get_status_summary() abort - let l:command = flog#get_fugitive_git_command() - let l:changes = len(systemlist(l:command . ' status -s')) - if l:changes == 0 - let l:change_summary = 'no changes' - else - let l:change_summary = l:changes . ' changed file' - endif - if l:changes > 1 - let l:change_summary = l:change_summary . 's' - endif - let l:branch = systemlist(l:command . ' rev-parse --abbrev-ref HEAD')[0] - return '(' . l:branch . ')' . ' ' . l:change_summary -endfunction - -function! flog#do_graph_update_hook(graph_buff_num) abort - if bufnr() != a:graph_buff_num - return - endif - - call flog#clear_graph_update_hook() - call flog#populate_graph_buffer() -endfunction - -function! flog#initialize_graph_update_hook(graph_buff_num) abort - augroup FlogGraphUpdate - - exec 'autocmd! * ' - if exists('##SafeState') - exec 'autocmd SafeState call flog#do_graph_update_hook(' . a:graph_buff_num . ')' - elseif has('nvim') - exec 'autocmd WinEnter call timer_start(0, {-> flog#do_graph_update_hook(' . a:graph_buff_num . ')})' - else - exec 'autocmd WinEnter call flog#do_graph_update_hook(' . a:graph_buff_num . ')' - endif - augroup END -endfunction - -" }}} - -" }}} - -" Command buffers {{{ - -function! flog#tmp_buffer_settings() abort - call flog#deprecate_autocmd('FlogPreviewSetup', 'FlogTmpWinSetup') - call flog#deprecate_autocmd('FlogTmpCommandWinSetup', 'FlogTmpWinSetup') - call flog#deprecate_autocmd('FlogTmpWinSetup', 'FlogTmpCmdBufferSetup') - silent doautocmd User FlogTmpCmdBufferSetup -endfunction - -function! flog#non_tmp_cmd_buffer_settings() abort - silent doautocmd User FlogNonTmpCmdBufferSetup -endfunction - -function! flog#cmd_buffer_settings() abort - silent doautocmd User FlogCmdBufferSetup -endfunction - -function! flog#initialize_cmd_buffer(state, is_tmp) abort - if a:is_tmp - call flog#set_buffer_state(a:state) - call flog#cmd_buffer_settings() - call flog#tmp_buffer_settings() - else - call flog#set_buffer_state(a:state) - call flog#cmd_buffer_settings() - call flog#non_tmp_cmd_buffer_settings() - endif -endfunction - -function! flog#initialize_tmp_cmd_buffer(state) abort - return flog#initialize_cmd_buffer(a:state, 1) -endfunction - -" }}} - -" }}} - -" Layout management {{{ - -" Command window layout management {{{ - -function! flog#close_tmp_win() abort - let l:state = flog#get_state() - let l:graph_window_id = win_getid() - - for l:tmp_window_id in l:state.tmp_cmd_window_ids - " temporary buffer is not open - if win_id2tabwin(l:tmp_window_id) == [0, 0] - continue - endif - - " get the previous buffer to switch back to it after closing - call win_gotoid(l:tmp_window_id) - close! - endfor - - let l:state.tmp_cmd_window_ids = [] - - " go back to the previous window - call win_gotoid(l:graph_window_id) - - return -endfunction - -function! flog#open_cmd(cmd, is_tmp) abort - let l:state = flog#get_state() - - let l:graph_window_id = win_getid() - let l:saved_window_ids = flog#get_all_window_ids() - - exec a:cmd - let l:final_cmd_window_id = win_getid() - - silent! let l:new_window_ids = flog#exclude(flog#get_all_window_ids(), l:saved_window_ids) - let l:state.last_cmd_window_ids = l:new_window_ids - - if l:new_window_ids != [] - silent! call win_gotoid(l:graph_window_id) - - if a:is_tmp - silent! call flog#close_tmp_win() - let l:state.tmp_cmd_window_ids = l:new_window_ids - endif - - for l:new_window_id in l:new_window_ids - silent! call win_gotoid(l:new_window_id) - if !flog#has_state() - silent! call flog#initialize_cmd_buffer(l:state, a:is_tmp) - endif - endfor - - silent! call win_gotoid(l:final_cmd_window_id) - endif -endfunction - -function! flog#open_tmp_cmd(cmd) abort - return flog#open_cmd(a:cmd, 1) -endfunction - -" }}} - -" Graph layout management {{{ - -function! flog#open_graph(state) abort - " grab git dir before opening a new buffer so we still have the opened file - let b:git_dir = flog#get_fugitive_git_dir() - - let l:window_name = 'flog-' . a:state.instance . ' [uninitialized]' - silent exec a:state.open_cmd . ' ' . l:window_name - - let a:state.graph_window_id = win_getid() - - call flog#initialize_graph_buffer(a:state) -endfunction - -function! flog#open(args) abort - if !flog#is_fugitive_buffer() - throw g:flog_not_a_fugitive_buffer - endif - - let l:original_file = expand('%:p') - - let l:parsed_args = flog#parse_args(a:args) - let l:initial_state = flog#get_initial_state(l:parsed_args, l:original_file) - - call flog#open_graph(l:initial_state) -endfunction - -function! flog#quit() abort - let l:flog_tab = tabpagenr() - let l:tabs = tabpagenr('$') - call flog#close_tmp_win() - quit! - if l:tabs > tabpagenr('$') && l:flog_tab == tabpagenr() - tabprev - endif -endfunction - -" }}} - -" }}} - -" Commit marks {{{ - -function! flog#is_reserved_commit_mark(key) abort - return a:key =~# '[<>@~^!]' -endfunction - -function! flog#is_dynamic_commit_mark(key) abort - return a:key =~# '[<>@~^]' -endfunction - -function! flog#is_cancel_commit_mark(key) abort - " 27 is the code for - return char2nr(a:key) == 27 -endfunction - -function! flog#get_commit_marks() abort - return flog#get_state().commit_marks -endfunction - -function! flog#reset_commit_marks() abort - let l:state = flog#get_state() - let l:state.commit_marks = {} -endfunction - -function! flog#set_internal_commit_mark(key, commit) abort - if flog#is_cancel_commit_mark(a:key) - return - endif - let l:marks = flog#get_commit_marks() - let l:marks[a:key] = a:commit -endfunction - -function! flog#set_commit_mark(key, commit) abort - if flog#is_reserved_commit_mark(a:key) - throw g:flog_invalid_mark - endif - return flog#set_internal_commit_mark(a:key, a:commit) -endfunction - -function! flog#set_internal_commit_mark_at_line(key, line) abort - let l:commit = flog#get_commit_at_line(a:line) - return flog#set_internal_commit_mark(a:key, l:commit) -endfunction - -function! flog#set_commit_mark_at_line(key, line) abort - let l:commit = flog#get_commit_at_line(a:line) - return flog#set_commit_mark(a:key, l:commit) -endfunction - -function! flog#set_jump_mark(...) abort - let l:line = a:0 >= 1 ? a:1 : line('.') - call flog#set_commit_mark_at_line("'", l:line) -endfunction - -function! flog#remove_commit_mark(key) abort - let l:marks = flog#get_commit_marks() - unlet! l:marks[a:key] -endfunction - -function! flog#has_commit_mark(key) abort - if flog#is_dynamic_commit_mark(a:key) - return 1 - endif - if flog#is_cancel_commit_mark(a:key) - throw g:flog_invalid_mark - endif - let l:marks = flog#get_commit_marks() - return has_key(l:marks, a:key) -endfunction - -function! flog#get_commit_mark(key) abort - if a:key ==# '<' || a:key ==# '>' - return flog#get_commit_at_line("'" . a:key) - endif - - if a:key ==# '@' - return flog#get_commit_at_ref('HEAD') - endif - - if a:key ==# '~' || a:key ==# '^' - return flog#get_commit_at_ref_spec('HEAD~') - endif - - if flog#is_cancel_commit_mark(a:key) - throw g:flog_invalid_mark - endif - - if !flog#has_commit_mark(a:key) - return v:null - endif - - let l:marks = flog#get_commit_marks() - return l:marks[a:key] -endfunction - -function! flog#jump_to_commit_mark(key) abort - let l:previous_line = line('.') - let l:commit = flog#get_commit_mark(a:key) - if type(l:commit) != v:t_dict - return - endif - call flog#jump_to_commit(l:commit.short_commit_hash) - call flog#set_jump_mark(l:previous_line) -endfunction - -function! flog#echo_commit_marks() abort - let l:marks = flog#get_commit_marks() - if empty(l:marks) - echo 'No commit marks.' - return - endif - for l:key in sort(keys(l:marks)) - echo ' ' . l:key . ' ' . l:marks[l:key].short_commit_hash - endfor -endfunction - -" }}} - -" Command utilities {{{ - -" Command formatting {{{ - -" Command formatting helpers {{{ - -function! flog#is_remote_ref(ref) abort - let l:split_ref = split(a:ref, '/') - if len(l:split_ref) < 2 - return 0 - endif - return index(flog#get_remotes(), l:split_ref[0]) >= 0 -endfunction - -function! flog#get_cache_refs(cache, commit) abort - if type(a:commit) != v:t_dict - return v:null - endif - - let l:ref_cache = a:cache['refs'] - - let l:hash = a:commit.short_commit_hash - - if !has_key(l:ref_cache, l:hash) - if empty(a:commit.ref_name_list) - return v:null - endif - let l:refs = a:commit.ref_name_list - - let l:original_refs = split( - \ a:commit.ref_names_unwrapped, '\( \ze-> \|, \|\zetag: \)\+') - - let l:remote_branches = [] - let l:local_branches = [] - let l:special = [] - let l:tags = [] - - let l:i = 0 - while l:i < len(l:refs) - let l:ref = l:refs[l:i] - - if l:ref =~# 'HEAD$\|^refs/' - call add(l:special, l:ref) - elseif l:original_refs[l:i] =~# '^tag: ' - call add(l:tags, l:ref) - elseif flog#is_remote_ref(l:ref) - call add(l:remote_branches, l:ref) - else - call add(l:local_branches, l:ref) - endif - - let l:i += 1 - endwhile - - let l:ref_cache[l:hash] = { - \ 'local_branches': l:local_branches, - \ 'remote_branches': l:remote_branches, - \ 'tags': l:tags, - \ 'special': l:special, - \ } - endif - - return l:ref_cache[l:hash] -endfunction - -" }}} - -" Command format specifier converters {{{ - -function! flog#cmd_convert_hash(cache, item, commit) abort - return flog#get(a:commit, 'short_commit_hash') -endfunction - -function! flog#cmd_convert_branch(cache, item, commit) abort - let l:refs = flog#get_cache_refs(a:cache, a:commit) - - if type(l:refs) != v:t_dict - return v:null - endif - - let l:local_branches = flog#get(l:refs, 'local_branches', []) - let l:remote_branches = flog#get(l:refs, 'remote_branches', []) - - let l:branch = get(l:local_branches, 0, get(l:remote_branches, 0, v:null)) - if type(l:branch) != v:t_string - return v:null - endif - return shellescape(l:branch) -endfunction - -function! flog#cmd_convert_local_branch(cache, item, commit) abort - let l:refs = flog#get_cache_refs(a:cache, a:commit) - - if type(l:refs) != v:t_dict - return v:null - endif - - let l:local_branches = flog#get(l:refs, 'local_branches', []) - let l:remote_branches = flog#get(l:refs, 'remote_branches', []) - - if empty(l:local_branches) - if empty(l:remote_branches) - return v:null - endif - return shellescape(substitute(l:remote_branches[0], '.*/', '', '')) - endif - return shellescape(l:local_branches[0]) -endfunction - -function! flog#cmd_convert_line(cache, item, Convert) abort - return a:Convert(a:cache, a:item, flog#get_commit_at_line('.')) -endfunction - -function! flog#cmd_convert_commit_mark(cache, item, Convert) abort - let l:commit = flog#get_commit_mark(a:item[2:]) - if type(l:commit) != v:t_dict - return v:null - endif - - return a:Convert(a:cache, a:item, l:commit) -endfunction - -function! flog#cmd_convert_path(cache, item) abort - let l:state = flog#get_state() - if empty(l:state.path) - return v:null - endif - return join(map(l:state.path, 'fnameescape(v:val)'), ' ') -endfunction - -function! flog#cmd_convert_tree(cache, item) abort - if empty(a:cache.index_tree) - let l:cmd = flog#get_fugitive_git_command() - let l:cmd .= ' write-tree' - let a:cache.index_tree = flog#systemlist(l:cmd)[0] - endif - return a:cache.index_tree -endfunction - -function! flog#convert_command_format_item(cache, item) abort - let l:item_cache = a:cache['items'] - - " return any cached data - - if has_key(l:item_cache, a:item) - return l:item_cache[a:item] - endif - - " convert the specifier - - let l:converted_item = v:null +function! flog#Exec(cmd, focus, static, tmp) abort + if empty(a:cmd) + return '' + end - if a:item ==# 'h' - call flog#set_internal_commit_mark_at_line('!', '.') - let l:converted_item = flog#cmd_convert_line(a:cache, a:item, function('flog#cmd_convert_hash')) - elseif a:item ==# 'H' - let l:converted_item = flog#cmd_convert_line(a:cache, a:item, function('flog#cmd_convert_hash')) - elseif a:item =~# "^h'." - let l:converted_item = flog#cmd_convert_commit_mark(a:cache, a:item, function('flog#cmd_convert_hash')) - elseif a:item =~# 'b' - let l:converted_item = flog#cmd_convert_line(a:cache, a:item, function('flog#cmd_convert_branch')) - elseif a:item =~# "^b'." - let l:converted_item = flog#cmd_convert_commit_mark(a:cache, a:item, function('flog#cmd_convert_branch')) - elseif a:item =~# 'l' - let l:converted_item = flog#cmd_convert_line(a:cache, a:item, function('flog#cmd_convert_local_branch')) - elseif a:item =~# "^l'." - let l:converted_item = flog#cmd_convert_commit_mark(a:cache, a:item, function('flog#cmd_convert_local_branch')) - elseif a:item =~# 'p' - let l:converted_item = flog#cmd_convert_path(a:cache, a:item) - elseif a:item ==# 't' - let l:converted_item = flog#cmd_convert_tree(a:cache, a:item) - else - echoerr printf('error converting %s', a:item) - throw g:flog_unsupported_command_format_item + if !flog#floggraph#buf#IsFlogBuf() + exec a:cmd + return a:cmd endif - " handle result - - let l:item_cache[a:item] = l:converted_item - return l:converted_item -endfunction - -" }}} - -function! flog#format_command(format) abort - " special token flags - let l:is_in_item = 0 - let l:is_in_long_item = 0 - let l:is_in_long_item_escape = 0 - - " special token data - let l:long_item = '' - - " memoized data - let l:cache = { - \ 'items': {}, - \ 'refs': {}, - \ 'index_tree': '' - \ } - - " return data - let l:ret = '' - - for l:char in split(a:format, '\zs') - " parse characters in %() - if l:is_in_long_item - if l:char ==# ')' - " end long specifier - let l:converted_item = flog#convert_command_format_item(l:cache, l:long_item) - if type(l:converted_item) != v:t_string - return v:null - endif - let l:ret .= l:converted_item - - let l:is_in_long_item = 0 - let l:long_item = '' - else - " build specifier - let l:long_item .= l:char - endif - continue - endif - - " parse character after % - if l:is_in_item - if l:char ==# '(' - " start long specifier - let l:is_in_long_item = 1 - else - " parse specifier chacter - let l:converted_item = flog#convert_command_format_item(l:cache, l:char) - if type(l:converted_item) != v:t_string - return v:null - endif - let l:ret .= l:converted_item - endif - - let l:is_in_item = 0 - continue - endif - - " parse normal character - if l:char ==# '%' - let l:is_in_item = 1 - else - let l:ret .= l:char - endif - endfor - - return l:ret -endfunction - -" }}} - -" Command running {{{ - -function! flog#handle_cmd_win_cleanup(keep_focus, graph_window_id) abort - if !a:keep_focus - call win_gotoid(a:graph_window_id) - if has('nvim') - redraw! - endif - endif -endfunction + let l:graph_win = flog#win#Save() + call flog#floggraph#side_win#Open(a:cmd, a:focus, a:tmp) -function! flog#handle_cmd_update_cleanup(should_update, graph_window_id, graph_buff_num) abort - if a:should_update - if win_getid() != a:graph_window_id - call flog#initialize_graph_update_hook(a:graph_buff_num) + if ! a:static + if flog#win#Is(l:graph_win) + call flog#floggraph#buf#Update() else - call flog#populate_graph_buffer() + call flog#floggraph#buf#InitUpdateHook(flog#win#GetSavedBufnr(l:graph_win)) endif endif -endfunction - -function! flog#handle_cmd_cleanup(keep_focus, should_update, graph_window_id, graph_buff_num) abort - call flog#handle_cmd_win_cleanup(a:keep_focus, a:graph_window_id) - call flog#handle_cmd_update_cleanup(a:should_update, a:graph_window_id, a:graph_buff_num) -endfunction - -function! flog#run_raw_command(command, ...) abort - let l:keep_focus = get(a:, 1, v:false) - let l:should_update = get(a:, 2, v:false) - let l:is_tmp = get(a:, 3, v:false) - - " Not running in a graph buffer - if !flog#has_state() - exec a:command - return - endif - - let l:graph_window_id = win_getid() - let l:graph_buff_num = bufnr('') - - if type(a:command) != v:t_string - return - endif - - call flog#open_cmd(a:command, l:is_tmp) - silent! call flog#handle_cmd_cleanup( - \ l:keep_focus, l:should_update, l:graph_window_id, l:graph_buff_num) -endfunction - -function! flog#run_command(command, ...) abort - let l:keep_focus = get(a:, 1, v:false) - let l:should_update = get(a:, 2, v:false) - let l:is_tmp = get(a:, 3, v:false) - - let l:command = flog#format_command(a:command) - - call flog#run_raw_command(l:command, l:keep_focus, l:should_update, l:is_tmp) -endfunction - -function! flog#run_tmp_command(command, ...) abort - let l:keep_focus = get(a:, 1, v:false) - let l:should_update = get(a:, 2, v:false) - - call flog#run_command(a:command, l:keep_focus, l:should_update, v:true) -endfunction - -" }}} - -" }}} - -" Deprecated functions {{{ - -function! flog#shell_command(...) range abort - call flog#deprecate_function('flog#shell_command', 'flog#systemlist', '{command}') -endfunction - -function! flog#get_commit_data(...) range abort - call flog#deprecate_function( - \ 'flog#get_commit_data', - \ 'flog#get_commit_at_line', - \ '[line]') -endfunction - -function! flog#get_ref_data(...) range abort - call flog#deprecate_function('flog#get_ref_data', 'flog#get_ref_at_line', '[line]') -endfunction - -function! flog#close_preview(...) range abort - call flog#deprecate_function('flog#close_preview', 'flog#close_tmp_win', '') -endfunction - -function! flog#preview(...) range abort - call flog#deprecate_function('flog#preview', 'flog#run_tmp_command', '{command}, [keep_focus], [should_update]') -endfunction - -function! flog#preview_commit(...) range abort - call flog#deprecate_function( - \ 'flog#preview_commit', - \ 'flog#run_tmp_command', - \ '"MyCommand %h", [keep_focus], [should_update]') -endfunction - -function! flog#preview_split_commit(...) range abort - call flog#deprecate_function( - \ 'flog#preview_split_commit', - \ 'flog#run_tmp_command', - \ '"(mods) Gsplit %h", [keep_focus]') -endfunction - -function! flog#git(...) range abort - call flog#deprecate_function( - \ 'flog#git', - \ 'flog#run_command', - \ '"(mods) Git (cmd)", [keep_focus], [should_update], [is_tmp]') -endfunction - -function! flog#format(...) range abort - call flog#deprecate_function( - \ 'flog#format', - \ 'flog#run_command', - \ '"(format)", ...') -endfunction - -function! flog#join(...) range abort - call flog#deprecate_function('flog#join', 'flog#run_command') -endfunction - -function! flog#get_paths(...) range abort - call flog#deprecate_function( - \ 'flog#get_paths', - \ 'flog#run_command', - \ '"MyCommand %p", ...') -endfunction - -function! flog#get_hash_at_line(...) range abort - call flog#deprecate_function( - \ 'flog#get_hash_at_line', - \ 'flog#run_command', - \ '"MyCommand %h", ...') -endfunction - -function! flog#format_commit(...) range abort - call flog#deprecate_function( - \ 'flog#format_commit', - \ 'flog#run_command', - \ '"MyCommand %h", ...') -endfunction - -function! flog#format_commit_selection(...) range abort - call flog#deprecate_function( - \ 'flog#format_commit_selection', - \ 'flog#run_command', - \ "\"MyCommand %(h'<) %(h'>)\", ...") -endfunction - -function! flog#get_branch_at_line(...) range abort - call flog#deprecate_function( - \ 'flog#get_branch_at_line', - \ 'flog#run_command', - \ '"MyCommand %b", ...') -endfunction - -function! flog#get_local_branch_at_line(...) range abort - call flog#deprecate_function( - \ 'flog#get_local_branch_at_line', - \ 'flog#run_command', - \ '"MyCommand %l", ...') -endfunction -function! flog#get_cache_curr_line_refs(...) range abort - call flog#deprecate_function( - \ 'flog#get_cache_curr_line_refs', - \ 'flog#get_cache_refs', - \ '(cache), "."') + return a:cmd endfunction -function! flog#cmd_item_hash_at_curr_line(...) range abort - call flog#deprecate_function( - \ 'flog#cmd_item_hash_at_curr_line', - \ 'flog#cmd_item_hash_at_curr_line', - \ '(cache), (item), "."') +function! flog#ExecTmp(cmd, focus, static) abort + return flog#Exec(a:cmd, a:focus, a:static, v:true) endfunction -function! flog#cmd_item_hash(...) range abort - call flog#deprecate_function( - \ 'flog#cmd_item_hash', - \ 'flog#cmd_convert_hash', - \ '(cache), (item), (line)') +function! flog#Format(cmd) abort + return flog#format#FormatCommand(a:cmd) endfunction -function! flog#cmd_item_branch(...) range abort - call flog#deprecate_function( - \ 'flog#cmd_item_branch', - \ 'flog#cmd_convert_branch', - \ '(cache), (item), "."') -endfunction +" Deprecations -function! flog#cmd_item_local_branch(...) range abort - call flog#deprecate_function( - \ 'flog#cmd_item_local_branch', - \ 'flog#cmd_convert_local_branch', - \ '(cache), (item), "."') +function! flog#run_raw_command(...) abort + call flog#deprecate#Function('flog#run_raw_command', 'flog#Exec') endfunction -function! flog#cmd_item_path(...) range abort - call flog#deprecate_function( - \ 'flog#cmd_item_path', - \ 'flog#cmd_convert_path', - \ '(cache), (item)') +function! flog#run_command(...) abort + call flog#deprecate#Function('flog#run_command', 'flog#Exec', 'flog#Format(...), ...') endfunction -function! flog#open_tmp_win(...) abort - call flog#deprecate_function('flog#open_tmp_win', 'flog#open_tmp_cmd') +function! flog#run_tmp_command(...) abort + call flog#deprecate#Function('flog#run_tmp_command', 'flog#ExecTmp') endfunction - -" }}} - -" vim: set et sw=2 ts=2 fdm=marker: diff --git a/autoload/flog/args.vim b/autoload/flog/args.vim new file mode 100644 index 0000000..7084d21 --- /dev/null +++ b/autoload/flog/args.vim @@ -0,0 +1,61 @@ +" +" This file contains functions for handling args to commands. +" + +function! flog#args#SplitArg(arg) abort + let l:match = matchlist(a:arg, '\v(.{-}(\=|$))(.*)') + return [l:match[1], l:match[3]] +endfunction + +function! flog#args#ParseArg(arg) abort + return flog#args#SplitArg(a:arg)[1] +endfunction + +function! flog#args#UnescapeArg(arg) abort + let l:unescaped = '' + let l:is_escaped = v:false + + for l:char in split(a:arg, '\zs') + if l:char ==# '\' && !l:is_escaped + let l:is_escaped = v:true + else + let l:unescaped .= l:char + let l:is_escaped = v:false + endif + endfor + + return l:unescaped +endfunction + +function! flog#args#SplitGitLimitArg(limit) abort + let [l:match, l:start, l:end] = matchstrpos(a:limit, '^.\{1,}:\zs') + if l:start < 0 + return [a:limit, ''] + endif + return [a:limit[ : l:start - 1], a:limit[l:start : ]] +endfunction + +function! flog#args#ParseGitLimitArg(workdir, arg) abort + let l:arg_opt = flog#args#ParseArg(a:arg) + let [l:range, l:path] = flog#args#SplitGitLimitArg(l:arg_opt) + + if empty(l:path) + return l:arg_opt + endif + + return l:range . flog#fugitive#GetRelativePath(a:workdir, expand(l:path)) +endfunction + +function! flog#args#ParseGitPathArg(workdir, arg) abort + let l:arg_opt = flog#args#ParseArg(a:arg) + return flog#fugitive#GetRelativePath(a:workdir, expand(l:arg_opt)) +endfunction + +function! flog#args#FilterCompletions(arg_lead, completions) abort + let l:lead = '^' . escape(a:arg_lead, '\\') + return filter(copy(a:completions), 'v:val =~# l:lead') +endfunction + +function! flog#args#EscapeCompletions(lead, completions) abort + return map(copy(a:completions), 'a:lead . substitute(v:val, " ", "\\\\ ", "g")') +endfunction diff --git a/autoload/flog/cmd.vim b/autoload/flog/cmd.vim new file mode 100644 index 0000000..4133a57 --- /dev/null +++ b/autoload/flog/cmd.vim @@ -0,0 +1,56 @@ +" +" This file contains functions which implement Flog Vim commands. +" +" The "cmd/" folder contains functions for each command. +" + +" The implementation of ":Flog". +" The "floggraph/" folder contains functions for dealing with this filetype. +function! flog#cmd#Flog(args) abort + if !flog#fugitive#IsGitBuf() + throw g:flog_not_a_fugitive_buffer + endif + + let state = flog#state#Create() + + let workdir = flog#fugitive#GetWorkdir() + call flog#state#SetWorkdir(state, workdir) + + let default_opts = flog#state#GetDefaultOpts() + let opts = flog#cmd#flog#args#Parse(default_opts, workdir, a:args) + call flog#state#SetOpts(state, opts) + + if g:flog_write_commit_graph && !flog#git#HasCommitGraph() + call flog#git#WriteCommitGraph() + endif + + call flog#floggraph#buf#Open(state) + call flog#floggraph#buf#Update() + + return state +endfunction + +" The implementation of ":Flogsetargs". +function! flog#cmd#FlogSetArgs(args, force) abort + let state = flog#state#GetBufState() + + let workdir = flog#state#GetWorkdir(state) + let opts = a:force ? flog#state#GetInternalDefaultOpts() : state.opts + + call flog#cmd#flog#args#Parse(opts, workdir, a:args) + call flog#state#SetOpts(state, opts) + + call flog#floggraph#buf#Update() + + return state +endfunction + +" The implementation of ":Floggit". +function! flog#cmd#Floggit(mods, args, bang) abort + let l:split_args = split(a:args) + let l:parsed_args = flog#cmd#floggit#args#Parse(l:split_args) + let l:cmd = flog#cmd#floggit#args#ToGitCommand(a:mods, a:bang, l:parsed_args) + + return flog#Exec( + \ l:cmd, l:parsed_args.focus, l:parsed_args.static, l:parsed_args.tmp) +endfunction diff --git a/autoload/flog/cmd/flog/args.vim b/autoload/flog/cmd/flog/args.vim new file mode 100644 index 0000000..df804b2 --- /dev/null +++ b/autoload/flog/cmd/flog/args.vim @@ -0,0 +1,334 @@ +" +" This file contains functions for handling args to the ":Flog" command. +" + +" Parse ":Flog" args into the options object. +function! flog#cmd#flog#args#Parse(current_opts, workdir, args) abort + let l:defaults = flog#state#GetInternalDefaultOpts() + + let l:has_set_path = v:false + + let l:has_set_rev = v:false + + let l:has_set_raw_args = v:false + let l:got_raw_args_token = v:false + let l:raw_args = [] + + for l:arg in a:args + if l:got_raw_args_token + let l:has_set_raw_args = v:true + let l:raw_args += [l:arg] + elseif l:arg ==# '--' + let l:got_raw_args_token = v:true + elseif l:arg =~# '^-format=.\+' + let a:current_opts.format = flog#args#ParseArg(l:arg) + elseif l:arg ==# '-format=' + let a:current_opts.format = l:defaults.format + elseif l:arg =~# '^-date=.\+' + let a:current_opts.date = flog#args#ParseArg(l:arg) + elseif l:arg ==# '-date=' + let a:current_opts.date = l:defaults.date + elseif l:arg =~# '^-raw-args=.\+' + let l:has_set_raw_args = v:true + let l:raw_args += [flog#args#ParseArg(l:arg)] + elseif l:arg ==# '-raw-args=' + let l:has_set_raw_args = v:false + let a:current_opts.raw_args = l:defaults.raw_args + elseif l:arg ==# '-all' + let a:current_opts.all = v:true + elseif l:arg ==# '-no-all' + let a:current_opts.all = v:false + elseif l:arg ==# '-bisect' + let a:current_opts.bisect = v:true + elseif l:arg ==# '-no-bisect' + let a:current_opts.bisect = v:false + elseif l:arg ==# '-merges' + let a:current_opts.merges = v:true + elseif l:arg ==# '-no-merges' + let a:current_opts.merges = v:false + elseif l:arg ==# '-reflog' + let a:current_opts.reflog = v:true + elseif l:arg ==# '-no-reflog' + let a:current_opts.reflog = v:false + elseif l:arg ==# '-reverse' + let a:current_opts.reverse = v:true + elseif l:arg ==# '-no-reverse' + let a:current_opts.reverse = v:false + elseif l:arg ==# '-graph' + let a:current_opts.graph = v:true + elseif l:arg ==# '-no-graph' + let a:current_opts.graph = v:false + elseif l:arg ==# '-patch' + let a:current_opts.patch = v:true + elseif l:arg ==# '-no-patch' + let a:current_opts.patch = v:false + elseif l:arg =~# '^-skip=\d\+' + let a:current_opts.skip = flog#args#ParseArg(l:arg) + elseif l:arg ==# '-skip=' + let a:current_opts.skip = l:defaults.skip + elseif l:arg =~# '^-\(order\|sort\)=.\+' + let a:current_opts.order = flog#args#ParseArg(l:arg) + elseif l:arg ==# '-order=' || l:arg ==# '-sort=' + let a:current_opts.order = l:defaults.order + elseif l:arg =~# '^-max-count=\d\+' + let a:current_opts.max_count = flog#args#ParseArg(l:arg) + elseif l:arg ==# '-max-count=' + let a:current_opts.max_count = l:defaults.max_count + elseif l:arg =~# '^-open-cmd=.\+' + let a:current_opts.open_cmd = flog#args#ParseArg(l:arg) + elseif l:arg ==# '-open-cmd=' + let a:current_opts.open_cmd = l:defaults.open_cmd + elseif l:arg =~# '^-\(search\|grep\)=.\+' + let a:current_opts.search = flog#args#ParseArg(l:arg) + elseif l:arg ==# '-search=' || l:arg ==# '-grep=' + let a:current_opts.search = l:defaults.search + elseif l:arg =~# '^-patch-\(search\|grep\)=.\+' + let a:current_opts.patch_search = flog#args#ParseArg(l:arg) + elseif l:arg ==# '-patch-search=' || l:arg ==# '-patch-grep=' + let a:current_opts.patch_search = l:defaults.patch_search + elseif l:arg =~# '^-author=.\+' + let a:current_opts.author = flog#args#ParseArg(l:arg) + elseif l:arg ==# '-author=' + let a:current_opts.author = l:defaults.author + elseif l:arg =~# '^-limit=.\+' + let a:current_opts.limit = flog#args#ParseGitLimitArg(a:workdir, l:arg) + elseif l:arg ==# '-limit=' + let a:current_opts.limit = l:defaults.limit + elseif l:arg =~# '^-rev=.\+' + if !l:has_set_rev + let a:current_opts.rev = [] + let l:has_set_rev = v:true + endif + call add(a:current_opts.rev, flog#args#ParseArg(l:arg)) + elseif l:arg ==# '-rev=' + let l:has_set_rev = v:false + let a:current_opts.rev = l:defaults.rev + elseif l:arg =~# '^-path=.\+' + if !l:has_set_path + let a:current_opts.path = [] + let l:has_set_path = v:true + endif + call add(a:current_opts.path, flog#args#ParseGitPathArg(a:workdir, l:arg)) + elseif l:arg ==# '-path=' + let a:current_opts.path = l:defaults.path + let l:has_set_path = v:false + else + echoerr 'error parsing argument ' . l:arg + throw g:flog_unsupported_argument + endif + endfor + + if l:has_set_raw_args + let a:current_opts.raw_args = join(l:raw_args, ' ') + endif + + return a:current_opts +endfunction + +function! flog#cmd#flog#args#CompleteFormat(arg_lead) abort + let l:is_escaped = v:false + let l:current_specifier = '' + let l:current_parens = '' + + " Find last specifier (handles escaped % signs) + for l:c in a:arg_lead + if l:c ==# '%' + if l:current_specifier ==# '%' + " Literal percent + let l:current_specifier = '%%' + else + " New specifier + let l:current_specifier = '%' + let l:current_parens = '' + endif + elseif l:current_specifier ==# '' + continue + elseif l:current_specifier ==# '%%' + let l:current_specifier = '' + let l:current_parens = '' + elseif l:current_specifier =~# '($' + if l:c ==# ')' + " End of parens/specifier + let l:current_specifier = '' + let l:current_parens = '' + else + " Inside parens + let l:current_parens .= l:c + endif + else + let l:current_specifier .= l:c + endif + endfor + + " Inside of parens, end parens + if !empty(l:current_parens) + return [a:arg_lead . ')'] + endif + + let l:completions = [] + let l:l = len(l:current_specifier) + + " Find specifiers that start with the current specifier + if l:l > 0 + let l:prefix = a:arg_lead[ : -l:l - 1] + + for l:specifier in g:flog_format_specifiers + if stridx(specifier, l:current_specifier) == 0 + call add(l:completions, l:prefix . l:specifier) + endif + endfor + endif + + " No specifier, start a new one + if empty(l:completions) + return [a:arg_lead . '%'] + endif + + return l:completions +endfunction + +function! flog#cmd#flog#args#CompleteDate(arg_lead) abort + let [l:lead, _] = flog#args#SplitArg(a:arg_lead) + let l:completions = map(copy(g:flog_date_formats), 'l:lead . v:val') + return flog#args#FilterCompletions(a:arg_lead, l:completions) +endfunction + +function! flog#cmd#flog#args#CompleteOpenCmd(arg_lead) abort + let [l:lead, _] = flog#args#SplitArg(a:arg_lead) + + let l:completions = g:flog_open_cmds + g:flog_open_cmd_modifiers + + " Add combined open commands + for l:modifier in g:flog_open_cmd_modifiers + for l:open_cmd in g:flog_open_cmds + call add(l:completions, l:modifier . ' ' . l:open_cmd) + endfor + endfor + + let l:completions = flog#args#EscapeCompletions(l:lead, l:completions) + + return flog#args#FilterCompletions(a:arg_lead, l:completions) +endfunction + +function! flog#cmd#flog#args#CompleteAuthor(arg_lead) abort + if !flog#fugitive#IsGitBuf() + return [] + endif + + let [l:lead, _] = flog#args#SplitArg(a:arg_lead) + let l:completions = flog#git#GetAuthors() + return flog#args#FilterCompletions( + \ a:arg_lead, + \ flog#args#EscapeCompletions(l:lead, l:completions) + \ ) +endfunction + +function! flog#cmd#flog#args#CompleteLimit(arg_lead) abort + let [l:lead, l:limit] = flog#args#SplitArg(a:arg_lead) + + let [l:range, l:path] = flog#args#SplitGitLimitArg(l:limit) + if l:range !~# ':$' + return [] + endif + let l:path = flog#args#UnescapeArg(l:path) + + let l:completions = getcompletion(l:path, 'file') + let l:completions = flog#args#EscapeCompletions(l:lead . l:range, l:completions) + return flog#args#FilterCompletions(a:arg_lead, l:completions) +endfunction + +function! flog#cmd#flog#args#CompleteRev(arg_lead) abort + if !flog#fugitive#IsGitBuf() + return [] + endif + let [l:lead, _] = flog#args#SplitArg(a:arg_lead) + + let l:refs = flog#git#GetRefs() + + let l:completions = flog#args#EscapeCompletions(l:lead, l:refs) + return flog#args#FilterCompletions(a:arg_lead, l:completions) +endfunction + +function! flog#cmd#flog#args#CompletePath(arg_lead) abort + let [l:lead, l:path] = flog#args#SplitArg(a:arg_lead) + let l:path = flog#args#UnescapeArg(l:path) + + let l:files = getcompletion(l:path, 'file') + + let l:completions = flog#args#EscapeCompletions(l:lead, l:files) + return flog#args#FilterCompletions(a:arg_lead, l:completions) +endfunction + +function! flog#cmd#flog#args#CompleteOrder(arg_lead) abort + let [l:lead, _] = flog#args#SplitArg(a:arg_lead) + + let l:order_types = [] + for l:order_type in g:flog_order_types + call add(l:order_types, l:order_type.name) + endfor + + let l:completions = flog#args#EscapeCompletions(l:lead, l:order_types) + return flog#args#FilterCompletions(a:arg_lead, l:completions) +endfunction + +function! flog#cmd#flog#args#Complete(arg_lead, cmd_line, cursor_pos) abort + if a:cmd_line[ : a:cursor_pos] =~# ' -- ' + return [] + endif + + let l:default_completion = [ + \ '-all ', + \ '--no-all ', + \ '-author=', + \ '-bisect ', + \ '-no-bisect ', + \ '-date=', + \ '-format=', + \ '-graph ', + \ '-no-graph ', + \ '-limit=', + \ '-max-count=', + \ '-merges ', + \ '-no-merges ', + \ '-open-cmd=', + \ '-patch ', + \ '-no-patch ', + \ '-patch-search=', + \ '-patch-grep=', + \ '-path=', + \ '-raw-args=', + \ '-reflog ', + \ '-no-reflog ', + \ '-rev=', + \ '-reverse ', + \ '-no-reverse ', + \ '-search=', + \ '-grep=', + \ '-skip=', + \ '-order=', + \ '-sort=', + \ ] + + if a:arg_lead ==# '' + return flog#args#FilterCompletions(a:arg_lead, l:default_completion) + elseif a:arg_lead =~# '^-format=' + return flog#cmd#flog#args#CompleteFormat(a:arg_lead) + elseif a:arg_lead =~# '^-date=' + return flog#cmd#flog#args#CompleteDate(a:arg_lead) + elseif a:arg_lead =~# '^-open-cmd=' + return flog#cmd#flog#args#CompleteOpenCmd(a:arg_lead) + elseif a:arg_lead =~# '^-\(patch-\)\?\(search\|grep\)=' + return [] + elseif a:arg_lead =~# '^-author=' + return flog#cmd#flog#args#CompleteAuthor(a:arg_lead) + elseif a:arg_lead =~# '^-limit=' + return flog#cmd#flog#args#CompleteLimit(a:arg_lead) + elseif a:arg_lead =~# '^-rev=' + return flog#cmd#flog#args#CompleteRev(a:arg_lead) + elseif a:arg_lead =~# '^-path=' + return flog#cmd#flog#args#CompletePath(a:arg_lead) + elseif a:arg_lead =~# '^-\(order\|sort\)=' + return flog#cmd#flog#args#CompleteOrder(a:arg_lead) + endif + return flog#args#FilterCompletions(a:arg_lead, l:default_completion) +endfunction diff --git a/autoload/flog/cmd/floggit/args.vim b/autoload/flog/cmd/floggit/args.vim new file mode 100644 index 0000000..3b8b084 --- /dev/null +++ b/autoload/flog/cmd/floggit/args.vim @@ -0,0 +1,280 @@ +" +" This file contains functions for handling args to the ":Floggit" command. +" + +function! flog#cmd#floggit#args#HasParam(arg) abort + if a:arg =~# '^--exec-path' + return v:true + elseif a:arg =~# '^--work-tree' + return v:true + elseif a:arg =~# '^--namespace' + return v:true + elseif a:arg =~# '^--super-prefix' + return v:true + elseif a:arg =~# '^--config-env' + return v:true + endif + return v:false +endfunction + +function! flog#cmd#floggit#args#Parse(args) abort + let l:nargs = len(a:args) + + " Find command and parse potions + + let l:arg_index = 0 + let l:git_args = [] + let l:subcommand = '' + let l:flags = { + \ 'focus': v:false, + \ 'f': v:false, + \ 'static': v:false, + \ 's': v:false, + \ 'tmp': v:false, + \ 't': v:false, + \ } + let l:is_flag = v:false + + while l:arg_index < l:nargs + let l:arg = a:args[l:arg_index] + let l:is_flag = v:false + + if l:arg !~# '^-' + let l:subcommand = l:arg + break + endif + + if l:arg ==# '--focus' + let l:flags.focus = v:true + let l:is_flag = v:true + elseif l:arg ==# '-f' + let l:flags.f = v:true + let l:is_flag = v:true + elseif l:arg ==# '--static' + let l:flags.static = v:true + let l:is_flag = v:true + elseif l:arg ==# '-s' + let l:flags.s = v:true + let l:is_flag = v:true + elseif l:arg ==# '--tmp' + let l:flags.tmp = v:true + let l:is_flag = v:true + elseif l:arg ==# '-t' + let l:flags.t = v:true + let l:is_flag = v:true + else + call add(l:git_args, l:arg) + + " Handle param in next arg + if l:arg ==# '-c' || flog#cmd#floggit#args#HasParam(l:arg) && l:arg !~# '=' + let l:arg_index += 1 + call add(l:git_args, a:args[l:arg_index]) + endif + endif + + let l:arg_index += 1 + endwhile + + " Resolve options and return + + return { + \ 'args': a:args, + \ 'subcommand_index': l:arg_index >= l:nargs ? -1 : l:arg_index, + \ 'is_subcommand': l:arg_index == l:nargs - 1, + \ 'is_flag': l:is_flag, + \ 'subcommand': l:subcommand, + \ 'git_args': l:git_args, + \ 'focus': l:flags.focus || l:flags.f, + \ 'static': l:flags.static || l:flags.s, + \ 'tmp': l:flags.tmp || l:flags.t, + \ 'flags': l:flags, + \ } +endfunction + +function! flog#cmd#floggit#args#ToGitCommand(mods, bang, parsed_args) abort + let l:cmd = '' + + if !empty(a:mods) + let l:cmd .= a:mods + let l:cmd .= ' ' + endif + + let l:cmd .= 'Git' + let l:cmd .= a:bang + + let l:git_args = a:parsed_args.git_args + if !empty(l:git_args) + let l:cmd .= ' ' + let l:cmd .= join(l:git_args) + endif + + let l:subcommand_index = a:parsed_args.subcommand_index + if l:subcommand_index >= 0 + let l:cmd .= ' ' + let l:cmd .= join(a:parsed_args.args[l:subcommand_index :]) + end + + return l:cmd +endfunction + +function! flog#cmd#floggit#args#CompleteOpts(arg_lead, cmd_line, cursor_pos) abort + return flog#args#FilterCompletions(a:arg_lead, + \ ['-f', '-s', '-t', '--focus', '--static', '--tmp']) +endfunction + +function! flog#cmd#floggit#args#CompleteCommitRefs(commit) abort + let l:completions = [] + + for l:ref in flog#state#GetCommitRefs(a:commit) + if !empty(l:ref.remote) + " Add remote + let l:remote = l:ref.prefix . l:ref.remote + if index(l:completions, l:remote) < 0 + call add(l:completions, l:remote) + endif + + " Add remote branch + if index(l:completions, l:ref.full) < 0 + call add(l:completions, l:ref.full) + endif + + " Add local branch + if index(l:completions, l:ref.tail) < 0 + call add(l:completions, l:ref.tail) + endif + elseif index(l:completions, l:ref.full) < 0 + " Add special/tag/branch + call add(l:completions, l:ref.full) + endif + + " Add original path + if !empty(l:ref.orig) + call add(l:completions, l:ref.orig) + endif + endfor + + return l:completions +endfunction + +function! flog#cmd#floggit#args#CompleteContext(arg_lead, cmd_line, cursor_pos) abort + let l:line = line('.') + let l:firstline = line("'<") + let l:lastline = line("'>") + + let l:is_range = (l:line == l:firstline || l:line == l:lastline) && l:firstline != l:lastline + let l:first_commit = {} + let l:last_commit = {} + + if l:is_range + let l:first_commit = flog#floggraph#commit#GetAtLine(l:firstline) + let l:last_commit = flog#floggraph#commit#GetAtLine(l:lastline) + let l:is_range = l:first_commit != l:last_commit + endif + + let l:completions = [] + + if l:is_range + " Complete range + + let l:has_first = !empty(l:first_commit) + let l:has_last = !empty(l:last_commit) + + if l:has_first + call add(l:completions, l:first_commit.hash) + endif + + if l:has_last + call add(l:completions, l:last_commit.hash) + endif + + if l:has_first && l:has_last + call add(l:completions, l:last_commit.hash . '^..' . l:first_commit.hash) + endif + + if l:has_first + let l:completions += flog#cmd#floggit#args#CompleteCommitRefs(l:first_commit) + if l:has_last + let l:last_completions = flog#cmd#floggit#args#CompleteCommitRefs(l:last_commit) + let l:completions += flog#list#Exclude(l:last_completions, l:completions) + endif + else + let l:completions += flog#cmd#floggit#args#CompleteCommitRefs(l:last_commit) + endif + + return l:completions + else + " Complete single line + + let l:commit = flog#floggraph#commit#GetAtLine('.') + if empty(l:commit) + return [] + endif + let l:completions = [l:commit.hash] + flog#cmd#floggit#args#CompleteCommitRefs(l:commit) + endif + + let l:completions = flog#args#FilterCompletions(a:arg_lead, l:completions) + return l:completions +endfunction + +function! flog#cmd#floggit#args#Complete(arg_lead, cmd_line, cursor_pos) abort + let l:is_flog = flog#floggraph#buf#IsFlogBuf() + let l:has_state = flog#state#HasBufState() + + let l:cmd = a:cmd_line[ : a:cursor_pos] + let l:split_cmd = split(l:cmd, '\s', v:true) + let l:split_args = l:split_cmd[1 :] + + let l:parsed_args = flog#cmd#floggit#args#Parse(l:split_args) + + let l:cmd = flog#cmd#floggit#args#ToGitCommand('', '', l:parsed_args) + + let l:fugitive_completions = flog#fugitive#Complete( + \ flog#shell#Escape(l:split_cmd[-1]), l:cmd, len(l:cmd)) + + " Complete subcommand args only + if l:parsed_args.is_subcommand + return l:fugitive_completions + endif + + " Complete Floggit options and Git base args + if l:parsed_args.subcommand_index < 0 + if l:is_flog + let l:opt_completions = flog#cmd#floggit#args#CompleteOpts( + \ a:arg_lead, a:cmd_line, a:cursor_pos) + return opt_completions + l:fugitive_completions + else + return l:fugitive_completions + endif + endif + + let l:completions = [] + + " Complete line + if l:is_flog + let l:completions += flog#shell#EscapeList( + \ flog#cmd#floggit#args#CompleteContext(a:arg_lead, a:cmd_line, a:cursor_pos)) + endif + + " Complete state + if l:has_state + let l:opts = flog#state#GetBufState().opts + + if !empty(l:opts.limit) + let [l:range, l:path] = flog#args#SplitGitLimitArg(l:opts.limit) + let l:paths = flog#args#FilterCompletions(a:arg_lead, [l:path]) + let l:paths = flog#shell#EscapeList(l:paths) + let l:completions += flog#list#Exclude(l:paths, l:completions) + endif + + if !empty(l:opts.path) + let l:paths = flog#FilterCompletions(a:arg_lead, l:opts.paths) + let l:paths = flog#shell#EscapeList(l:paths) + let l:completions += flog#list#Exclude(l:paths, l:completions) + endif + endif + + " Complete Fugitive + let l:completions += flog#list#Exclude(l:fugitive_completions, l:completions) + + return l:completions +endfunction diff --git a/autoload/flog/deprecate.vim b/autoload/flog/deprecate.vim new file mode 100644 index 0000000..347a38a --- /dev/null +++ b/autoload/flog/deprecate.vim @@ -0,0 +1,50 @@ +" +" This file contains functions for deprecation. +" + +let g:flog_shown_deprecation_warnings = {} + +function! flog#deprecate#ShowWarning(old_usage, new_usage) abort + echoerr printf('Deprecated: %s', a:old_usage) + echoerr printf('New usage: %s', a:new_usage) + let g:flog_shown_deprecation_warnings[a:old_usage] = v:true +endfunction + +function! flog#deprecate#DidShowWarning(old_usage) abort + return has_key(g:flog_shown_deprecation_warnings, a:old_usage) +endfunction + +function! flog#deprecate#DefaultMapping(old_mapping, new_mapping) abort + call flog#deprecate#ShowWarning(a:old_mapping, a:new_mapping) +endfunction + +function! flog#deprecate#Setting(old_setting, new_setting, new_value = '...') abort + if exists(a:old_setting) && !flog#deprecate#DidShowWarning(a:old_setting) + let l:new_usage = printf('let %s = %s', a:new_setting, a:new_value) + call flog#deprecate#ShowWarning(a:old_setting, l:new_usage) + endif +endfunction + +function! flog#deprecate#Function(old_func, new_func, new_args = '...') abort + let l:old_usage = printf('%s()', a:old_func) + let l:new_usage = printf('call %s(%s)', a:new_func, a:new_args) + call flog#deprecate#ShowWarning(l:old_usage, l:new_usage) +endfunction + +function! flog#deprecate#Command(old_cmd, new_usage) abort + call flog#deprecate#ShowWarning(a:old_cmd, a:new_usage) +endfunction + +function! flog#deprecate#Autocmd(old_autocmd, new_autocmd, new_args = '...') abort + if !exists(printf('#User#%s', a:old_autocmd)) + return + endif + + let l:old_usage = printf('autocmd User %s', a:old_autocmd) + if flog#deprecate#DidShowWarning(l:old_usage) + return + endif + + let l:new_usage = printf('autocmd User %s %s', a:new_autocmd, a:new_args) + call flog#deprecate#ShowWarning(l:old_usage, l:new_usage) +endfunction diff --git a/autoload/flog/floggraph/buf.vim b/autoload/flog/floggraph/buf.vim new file mode 100644 index 0000000..33991fe --- /dev/null +++ b/autoload/flog/floggraph/buf.vim @@ -0,0 +1,217 @@ +" +" This file contains functions for creating and updating "floggraph" buffers. +" + +function! flog#floggraph#buf#IsFlogBuf() abort + return &filetype ==# 'floggraph' +endfunction + +function! flog#floggraph#buf#AssertFlogBuf() abort + if !flog#floggraph#buf#IsFlogBuf() + throw g:flog_not_a_flog_buffer + endif + return v:true +endfunction + +function! flog#floggraph#buf#UpdateStatus() abort + call flog#floggraph#buf#AssertFlogBuf() + + let cmd = flog#fugitive#GetGitCommand() + let cmd .= ' status -s' + let changes = len(flog#shell#Run(cmd)) + + if changes == 0 + let b:flog_status_summary = 'No changes' + elseif changes == 1 + let b:flog_status_summary = '1 file changed' + else + let b:flog_status_summary = string(changes) . ' files changed' + endif + + let head = flog#fugitive#GetHead() + + if !empty(head) + let b:flog_status_summary .= ' (' . head . ')' + endif + + return b:flog_status_summary +endfunction + +function! flog#floggraph#buf#GetInitialName(instance_number) abort + return ' flog-' . string(a:instance_number) . ' [uninitialized]' +endfunction + +function! flog#floggraph#buf#GetName(instance_number, opts) abort + let name = 'flog-' . string(a:instance_number) + + if a:opts.all + let name .= ' [all]' + endif + if a:opts.bisect + let name .= ' [bisect]' + endif + if !a:opts.merges + let name .= ' [no_merges]' + endif + if a:opts.reflog + let name .= ' [reflog]' + endif + if a:opts.reverse + let name .= ' [reverse]' + endif + if !a:opts.graph + let name .= ' [no_graph]' + endif + if !a:opts.patch + let name .= ' [no_patch]' + endif + if !empty(a:opts.skip) + let name .= ' [skip=' . a:opts.skip . ']' + endif + if !empty(a:opts.order) + let name .= ' [order=' . a:opts.order . ']' + endif + if !empty(a:opts.max_count) + let name .= ' [max_count=' . a:opts.max_count . ']' + endif + if !empty(a:opts.search) + let name .= ' [search=' . flog#str#Ellipsize(a:opts.search, 15) . ']' + endif + if !empty(a:opts.patch_search) + let name .= ' [patch_search=' . flog#str#Ellipsize(a:opts.patch_search, 15) . ']' + endif + if !empty(a:opts.author) + let name .= ' [author=' . a:opts.author . ']' + endif + if !empty(a:opts.limit) + let [range, path] = flog#args#SplitGitLimitArg(a:opts.limit) + let name .= ' [limit=' . flog#str#Ellipsize(range . fnamemodify(path, ':t'), 15) . ']' + endif + if len(a:opts.rev) == 1 + let name .= ' [rev=' . flog#str#Ellipsize(a:opts.rev[0], 15) . ']' + endif + if len(a:opts.rev) > 1 + let name .= ' [rev=...]' + endif + if len(a:opts.path) == 1 + let name .= ' [path=' . flog#str#Ellipsize(fnamemodify(a:opts.path[0], ':t'), 15) . ']' + elseif len(a:opts.path) > 1 + let name .= ' [path=...]' + endif + + return fnameescape(name) +endfunction + +function! flog#floggraph#buf#Open(state) abort + let bufname = flog#floggraph#buf#GetInitialName(a:state.instance_number) + execute 'silent! ' . a:state.opts.open_cmd . bufname + + call flog#state#SetBufState(a:state) + + let bufnr = bufnr() + call flog#state#SetGraphBufnr(a:state, bufnr) + + call flog#fugitive#TriggerDetection(flog#state#GetWorkdir(a:state)) + exec 'lcd ' . flog#fugitive#GetWorkdir() + + setlocal filetype=floggraph + + return bufnr +endfunction + +function! flog#floggraph#buf#Update() abort + call flog#floggraph#buf#AssertFlogBuf() + let state = flog#state#GetBufState() + let opts = flog#state#GetResolvedOpts(state) + + let graph_win = flog#win#Save() + + if g:flog_enable_status + call flog#floggraph#buf#UpdateStatus() + endif + + let cmd = flog#floggraph#git#BuildLogCmd() + call flog#state#SetPrevLogCmd(state, cmd) + if has('nvim') + let graph = flog#graph#nvim#Get(cmd) + else + let graph = flog#graph#vim#Get(cmd) + end + + " Record previous commit + let last_commit = flog#floggraph#commit#GetAtLine('.') + + " Update graph + call flog#state#SetGraph(state, graph) + call flog#floggraph#buf#SetContent(graph.output) + + " Restore commit position + call flog#floggraph#commit#RestorePosition(graph_win, last_commit) + + silent! exec 'file ' . flog#floggraph#buf#GetName(state.instance_number, opts) + + if exists('#User#FlogUpdate') + doautocmd User FlogUpdate + endif + + return state.graph_bufnr +endfunction + +function! flog#floggraph#buf#FinishUpdateHook(bufnr) abort + if bufnr() != a:bufnr + return -1 + endif + + augroup FlogGraphBufUpdate + exec 'autocmd! * ' + augroup END + + call flog#floggraph#buf#Update() + + return a:bufnr +endfunction + +function! flog#floggraph#buf#InitUpdateHook(bufnr) abort + let buf = string(a:bufnr) + + augroup FlogGraphBufUpdate + exec 'autocmd! * ' + if exists('##SafeState') + exec 'autocmd SafeState call flog#floggraph#buf#FinishUpdateHook(' . buf . ')' + else + exec 'autocmd WinEnter call flog#floggraph#buf#FinishUpdateHook(' . buf . ')' + endif + augroup END + + return a:bufnr +endfunction + +function! flog#floggraph#buf#SetContent(content) abort + call flog#floggraph#buf#AssertFlogBuf() + + setlocal modifiable noreadonly + silent! 1,$ delete + call setline(1, a:content) + setlocal nomodifiable readonly + + return a:content +endfunction + +function! flog#floggraph#buf#Close() abort + call flog#floggraph#buf#AssertFlogBuf() + let state = flog#state#GetBufState() + + let graph_win = flog#win#Save() + call flog#floggraph#side_win#CloseTmp() + + call flog#win#Restore(graph_win) + if flog#win#Is(graph_win) + let l:tab_info = flog#tab#GetInfo() + silent! bdelete! + if flog#tab#DidCloseRight(l:tab_info) + tabprev + end + endif + + return flog#win#GetSavedId(graph_win) +endfunction diff --git a/autoload/flog/floggraph/commit.vim b/autoload/flog/floggraph/commit.vim new file mode 100644 index 0000000..dacec24 --- /dev/null +++ b/autoload/flog/floggraph/commit.vim @@ -0,0 +1,161 @@ +" +" This file contains functions for handling commits in "floggraph" buffers. +" + +function! flog#floggraph#commit#GetAtLine(...) abort + let l:line = get(a:, 1, '.') + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + let l:lnum = type(l:line) == v:t_number ? l:line : line(l:line) + + return get(l:state.line_commits, l:lnum - 1, {}) +endfunction + +function! flog#floggraph#commit#GetByHash(hash) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + let l:commit = get(l:state.commits_by_hash, a:hash, {}) + if empty(l:commit) + return {} + endif + + return l:commit +endfunction + +function! flog#floggraph#commit#GetByRef(ref) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + let l:cmd = flog#fugitive#GetGitCommand() + let l:cmd .= ' rev-parse --short ' . flog#shell#Escape(a:ref) + + let l:result = flog#shell#Run(l:cmd) + if empty(l:result) + return {} + endif + + return flog#floggraph#commit#GetByHash(l:result[0]) +endfunction + +function! flog#floggraph#commit#GetNext(offset = 1) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + let l:commit = flog#floggraph#commit#GetAtLine('.') + let l:commit_index = index(l:state.commits, l:commit) + + if l:commit_index < 0 || l:commit_index + a:offset < 0 + return {} + endif + + return get(l:state.commits, l:commit_index + a:offset, {}) +endfunction + +function! flog#floggraph#commit#GetPrev(offset = 1) abort + return flog#floggraph#commit#GetNext(-a:offset) +endfunction + +function! flog#floggraph#commit#GetNextRef(count = 1) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + if a:count == 0 + return [0, {}] + endif + + let l:step = a:count > 0 ? 1 : -1 + + let l:commits = l:state.commits + let l:ncommits = len(l:commits) + + let l:ref_commit = {} + let l:commit = flog#floggraph#commit#GetAtLine('.') + + let l:nrefs = 0 + let l:i = index(l:state.commits, l:commit) + l:step + while l:i >= 0 && l:i < l:ncommits && l:nrefs != a:count + let l:commit = l:commits[l:i] + if !empty(l:commit.refs) + let l:ref_commit = l:commit + let l:nrefs += l:step + endif + + let l:i += l:step + endwhile + + return [l:nrefs, l:ref_commit] +endfunction + +function! flog#floggraph#commit#GetPrevRef(count = 1) abort + return flog#floggraph#commit#GetNext(-a:count) +endfunction + +function! flog#floggraph#commit#RestoreOffset(saved_win, saved_commit) abort + if empty(a:saved_commit) + return [-1, -1] + endif + + let l:saved_view = flog#win#GetSavedView(a:saved_win) + + let l:line_offset = l:saved_view.lnum - a:saved_commit.line + if l:line_offset < 0 + return [-1, -1] + endif + + if l:line_offset == 0 + let l:new_col = 0 + let l:saved_vcol = flog#win#GetSavedVcol(a:saved_win) + + if l:saved_vcol == a:saved_commit.col + let l:new_col = flog#floggraph#commit#GetAtLine('.').col + elseif l:saved_vcol == a:saved_commit.format_col + let l:new_col = flog#floggraph#commit#GetAtLine('.').format_col + endif + + if l:new_col > 0 + call setcursorcharpos('.', l:new_col) + endif + + return [0, l:new_col] + endif + + let l:new_line = line('.') + l:line_offset + + let l:new_line_commit = flog#floggraph#commit#GetAtLine(l:new_line) + if empty(l:new_line_commit) || l:new_line_commit.hash !=# a:saved_commit.hash + return [-1, -1] + endif + + call cursor(l:new_line, col('.')) + + return [l:line_offset, 0] +endfunction + +function! flog#floggraph#commit#RestorePosition(saved_win, saved_commit) abort + " Restore commit + let l:commit_line = -1 + if !empty(a:saved_commit) + let l:commit_line = flog#floggraph#nav#JumpToCommit(a:saved_commit.hash)[0] + endif + + if l:commit_line < 0 + " If commit was not found, restore full window position + call flog#win#Restore(a:saved_win) + return {} + endif + + " Try restoring the relative position + let [l:line_offset, l:new_col] = flog#floggraph#commit#RestoreOffset( + \ a:saved_win, + \ a:saved_commit) + + " Restore parts of window position + call flog#win#RestoreTopline(a:saved_win) + if l:new_col == 0 + call flog#win#RestoreVcol(a:saved_win) + endif + + return a:saved_commit +endfunction diff --git a/autoload/flog/floggraph/git.vim b/autoload/flog/floggraph/git.vim new file mode 100644 index 0000000..4580273 --- /dev/null +++ b/autoload/flog/floggraph/git.vim @@ -0,0 +1,116 @@ +" +" This file contains functions for working with git for "floggraph" buffers. +" + +function! flog#floggraph#git#BuildLogFormat() abort + let l:state = flog#state#GetBufState() + let l:opts = flog#state#GetResolvedOpts(l:state) + + let l:format = 'format:' + " Add token so we can find commits + let l:format .= g:flog_commit_start_token + " Add commit data + let l:format .= '%n%h%n%p%n%D%n' + " Add user format + let l:format .= l:opts.format + + return flog#shell#Escape(l:format) +endfunction + +function! flog#floggraph#git#BuildLogArgs() abort + let l:state = flog#state#GetBufState() + let l:opts = flog#state#GetResolvedOpts(l:state) + + if l:opts.reverse && l:opts.graph + throw g:flog_reverse_requires_no_graph + endif + + let l:args = '' + + if l:opts.graph + let l:args .= ' --parents --topo-order' + endif + let l:args .= ' --no-color' + let l:args .= ' --pretty=' . flog#floggraph#git#BuildLogFormat() + let l:args .= ' --date=' . flog#shell#Escape(l:opts.date) + if l:opts.all && empty(l:opts.limit) + let l:args .= ' --all' + endif + if l:opts.bisect + let l:args .= ' --bisect' + endif + if !l:opts.merges + let l:args .= ' --no-merges' + endif + if l:opts.reflog + let l:args .= ' --reflog' + endif + if l:opts.reverse + let l:args .= ' --reverse' + endif + if !l:opts.patch + let l:args .= ' --no-patch' + endif + if !empty(l:opts.skip) + let l:args .= ' --skip=' . flog#shell#Escape(l:opts.skip) + endif + if !empty(l:opts.order) + let l:order_type = flog#global_opts#GetOrderType(l:opts.order) + let l:args .= ' ' . l:order_type.args + endif + if !empty(l:opts.max_count) + let l:args .= ' --max-count=' . flog#shell#Escape(l:opts.max_count) + endif + if !empty(l:opts.search) + let l:args .= ' --grep=' . flog#shell#Escape(l:opts.search) + endif + if !empty(l:opts.patch_search) + let l:args .= ' -G' . flog#shell#Escape(l:opts.patch_search) + endif + if !empty(l:opts.author) + let l:args .= ' --author=' . flog#shell#Escape(l:opts.author) + endif + if !empty(l:opts.limit) + let l:args .= ' -L' . flog#shell#Escape(l:opts.limit) + endif + if !empty(l:opts.raw_args) + let l:args .= ' ' . l:opts.raw_args + endif + if len(l:opts.rev) >= 1 + let l:rev = '' + if !empty(l:opts.limit) + let l:rev = flog#shell#Escape(l:opts.rev[0]) + else + let l:rev = join(flog#shell#EscapeList(l:opts.rev), ' ') + endif + let l:args .= ' ' . l:rev + endif + + return l:args +endfunction + +function! flog#floggraph#git#BuildLogPaths() abort + let l:state = flog#state#GetBufState() + let l:opts = flog#state#GetResolvedOpts(l:state) + + if !empty(l:opts.limit) + return '' + endif + + if empty(l:opts.path) + return '' + endif + + return join(flog#shell#EscapeList(l:opts.path), ' ') +endfunction + +function! flog#floggraph#git#BuildLogCmd() abort + let cmd = flog#fugitive#GetGitCommand() + + let cmd .= ' log' + let cmd .= flog#floggraph#git#BuildLogArgs() + let cmd .= ' -- ' + let cmd .= flog#floggraph#git#BuildLogPaths() + + return cmd +endfunction diff --git a/autoload/flog/floggraph/mark.vim b/autoload/flog/floggraph/mark.vim new file mode 100644 index 0000000..91cfa84 --- /dev/null +++ b/autoload/flog/floggraph/mark.vim @@ -0,0 +1,62 @@ +" +" This file contains functions for working with commit marks in "floggraph" buffers. +" + +function! flog#floggraph#mark#SetInternal(key, line) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + let l:commit = flog#floggraph#commit#GetAtLine(a:line) + return flog#state#SetInternalCommitMark(l:state, a:key, l:commit) +endfunction + +function! flog#floggraph#mark#Set(key, line) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + let l:commit = flog#floggraph#commit#GetAtLine(a:line) + return flog#state#SetCommitMark(l:state, a:key, l:commit) +endfunction + +function! flog#floggraph#mark#SetJump(line = '.') abort + return flog#floggraph#mark#Set("'", a:line) +endfunction + +function! flog#floggraph#mark#Get(key) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + if a:key =~# '[<>]' + return flog#floggraph#commit#GetAtLine("'" . a:key) + endif + + if a:key ==# '@' + return flog#floggraph#commit#GetByRef('HEAD') + endif + if a:key =~# '[~^]' + return flog#floggraph#commit#GetByRef('HEAD~') + endif + + if flog#state#IsCancelCommitMark(a:key) + throw g:flog_invalid_mark + endif + + if !flog#state#HasCommitMark(l:state, a:key) + return {} + endif + + return flog#state#GetCommitMark(l:state, a:key) +endfunction + +function! flog#floggraph#mark#PrintAll() abort + let l:marks = flog#state#GetBufState().commit_marks + + if empty(l:marks) + echo 'No commit marks.' + return l:marks + endif + + for l:key in order(keys(l:marks)) + echo ' ' . l:key . ' ' . l:marks[l:key].hash + endfor + + return l:marks +endfunction diff --git a/autoload/flog/floggraph/nav.vim b/autoload/flog/floggraph/nav.vim new file mode 100644 index 0000000..6ead83a --- /dev/null +++ b/autoload/flog/floggraph/nav.vim @@ -0,0 +1,182 @@ +" +" This file contains functions for navigating in "floggraph" buffers. +" + +function! flog#floggraph#nav#JumpToCommit(hash) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + if empty(a:hash) + return [-1, -1] + endif + + let l:commit = get(l:state.commits_by_hash, a:hash, {}) + if empty(l:commit) + return [-1, -1] + endif + + let l:lnum = max([l:commit.line, 1]) + let l:col = max([l:commit.col, 1]) + + call setcursorcharpos(l:lnum, l:col) + + return [l:lnum, l:col] +endfunction + +function! flog#floggraph#nav#JumpToMark(key) abort + call flog#floggraph#buf#AssertFlogBuf() + + let l:prev_line = line('.') + let l:prev_commit = flog#floggraph#commit#GetAtLine(l:prev_line) + + let l:commit = flog#floggraph#mark#Get(a:key) + if empty(l:commit) + return [-1, -1] + endif + + let l:result = flog#floggraph#nav#JumpToCommit(l:commit.hash) + + if l:commit != l:prev_commit + call flog#floggraph#mark#SetJump(l:prev_line) + endif + + return l:result +endfunction + +function! flog#floggraph#nav#NextCommit(count = 1) abort + call flog#floggraph#buf#AssertFlogBuf() + + let l:prev_line = line('.') + + let l:commit = flog#floggraph#commit#GetNext(a:count) + + if !empty(l:commit) + call flog#floggraph#nav#JumpToCommit(l:commit.hash) + call flog#floggraph#mark#SetJump(l:prev_line) + endif + + return l:commit +endfunction + +function! flog#floggraph#nav#PrevCommit(count = 1) abort + return flog#floggraph#nav#NextCommit(-a:count) +endfunction + +function! flog#floggraph#nav#NextRefCommit(count = 1) abort + call flog#floggraph#buf#AssertFlogBuf() + + let l:prev_line = line('.') + + let [l:nrefs, l:commit] = flog#floggraph#commit#GetNextRef(a:count) + + if !empty(l:commit) + call flog#floggraph#nav#JumpToCommit(l:commit.hash) + call flog#floggraph#mark#SetJump(l:prev_line) + endif + + return l:nrefs +endfunction + +function! flog#floggraph#nav#PrevRefCommit(count = 1) abort + return flog#floggraph#nav#NextRefCommit(-a:count) +endfunction + +function! flog#floggraph#nav#SkipTo(skip) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + let l:skip_opt = string(a:skip) + if l:skip_opt ==# '0' + let l:skip_opt = '' + endif + + if l:state.opts.skip ==# l:skip_opt + return a:skip + endif + + let l:state.opts.skip = l:skip_opt + + call flog#floggraph#buf#Update() + + return a:skip +endfunction + +function! flog#floggraph#nav#SkipAhead(count) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:opts = flog#state#GetBufState().opts + + if empty(l:opts.max_count) + return -1 + endif + + let l:skip = empty(l:opts.skip) ? 0 : str2nr(l:opts.skip) + let l:skip += str2nr(l:opts.max_count) * a:count + if l:skip < 0 + let l:skip = 0 + endif + + return flog#floggraph#nav#SkipTo(l:skip) +endfunction + +function! flog#floggraph#nav#SkipBack(count) abort + return flog#floggraph#nav#SkipAhead(-a:count) +endfunction + +function! flog#floggraph#nav#SetRevToCommitAtLine(line = '.') abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + let l:commit = flog#floggraph#commit#GetAtLine(a:line) + + if empty(l:commit) + return '' + endif + + let l:hash = l:commit.hash + let l:rev = [l:hash] + + if l:state.opts.rev ==# l:rev + return '' + endif + + let l:state.opts.skip = '' + let l:state.opts.rev = l:rev + + call flog#floggraph#buf#Update() + + return l:hash +endfunction + +function! flog#floggraph#nav#ClearRev() abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + if empty(l:state.opts.rev) + return v:false + endif + + let l:state.opts.rev = [] + call flog#floggraph#buf#Update() + + return v:true +endfunction + +function! flog#floggraph#nav#JumpToCommitStart() abort + call flog#floggraph#buf#AssertFlogBuf() + + let l:curr_col = virtcol('.') + + let l:commit = flog#floggraph#commit#GetAtLine('.') + if empty(l:commit) + return -1 + endif + + let l:new_col = l:commit.col + if l:commit.line == line('.') && l:curr_col <= l:commit.col + let l:new_col = l:commit.format_col + endif + + call setcursorcharpos(l:commit.line, l:new_col) + + return l:new_col +endfunction diff --git a/autoload/flog/floggraph/opts.vim b/autoload/flog/floggraph/opts.vim new file mode 100644 index 0000000..70bf07c --- /dev/null +++ b/autoload/flog/floggraph/opts.vim @@ -0,0 +1,75 @@ +" +" This file contains functions for modifying options in "floggraph" buffers. +" + +function! flog#floggraph#opts#Toggle(name) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:opts = flog#state#GetBufState().opts + + let l:val = !l:opts[a:name] + let l:opts[a:name] = l:val + + call flog#floggraph#buf#Update() + + return l:val +endfunction + +function! flog#floggraph#opts#ToggleAll() abort + return flog#floggraph#opts#Toggle('all') +endfunction + +function! flog#floggraph#opts#ToggleBisect() abort + return flog#floggraph#opts#Toggle('bisect') +endfunction + +function! flog#floggraph#opts#ToggleMerges() abort + return flog#floggraph#opts#Toggle('merges') +endfunction + +function! flog#floggraph#opts#ToggleReflog() abort + return flog#floggraph#opts#Toggle('reflog') +endfunction + +function! flog#floggraph#opts#ToggleReverse() abort + return flog#floggraph#opts#Toggle('reverse') +endfunction + +function! flog#floggraph#opts#ToggleGraph() abort + return flog#floggraph#opts#Toggle('graph') +endfunction + +function! flog#floggraph#opts#TogglePatch() abort + return flog#floggraph#opts#Toggle('patch') +endfunction + +function! flog#floggraph#opts#CycleOrder() abort + call flog#floggraph#buf#AssertFlogBuf() + let l:opts = flog#state#GetBufState().opts + + let l:default_order = l:opts.graph ? 'topo' : 'date' + + let l:order = l:opts.order + if empty(l:order) + let l:order = l:default_order + endif + + let l:order_type = flog#global_opts#GetOrderType(l:order) + + if empty(l:order_type) + let l:order = g:flog_order_types[0].name + else + let l:order_index = index(g:flog_order_types, l:order_type) + + if l:order_index == len(g:flog_order_types) - 1 + let l:order = g:flog_order_types[0].name + else + let l:order = g:flog_order_types[l:order_index + 1].name + endif + endif + + let l:opts.order = l:order + + call flog#floggraph#buf#Update() + + return l:order +endfunction diff --git a/autoload/flog/floggraph/reg.vim b/autoload/flog/floggraph/reg.vim new file mode 100644 index 0000000..2273976 --- /dev/null +++ b/autoload/flog/floggraph/reg.vim @@ -0,0 +1,59 @@ +" +" This file contains functions for manipulating the register in "floggraph" buffers. +" + +function! flog#floggraph#reg#YankHash(reg = '"', line = '.', count = 1) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + if a:count < 1 + call setreg(a:reg, [], 'v') + return 0 + endif + + let l:commit = flog#floggraph#commit#GetAtLine(a:line) + if empty(l:commit) + call setreg(a:reg, [], 'v') + return 0 + endif + + let l:commit_index = index(l:state.commits, l:commit) + + let l:hashes = [l:commit.hash] + let l:i = 1 + while l:i < a:count + let l:commit = get(l:state.commits, l:commit_index + l:i, {}) + if empty(l:commit) + break + endif + + call add(l:hashes, l:commit.hash) + + let l:i += 1 + endwhile + + call setreg(a:reg, l:hashes, 'v') + + return l:i +endfunction + +function! flog#floggraph#reg#YankHashRange(reg = '"', start_line = "'<", end_line = "'>") abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + let l:start_commit = flog#floggraph#commit#GetAtLine(a:start_line) + let l:end_commit = flog#floggraph#commit#GetAtLine(a:end_line) + if empty(l:start_commit) || empty(l:end_commit) + call setreg(a:reg, []) + return 0 + endif + + let l:start_index = index(l:state.commits, l:start_commit) + let l:end_index = index(l:state.commits, l:end_commit) + if l:start_index < 0 || l:end_index < 0 + call setreg(a:reg, []) + return 0 + endif + + return flog#floggraph#reg#YankHash(a:reg, a:start_line, l:end_index - l:start_index + 1) +endfunction diff --git a/autoload/flog/floggraph/side_win.vim b/autoload/flog/floggraph/side_win.vim new file mode 100644 index 0000000..0f43909 --- /dev/null +++ b/autoload/flog/floggraph/side_win.vim @@ -0,0 +1,102 @@ +" +" This file contains functions for handling side windows in "floggraph" buffers. +" + +function! flog#floggraph#side_win#CloseTmp() abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + let l:prev_win = flog#win#Save() + + for l:tmp_id in l:state.tmp_side_wins + " Buffer is not open + if win_id2tabwin(l:tmp_id) == [0, 0] + continue + endif + + " Buffer is open, close + call win_gotoid(l:tmp_id) + silent! close! + endfor + + call flog#win#Restore(l:prev_win) + + return flog#state#ResetTmpSideWins(l:state) +endfunction + +function! flog#floggraph#side_win#IsInitialized() abort + return exists('b:flog_side_win_initialized') +endfunction + +function! flog#floggraph#side_win#Initialize(state, is_tmp) abort + if !flog#state#HasBufState() + call flog#state#SetBufState(a:state) + endif + + call flog#deprecate#Autocmd('FlogCmdBufferSetup', 'FlogSideWinSetup') + call flog#deprecate#Autocmd('FlogTmpCmdBufferSetup', 'FlogTmpSideWinSetup') + call flog#deprecate#Autocmd('FlogNonTmpCmdBufferSetup', 'FlogNonTmpSideWinSetup') + + if exists('#User#FlogSideWinSetup') + doautocmd User FlogSideWinSetup + endif + + if a:is_tmp + if exists('#User#FlogTmpSideWinSetup') + doautocmd User FlogTmpSideWinSetup + endif + else + if exists('#User#FlogNonTmpSideWinSetup') + doautocmd User FlogNonTmpSideWinSetup + endif + endif + + let b:flog_side_win_initialized = v:true + + return win_getid() +endfunction + +function! flog#floggraph#side_win#Open(cmd, keep_focus, is_tmp) abort + call flog#floggraph#buf#AssertFlogBuf() + let l:state = flog#state#GetBufState() + + let l:graph_win = flog#win#Save() + let l:saved_win_ids = flog#win#GetAllIds() + + exec a:cmd + let l:final_win = flog#win#Save() + + let l:new_win_ids = flog#win#GetAllIds() + let l:new_win_ids = flog#list#Exclude(l:new_win_ids, l:saved_win_ids) + + if !empty(l:new_win_ids) + call flog#win#Restore(l:graph_win) + + if a:is_tmp + call flog#floggraph#side_win#CloseTmp() + endif + + for l:win_id in l:new_win_ids + silent! call win_gotoid(l:win_id) + if !flog#floggraph#side_win#IsInitialized() + call flog#floggraph#side_win#Initialize(l:state, a:is_tmp) + endif + endfor + + call flog#win#Restore(l:final_win) + + if a:is_tmp + call flog#state#SetTmpSideWins(l:state, l:new_win_ids) + endif + endif + + if !a:keep_focus + call flog#win#Restore(l:graph_win) + endif + + return flog#win#GetSavedId(l:final_win) +endfunction + +function! flog#floggraph#side_win#OpenTmp(cmd, keep_focus) abort + return flog#floggraph#side_win#Open(a:cmd, a:keep_focus, v:true) +endfunction diff --git a/autoload/flog/format.vim b/autoload/flog/format.vim new file mode 100644 index 0000000..0f064b3 --- /dev/null +++ b/autoload/flog/format.vim @@ -0,0 +1,217 @@ +" +" This file contains utility functions for "flog#Format()". +" + +function! flog#format#GetCacheRefs(cache, commit) abort + let l:ref_cache = a:cache.refs + + let l:refs = flog#state#GetCommitRefs(a:commit) + + let l:ref_cache[a:commit.hash] = l:refs + return l:refs +endfunction + +function! flog#format#FormatHash(save) abort + let l:commit = flog#floggraph#commit#GetAtLine('.') + + if !empty(l:commit) + if a:save + call flog#floggraph#mark#SetInternal('!', '.') + endif + return l:commit.hash + endif + + return '' +endfunction + +function! flog#format#FormatMarkHash(key) abort + let l:commit = flog#floggraph#mark#Get(a:key) + return empty(l:commit) ? '' : l:commit.hash +endfunction + +function! flog#format#FormatCommitBranch(cache, commit) abort + let l:local_branch = '' + let l:remote_branch = '' + + for l:ref in flog#format#GetCacheRefs(a:cache, a:commit) + " Skip non-branches + if l:ref.tag || l:ref.tail =~# 'HEAD$' + continue + endif + + " Get local branch + if empty(l:ref.remote) && empty(l:ref.prefix) + let l:local_branch = l:ref.tail + break + endif + + " Get remote branch + if empty(l:remote_branch) && !empty(l:ref.remote) + let l:remote_branch = l:ref.path + endif + endfor + + let l:branch = empty(l:local_branch) ? l:remote_branch : l:local_branch + + return flog#shell#Escape(l:branch) +endfunction + +function! flog#format#FormatBranch(cache) abort + let l:commit = flog#floggraph#commit#GetAtLine('.') + return flog#format#FormatCommitBranch(a:cache, l:commit) +endfunction + +function! flog#format#FormatMarkBranch(cache, key) abort + let l:commit = flog#floggraph#mark#Get(a:key) + return flog#format#FormatCommitBranch(a:cache, l:commit) +endfunction + +function! flog#format#FormatCommitLocalBranch(cache, commit) abort + let l:branch = flog#format#FormatCommitBranch(a:cache, a:commit) + return substitute(l:branch, '.\{-}/', '', '') +endfunction + +function! flog#format#FormatLocalBranch(cache) abort + let l:commit = flog#floggraph#commit#GetAtLine('.') + return flog#format#FormatCommitLocalBranch(a:cache, l:commit) +endfunction + +function! flog#format#FormatMarkLocalBranch(cache, key) abort + let l:commit = flog#floggraph#mark#Get(a:key) + return flog#format#FormatCommitLocalBranch(a:cache, l:commit) +endfunction + +function! flog#format#FormatPath() abort + let l:state = flog#state#GetBufState() + let l:path = l:state.opts.path + + if !empty(l:state.opts.limit) + let [l:range, l:limit_path] = flog#args#SplitGitLimitArg(l:state.opts.limit) + + if empty(l:limit_path) + return '' + endif + + let l:path = [l:limit_path] + elseif empty(l:path) + return '' + endif + + return join(flog#shell#EscapeList(l:path), ' ') +endfunction + +function! flog#format#FormatIndexTree(cache) abort + if empty(a:cache.index_tree) + let l:cmd = flog#fugitive#GetGitCommand() + let l:cmd .= ' write-tree' + let a:cache.index_tree = flog#shell#Run(l:cmd)[0] + endif + return a:cache.index_tree +endfunction + +function! flog#format#FormatItem(cache, item) abort + let l:item_cache = a:cache.items + + " Return cached items + + if has_key(l:item_cache, a:item) + return l:item_cache[a:item] + endif + + " Format the item + + let l:formatted_item = '' + + if a:item ==# 'h' + let l:formatted_item = flog#format#FormatHash(v:true) + elseif a:item ==# 'H' + let l:formatted_item = flog#format#FormatHash(v:false) + elseif a:item =~# "^h'." + let l:formatted_item = flog#format#FormatMarkHash(a:item[2 : ]) + elseif a:item =~# 'b' + let l:formatted_item = flog#format#FormatBranch(a:cache) + elseif a:item =~# "^b'." + let l:formatted_item = flog#format#FormatMarkBranch(a:cache, a:item[2 : ]) + elseif a:item =~# 'l' + let l:formatted_item = flog#format#FormatLocalBranch(a:cache) + elseif a:item =~# "^l'." + let l:formatted_item = flog#format#FormatMarkLocalBranch(a:cache, a:item[2 : ]) + elseif a:item ==# 'p' + let l:formatted_item = flog#format#FormatPath() + elseif a:item ==# 't' + let l:formatted_item = flog#format#FormatIndexTree(a:cache) + else + echoerr printf('error converting "%s"', a:item) + throw g:flog_unsupported_exec_format_item + endif + + " Handle result + let l:item_cache[a:item] = l:formatted_item + return l:formatted_item +endfunction + +function! flog#format#FormatCommand(str) abort + call flog#floggraph#buf#AssertFlogBuf() + + " Special token flags + let l:is_in_item = v:false + let l:is_in_long_item = v:false + + " Special token data + let l:long_item = '' + + " Memoized data + let l:cache = { + \ 'items': {}, + \ 'refs': {}, + \ 'index_tree': '', + \ } + + " Return data + let l:result = '' + + for l:char in split(a:str, '\zs') + if l:is_in_long_item + " Parse characters in %() + + if l:char ==# ')' + " End long specifier + let l:formatted_item = flog#format#FormatItem(l:cache, l:long_item) + if empty(l:formatted_item) + return '' + endif + + let l:result .= l:formatted_item + let l:is_in_long_item = v:false + let l:long_item = '' + else + let l:long_item .= l:char + endif + elseif l:is_in_item + " Parse character after % + + if l:char ==# '(' + " Start long specifier + let l:is_in_long_item = v:true + else + " Parse specifier character + let l:formatted_item = flog#format#FormatItem(l:cache, l:char) + if empty(l:formatted_item) + return '' + endif + + let l:result .= l:formatted_item + endif + + let l:is_in_item = v:false + elseif l:char ==# '%' + " Start specifier + let l:is_in_item = v:true + else + " Append normal character + let l:result .= l:char + endif + endfor + + return l:result +endfunction diff --git a/autoload/flog/fugitive.vim b/autoload/flog/fugitive.vim new file mode 100644 index 0000000..a155cb7 --- /dev/null +++ b/autoload/flog/fugitive.vim @@ -0,0 +1,40 @@ +" +" This file contains functions for working with Fugitive. +" + +function! flog#fugitive#IsGitBuf() abort + return FugitiveIsGitDir() +endfunction + +function! flog#fugitive#GetRelativePath(workdir, path) abort + let l:full_path = fnamemodify(a:path, ':p') + if stridx(l:full_path, a:workdir) == 0 + return l:full_path[len(a:workdir) + 1 : ] + endif + return a:path +endfunction + +function! flog#fugitive#GetWorkdir() abort + return FugitiveFind(':/') +endfunction + +function! flog#fugitive#GetGitDir() abort + return FugitiveGitDir() +endfunction + +function! flog#fugitive#GetGitCommand() abort + return FugitiveShellCommand() +endfunction + +function! flog#fugitive#GetHead() abort + return fugitive#Head() +endfunction + +function! flog#fugitive#TriggerDetection(workdir) abort + call FugitiveDetect(a:workdir) + return a:workdir +endfunction + +function! flog#fugitive#Complete(arg_lead, cmd_line, cursor_pos) abort + return fugitive#Complete(a:arg_lead, a:cmd_line, a:cursor_pos) +endfunction diff --git a/autoload/flog/git.vim b/autoload/flog/git.vim new file mode 100644 index 0000000..057a719 --- /dev/null +++ b/autoload/flog/git.vim @@ -0,0 +1,36 @@ +" +" This file contains functions for working with git. +" + +function! flog#git#HasCommitGraph() abort + let l:path = flog#fugitive#GetGitDir() + let l:path .= '/objects/info/commit-graph' + return filereadable(l:path) +endfunction + +function! flog#git#WriteCommitGraph() abort + let l:cmd = 'Git commit-graph write ' + let l:cmd .= g:flog_write_commit_graph_args + + exec l:cmd + + return l:cmd +endfunction + +function! flog#git#GetAuthors() abort + let l:cmd = flog#fugitive#GetGitCommand() + let l:cmd .= ' shortlog -s -n ' + let l:cmd .= g:flog_get_author_args + + let l:result = flog#shell#Run(l:cmd) + + " Filter author commit numbers before returning + return map(copy(l:result), 'substitute(v:val, "^\\s*\\d*\\s*", "", "")') +endfunction + +function! flog#git#GetRefs() abort + let l:cmd = flog#fugitive#GetGitCommand() + let l:cmd .= ' rev-parse --symbolic --branches --tags --remotes' + + return flog#shell#Run(l:cmd) + ['HEAD', 'FETCH_HEAD', 'ORIG_HEAD'] +endfunction diff --git a/autoload/flog/global_opts.vim b/autoload/flog/global_opts.vim new file mode 100644 index 0000000..35e7848 --- /dev/null +++ b/autoload/flog/global_opts.vim @@ -0,0 +1,12 @@ +" +" This file contains functions for working with global options. +" + +function! flog#global_opts#GetOrderType(name) abort + for l:order_type in g:flog_order_types + if l:order_type.name ==# a:name + return l:order_type + endif + endfor + return {} +endfunction diff --git a/autoload/flog/graph/nvim.vim b/autoload/flog/graph/nvim.vim new file mode 100644 index 0000000..06bcc4e --- /dev/null +++ b/autoload/flog/graph/nvim.vim @@ -0,0 +1,19 @@ +" +" This file contains functions for generating the commit graph in Neovim. +" + +function! flog#graph#nvim#Get(git_cmd) abort + let l:state = flog#state#GetBufState() + + let l:graph_lib = flog#lua#GetLibPath('graph.lua') + exec 'luafile ' . fnameescape(l:graph_lib) + + return v:lua.flog_get_graph( + \ v:false, + \ v:true, + \ v:true, + \ g:flog_commit_start_token, + \ state.opts.graph ? v:true : v:false, + \ a:git_cmd + \ ) +endfunction diff --git a/autoload/flog/graph/vim.vim b/autoload/flog/graph/vim.vim new file mode 100644 index 0000000..0f6ab67 --- /dev/null +++ b/autoload/flog/graph/vim.vim @@ -0,0 +1,163 @@ +vim9script + +# +# This file contains functions for generating the commit graph in Vim. +# + +export def GetUsingInternalLua(git_cmd: string): dict + flog#floggraph#buf#AssertFlogBuf() + const state = flog#state#GetBufState() + + # Check version + flog#lua#CheckInternalVersion() + + # Load graph lib + const graph_lib = flog#lua#GetLibPath('graph.lua') + exec 'luafile ' .. fnameescape(graph_lib) + + # Set temporary vars + g:flog_tmp_enable_graph = state.opts.graph + g:flog_tmp_git_cmd = git_cmd + + # Build command + var cmd = 'flog_get_graph(' + # enable_vim + cmd ..= 'true, ' + # enable_nvim + cmd ..= 'false, ' + # enable_porcelain + cmd ..= 'true, ' + # start_token + cmd ..= 'vim.eval("g:flog_commit_start_token"), ' + # enable_graph + cmd ..= 'vim.eval("g:flog_tmp_enable_graph"), ' + # cmd + cmd ..= 'vim.eval("g:flog_tmp_git_cmd"))' + + # Evaluate command + var result = luaeval(cmd) + + # Cleanup + unlet! g:flog_tmp_enable_graph + unlet! g:flog_tmp_git_cmd + + return { + output: result.output, + commits: result.commits, + commits_by_hash: result.commits_by_hash, + line_commits: result.line_commits, + } +enddef + +export def GetUsingBinLua(git_cmd: string): dict + flog#floggraph#buf#AssertFlogBuf() + const state = flog#state#GetBufState() + + # Get paths + const script_path = flog#lua#GetLibPath('graph_bin.lua') + + # Build command + var cmd = flog#lua#GetBin() + cmd ..= ' ' + cmd ..= shellescape(script_path) + cmd ..= ' ' + # start_token + cmd ..= shellescape(g:flog_commit_start_token) + cmd ..= ' ' + # enable_graph + cmd ..= state.opts.graph ? 'true' : 'false' + cmd ..= ' ' + # cmd + cmd ..= shellescape(git_cmd) + + # Run command + const out = flog#shell#Run(cmd) + + # Parse number of commits + const ncommits = str2nr(out[0]) + + # Init data + var out_index = 1 + var commits = [] + var commit_index = 0 + var commits_by_hash = {} + var line_commits = [] + var final_out = [] + var total_lines = 0 + + # Parse output + while commit_index < ncommits + # Init commit + var commit = {} + + # Parse hash + const hash = out[out_index] + commit.hash = hash + out_index += 1 + + # Parse parents + const last_parent_index = out_index + str2nr(out[out_index]) + var parents = [] + while out_index < last_parent_index + out_index += 1 + add(parents, out[out_index]) + endwhile + commit.parents = parents + out_index += 1 + + # Parse refs + commit.refs = out[out_index] + out_index += 1 + + # Parse commit column + commit.col = str2nr(out[out_index]) + out_index += 1 + + # Parse commit visual column + commit.format_col = str2nr(out[out_index]) + out_index += 1 + + # Parse output + const ncommit_lines = str2nr(out[out_index]) + const last_out_index = out_index + ncommit_lines + while out_index < last_out_index + out_index += 1 + add(final_out, out[out_index]) + add(line_commits, commit) + endwhile + out_index += 1 + commit.line = total_lines + 1 + total_lines += ncommit_lines + + # Increment + add(commits, commit) + commits_by_hash[hash] = commit + commit_index += 1 + endwhile + + return { + output: final_out, + commits: commits, + commits_by_hash: commits_by_hash, + line_commits: line_commits, + } +enddef + +export def Get(git_cmd: string): dict + flog#floggraph#buf#AssertFlogBuf() + + const lua_path_info = flog#lua#SetLuaPath() + var graph: dict + + try + if flog#lua#ShouldUseInternal() + graph = GetUsingInternalLua(git_cmd) + else + graph = GetUsingBinLua(git_cmd) + endif + finally + flog#lua#ResetLuaPath(lua_path_info) + endtry + + return graph +enddef diff --git a/autoload/flog/list.vim b/autoload/flog/list.vim new file mode 100644 index 0000000..a9900e5 --- /dev/null +++ b/autoload/flog/list.vim @@ -0,0 +1,7 @@ +" +" This file contains functions for handling lists. +" + +function! flog#list#Exclude(list, filters) abort + return filter(copy(a:list), 'index(a:filters, v:val) < 0') +endfunction diff --git a/autoload/flog/lua.vim b/autoload/flog/lua.vim new file mode 100644 index 0000000..0d3dcee --- /dev/null +++ b/autoload/flog/lua.vim @@ -0,0 +1,94 @@ +" +" This file contains functions which allow Flog to communicate with Lua. +" + +function! flog#lua#ShouldUseInternal() abort + let l:use_lua = get(g:, 'flog_use_internal_lua', v:false) + + if l:use_lua && !has('lua') + echoerr 'flog: warning: internal Lua is enabled but unavailable' + return v:false + endif + + return l:use_lua +endfunction + +let g:flog_did_check_lua_internal_version = v:false + +function! flog#lua#CheckInternalVersion() abort + if g:flog_check_lua_version && !g:flog_did_check_lua_internal_version + let g:flog_did_check_lua_internal_version = v:true + + if luaeval('_VERSION') !~# '\c^lua 5\.1\(\.\|$\)' + echoerr 'flog: warning: only Lua 5.1 and LuaJIT 2.1 are supported' + elseif empty(luaeval('jit and jit.version')) + echoerr 'flog: warning: for speed improvements, please compile Vim with LuaJIT 2.1' + endif + + return v:true + endif + + return v:false +endfunction + +let g:flog_did_check_lua_bin_version = v:false + +function! flog#lua#CheckBinVersion(bin) abort + if g:flog_check_lua_version && !g:flog_did_check_lua_bin_version + let g:flog_did_check_lua_bin_version = v:true + + let l:out = flog#shell#Run(a:bin . ' -v')[0] + + if l:out =~# '\c^lua 5\.1\(\.\|$\)' + echoerr 'flog: warning: for speed improvements, please install LuaJIT 2.1' + elseif l:out !~# '\c^luajit 2\.1\(\.\|$\)' + echoerr 'flog: warning: only Lua 5.1 and LuaJIT 2.1 are supported' + endif + + return v:true + endif + + return v:false +endfunction + +function! flog#lua#GetBin() abort + let l:bin = '' + + if exists('g:flog_lua_bin') + let l:bin = shellescape(g:flog_lua_bin) + elseif executable('luajit') + let l:bin = 'luajit' + elseif executable('lua') + let l:bin = 'lua' + else + echoerr 'flog: please install LuaJIT 2.1 it or set it with g:flog_lua_bin' + throw g:flog_lua_not_found + endif + + call flog#lua#CheckBinVersion(l:bin) + + return l:bin +endfunction + +function! flog#lua#GetLibPath(lib) abort + return g:flog_lua_dir . '/flog/' . a:lib +endfunction + +function! flog#lua#SetLuaPath() abort + let l:had_lua_path = exists('$LUA_PATH') + let l:original_lua_path = $LUA_PATH + + let $LUA_PATH = escape(g:flog_lua_dir, '\;?') . '/?.lua' + + return [l:had_lua_path, l:original_lua_path] +endfunction + +function! flog#lua#ResetLuaPath(lua_path_info) abort + let [l:had_lua_path, l:original_lua_path] = a:lua_path_info + + if !l:had_lua_path + unlet $LUA_PATH + else + let $LUA_PATH = l:original_lua_path + endif +endfunction diff --git a/autoload/flog/shell.vim b/autoload/flog/shell.vim new file mode 100644 index 0000000..bd0bd34 --- /dev/null +++ b/autoload/flog/shell.vim @@ -0,0 +1,24 @@ +" +" This file contains functions for working with shell commands. +" + +function! flog#shell#Escape(str) abort + " Fix bug where '-' is escaped + if a:str ==# '-' + return a:str + endif + return fnameescape(a:str) +endfunction + +function! flog#shell#EscapeList(list) abort + return map(copy(a:list), 'flog#shell#Escape(v:val)') +endfunction + +function! flog#shell#Run(cmd) abort + let l:output = systemlist(a:cmd) + if !empty(v:shell_error) + echoerr join(l:output, "\n") + throw g:flog_shell_error + endif + return l:output +endfunction diff --git a/autoload/flog/state.vim b/autoload/flog/state.vim new file mode 100644 index 0000000..310c6c2 --- /dev/null +++ b/autoload/flog/state.vim @@ -0,0 +1,256 @@ +" +" This file contains functions for creating and updating the internal state +" object. +" + +let g:flog_instance_counter = 0 + +function! flog#state#Create() abort + let l:state = { + \ 'instance_number': g:flog_instance_counter, + \ 'opts': {}, + \ 'prev_log_cmd': '', + \ 'graph_bufnr': -1, + \ 'workdir': '', + \ 'commits': [], + \ 'commits_by_hash': {}, + \ 'line_commits': [], + \ 'commit_marks': {}, + \ 'tmp_side_wins': [], + \ } + + let g:flog_instance_counter += 1 + + return l:state +endfunction + +function! flog#state#GetInternalDefaultOpts() abort + let l:defaults = { + \ 'raw_args': '', + \ 'format': '%ad [%h] {%an}%d %s', + \ 'date': 'iso', + \ 'all': v:false, + \ 'bisect': v:false, + \ 'merges': v:true, + \ 'reflog': v:false, + \ 'reverse': v:false, + \ 'graph': v:true, + \ 'patch': v:true, + \ 'skip': '', + \ 'order': '', + \ 'max_count': '5000', + \ 'open_cmd': 'tabedit', + \ 'search': '', + \ 'patch_search': '', + \ 'author': '', + \ 'limit': '', + \ 'rev': [], + \ 'path': [], + \ } + + " Show deprecation warning for old setting + call flog#deprecate#Setting( + \ 'g:flog_permanent_default_arguments', + \ 'g:flog_permanent_default_opts' + \ ) + + " Read the user immutable defaults + if exists('g:flog_permanent_default_opts') + for [l:key, l:value] in items(g:flog_permanent_default_opts) + if has_key(l:defaults, l:key) + let l:defaults[key] = l:value + else + echoerr 'Warning: unrecognized permanent default option ' . l:key + endif + endfor + endif + + if type(l:defaults.max_count) == v:t_number + let l:defaults.max_count = string(l:defaults.max_count) + endif + + if type(l:defaults.skip) == v:t_number + let l:defaults.skip = string(l:defaults.skip) + endif + + return l:defaults +endfunction + +function! flog#state#GetDefaultOpts() abort + let l:defaults = flog#state#GetInternalDefaultOpts() + + " Show deprecation warning for old setting + call flog#deprecate#Setting( + \ 'g:flog_default_arguments', + \ 'g:flog_default_opts' + \ ) + + " Read the user defaults + if exists('g:flog_default_opts') + for [l:key, l:value] in items(g:flog_default_opts) + if has_key(l:defaults, l:key) + let l:defaults[key] = l:value + else + echoerr 'Warning: unrecognized default option ' . l:key + endif + endfor + endif + + if type(l:defaults.max_count) == v:t_number + let l:defaults.max_count = string(l:defaults.max_count) + endif + + if type(l:defaults.skip) == v:t_number + let l:defaults.skip = string(l:defaults.skip) + endif + + return l:defaults +endfunction + +function! flog#state#SetOpts(state, opts) abort + let a:state.opts = a:opts + return a:opts +endfunction + +function! flog#state#GetOpts(state) abort + return a:state.opts +endfunction + +function! flog#state#GetResolvedOpts(state) abort + let l:opts = copy(a:state.opts) + + let l:opts.bisect = l:opts.bisect && !l:opts.limit + let l:opts.reflog = l:opts.reflog && !l:opts.limit + + return l:opts +endfunction + +function! flog#state#SetPrevLogCmd(state, prev_log_cmd) abort + let a:state.prev_log_cmd = a:prev_log_cmd + return a:prev_log_cmd +endfunction + +function! flog#state#SetGraphBufnr(state, bufnr) abort + let a:state.graph_bufnr = a:bufnr + return a:bufnr +endfunction + +function! flog#state#SetWorkdir(state, workdir) abort + let a:state.workdir = a:workdir + return a:workdir +endfunction + +function! flog#state#GetWorkdir(state) abort + return a:state.workdir +endfunction + +function! flog#state#GetCommitRefs(commit) abort + let l:refs = [] + + for l:ref in split(a:commit.refs, ', ') + let l:match = matchlist(l:ref, '\v^(([^ ]+) -\> )?(tag: )?((refs/(remote|.*)?/)?((.{-}/)?(.*)))') + + " orig: The name of the original path, ex. "HEAD" + " tag: Whether the ref is a tag + " prefix: ex. "refs/remotes", "refs/bisect", etc. + " remote: Remote name only + " full: Full path including refs/.*/ + " path: Path with remote + " tail: End of path only (no remote) + call add(l:refs, { + \ 'orig': l:match[2], + \ 'tag': !empty(l:match[3]), + \ 'prefix': l:match[5][ : -2], + \ 'remote': l:match[8][ : -2], + \ 'full': l:match[4], + \ 'path': l:match[7], + \ 'tail': l:match[9], + \ }) + endfor + + return l:refs +endfunction + +function! flog#state#SetGraph(state, graph) abort + " Selectively set graph properties + let a:state.commits = a:graph.commits + let a:state.commits_by_hash = a:graph.commits_by_hash + let a:state.line_commits = a:graph.line_commits + return a:graph +endfunction + +function! flog#state#IsReservedCommitMark(key) abort + return a:key =~# '[<>@~^!]' +endfunction + +function! flog#state#IsDynamicCommitMark(key) abort + return a:key =~# '[<>@~^]' +endfunction + +function! flog#state#IsCancelCommitMark(key) abort + " 27 is the key code for + return char2nr(a:key) == 27 +endfunction + +function! flog#state#ResetCommitMarks(state) abort + let l:new_commit_marks = {} + let a:state.commit_marks = l:new_commit_marks + return l:new_commit_marks +endfunction + +function! flog#state#HasCommitMark(state, key) abort + if flog#state#IsDynamicCommitMark(a:key) + return v:true + endif + if flog#state#IsCancelCommitMark(a:key) + throw g:flog_invalid_commit_mark + endif + return has_key(a:state.commit_marks, a:key) +endfunction + +function! flog#state#SetInternalCommitMark(state, key, commit) abort + let a:state.commit_marks[a:key] = a:commit + return a:commit +endfunction + +function! flog#state#SetCommitMark(state, key, commit) abort + if flog#state#IsReservedCommitMark(a:key) + throw g:flog_invalid_commit_mark + endif + return flog#state#SetInternalCommitMark(a:state, a:key, a:commit) +endfunction + +function! flog#state#GetCommitMark(state, key) abort + return get(a:state.commit_marks, a:key, {}) +endfunction + +function! flog#state#RemoveCommitMark(state, key) abort + if !has_key(a:state.commit_marks, a:key) + return {} + endif + return remove(a:state.commit_marks, a:key) +endfunction + +function! flog#state#SetTmpSideWins(state, tmp_side_wins) abort + let a:state.tmp_side_wins = a:tmp_side_wins + return a:tmp_side_wins +endfunction + +function! flog#state#ResetTmpSideWins(state) abort + return flog#state#SetTmpSideWins(a:state, []) +endfunction + +function! flog#state#SetBufState(state) abort + let b:flog_state = a:state +endfunction + +function! flog#state#HasBufState() abort + return exists('b:flog_state') +endfunction + +function! flog#state#GetBufState() abort + if !flog#state#HasBufState() + throw g:flog_missing_state + endif + return b:flog_state +endfunction diff --git a/autoload/flog/str.vim b/autoload/flog/str.vim new file mode 100644 index 0000000..2ef41bf --- /dev/null +++ b/autoload/flog/str.vim @@ -0,0 +1,11 @@ +" +" This file contains functions for working with strings. +" + +function! flog#str#Ellipsize(str, max_len) abort + if len(a:str) > a:max_len + return a:str[ : a:max_len - 4] . '...' + endif + + return a:str +endfunction diff --git a/autoload/flog/tab.vim b/autoload/flog/tab.vim new file mode 100644 index 0000000..046b9fa --- /dev/null +++ b/autoload/flog/tab.vim @@ -0,0 +1,12 @@ +" +" This file contains functions for handling tabs. +" + +function! flog#tab#GetInfo() abort + return [tabpagenr(), tabpagenr('$')] +endfunction + +function! flog#tab#DidCloseRight(tab_info) abort + let [l:current_tab, l:last_tab] = a:tab_info + return l:last_tab > tabpagenr('$') && l:current_tab == tabpagenr() +endfunction diff --git a/autoload/flog/test.vim b/autoload/flog/test.vim new file mode 100644 index 0000000..6c602d4 --- /dev/null +++ b/autoload/flog/test.vim @@ -0,0 +1,9 @@ +" +" This file contains utils only for use in tests. +" + +function! flog#test#Assert(cmd) abort + if !eval(a:cmd) + echoerr a:cmd + endif +endfunction diff --git a/autoload/flog/win.vim b/autoload/flog/win.vim new file mode 100644 index 0000000..dc226a0 --- /dev/null +++ b/autoload/flog/win.vim @@ -0,0 +1,74 @@ +" +" This file contains functions for handling windows. +" + +function! flog#win#GetAllIds() abort + let l:windows = [] + for l:tab in gettabinfo() + let l:windows += l:tab.windows + endfor + return l:windows +endfunction + +function! flog#win#Save() abort + return [win_getid(), bufnr(), winsaveview(), virtcol('.'), virtcol('$')] +endfunction + +function! flog#win#GetSavedId(saved_win) abort + return a:saved_win[0] +endfunction + +function! flog#win#GetSavedBufnr(saved_win) abort + return a:saved_win[1] +endfunction + +function! flog#win#GetSavedView(saved_win) abort + return a:saved_win[2] +endfunction + +function! flog#win#GetSavedVcol(saved_win) abort + return a:saved_win[3] +endfunction + +function! flog#win#GetSavedVcols(saved_win) abort + return a:saved_win[4] +endfunction + +function! flog#win#Is(saved_win) abort + return win_getid() == a:saved_win[0] +endfunction + +function! flog#win#Restore(saved_win) abort + let [l:win_id, l:bufnr, l:view, _, _] = a:saved_win + + silent! call win_gotoid(l:win_id) + + let l:new_win_id = win_getid() + + if flog#win#Is(a:saved_win) + call winrestview(l:view) + call flog#win#RestoreVcol(a:saved_win) + endif + + return l:new_win_id +endfunction + +function! flog#win#RestoreTopline(saved_win) abort + let l:view = flog#win#GetSavedView(a:saved_win) + + if l:view.topline == 1 + return -1 + endif + + let l:topline = l:view.topline - l:view.lnum + line('.') + + call winrestview({ 'topline': l:topline }) + + return l:topline +endfunction + +function! flog#win#RestoreVcol(saved_win) abort + let l:vcol = flog#win#GetSavedVcol(a:saved_win) + call setcursorcharpos('.', l:vcol) + return l:vcol +endfunction diff --git a/doc/flog.txt b/doc/flog.txt index 86a1193..825d920 100644 --- a/doc/flog.txt +++ b/doc/flog.txt @@ -1,4 +1,4 @@ -*flog.txt* A branch viewer for fugitive +*flog.txt* A git branch viewer for Vim Flog *flog* Author: Roger Bongers @@ -7,137 +7,225 @@ License: Same terms as Vim itself (see |license|) ============================================================================== INTRODUCTION *flog-intro* -Flog is a lightweight branch viewer for vim. Flog provides a wrapper to "git -log --graph" that can be run from any git repository. It provides several -helpful commands and bindings to interact with the graph. Flog integrates with -|fugitive|. See "git log --help" and |fugitive| for more details. +Flog is a lightweight git branch viewer for Vim. It provides several helpful +commands and mappings to interact with the git branch graph. + +============================================================================== +PREREQUISITES *flog-prereqs* + +Flog supports Vim 8, Vim 9, and Neovim. + +Flog requires the |fugitive| plugin to be installed. + +In Vim 8/9, Flog requires LuaJIT 2.1 to be installed on the system. + +See also |g:flog_lua_bin|, |g:flog_use_internal_lua|, +|g:flog_check_lua_version|. ============================================================================== COMMANDS *flog-commands* +------------------------------------------------------------------------------ +FLOG OPEN COMMANDS *flog-open-commands* + +These commands open Flog. + :Flog *:Flog* - Open the log graph in a new tab. This command can be run from any file in a - git repository. Any |fugitive-commands| can be run from the buffer. + Open Flog in a new tab, showing the git branch graph. This command can be + run from any file in a git repository. Any |fugitive-commands| can be run + from the buffer. - When opened in visual mode, the currently selected range and filename is - passed to "-limit=". + When opened in visual mode or passed a range, the currently selected range + and filename is passed to "-limit=". - The following options are supported: + The first time this is called, runs "git commit-graph write". This can be + changed with |g:flog_write_commit_graph|. - *:Flog-all* - -all Enable the "--all" argument by default. +:Flog -all *:Flog_-all* +:Flog -no-all *:Flog_-no-all* - *:Flog-bisect* - -bisect Enable the "--bisect" argument by default. + When enabled, pass the "--all" argument to "git log". + Disabled when |:Flog_-limit| is set. + Disabled by default. - *:Flog-no-merges* - -no-merges Enable the "--no-merges" argument by default. +:Flog -bisect *:Flog_-bisect* +:Flog -no-bisect *:Flog_-no-bisect* - *:Flog-reflog* - -reflog Enable the "--reflog" argument by default. + When enabled, pass the "--bisect" argument to "git log". + Disabled by default. - *:Flog-reverse* - -reverse Enable the "--reverse" argument by default. +:Flog -merges *:Flog_-merges* +:Flog -no-merges *:Flog_-no-merges* - *:Flog-no-graph* - -no-graph Disable the "--graph" argument by default. + When disabled, pass the "--no-merges" argument to "git log". + Enabled by default. - *:Flog-no-patch* - -no-patch Enable the "--no-patch" argument by default. +:Flog -reflog *:Flog_-reflog* +:Flog -no-reflog *:Flog_-no-reflog* - *:Flog-skip* - -skip= Passed to "--skip", skipping that number of commits by - default. + When enabled, pass the "--reflog" argument to "git log". + Disabled by default. - *:Flog-max-count* - -max-count= Passed to "--max-count", limiting to that number of - commits by default. +:Flog -reverse *:Flog_-reverse* +:Flog -no-reverse *:Flog_-no-reverse* - *:Flog-open-cmd* - -open-cmd= The command to use to open the window containing the - graph. Must give initial focus to the window. Defaults - to |tabedit|. + When enabled, pass the "--reverse" argument to "git log". + Requires |:Flog_-no-graph|. + Disabled by default. - *:Flog-format* - -format= A format specifier to pass to "--pretty=format:". - Multiline formats are supported. +:Flog -patch *:Flog_-patch* +:Flog -no-patch *:Flog_-no-patch* - *:Flog-date* - -date= The date format to pass to "--date=". Currently, - "short" and "iso8601" are supported. + When disabled, pass the "--no-patch" argument to "git log". + Enabled by default. - *:Flog-search* - -search= A regex pattern to pass to "--grep=". +:Flog -graph *:Flog_-graph* +:Flog -no-graph *:Flog_-no-graph* - *:Flog-patch-search* - -patch-search= A regex pattern to pass to "-G". + When disabled, do not draw the git branch graph. + The git branch cannot be drawn when |:Flog_-reverse| is set. + Enabled by default. - *:Flog-author* - -author= An author name to pass to "--author=". +:Flog -author= *:Flog_-author* - *:Flog-limit* - -limit= A limit to pass to "-L". This will restrict commit - history to the specified file/lines. This disables - some options. + Passes the "--author=" argument to "git log". - Because it uses the "git log -L" argument, this option - will display a patch inline to the graph window. This - can be toggled with "gp". +:Flog -date= *:Flog_-date* - Instead of manually specifying this argument, you can - also use a range or visually select some lines and - type ":Flog". + Passes the "--date=" argument to "git log". + All named formats are supported. - *:Flog-sort* - -sort= Sort by one of , where type is in "date", - "author", or "topo". These correspond to the options - "--date-order", "--author-order", and "--topo-order" - respectfully. +:Flog -format= *:Flog_-format* - *:Flog-rev* - -rev= The git revision to pass to the log command. Can be - specified more than once. When "-limit=" is specified, - only the first revision is used. + Passes the "--pretty=format:" argument to "git log". + Multiline formats are supported. - *:Flog-path* - -path= A path to pass to the log command. This option can be - specified multiple times for multiple paths. + To support highlighting, special items should go at the start. + This includes date, hash, author, and refs. - *:Flog-raw-args* - -raw-args= Additional args to pass to "git log --graph". No args - passed in using this option are guaranteed to work. - Can be specified multiple times, which will be - combined together. +:Flog -limit= *:Flog_-limit* - *:Flog--* - -- Parse the rest of the arguments as if they were passed - to -raw-args. Does not require escaping spaces. + Passes the "-L=" argument to "git log". + The patch displayed can be disabled with |:Flog_-no-patch|. -:Flogsplit *:Flogsplit* + You can also specify this argument by passing a range to the command. + You can also visually select lines and run the command. + This will use the current file and selected lines for the limit. - Open the log graph in a split. Supports ||. Behaves the same as - |:Flog| otherwise. +:Flog -max-count= *:Flog_-max-count* -:Flogsplitcommit *:Flogsplitcommit* + Passes the "--max-count=" argument to "git log". - Open a commit under the cursor using |:Gsplit| in a |flog-temp-window|. Can - only be run in the |:Flog| window. + Defaults to 5000. - Sets |flog-'!|. +:Flog -order= *:Flog_-order* +:Flog -sort= *:Flog_-sort* + + Sort by one of , where type is in "date", "author", or "topo". + + These pass either the argument "--date-order", "--author-order", or + "--topo-order" to "git log", respectfully. + + Defaults to "topo" when |:Flog_-graph| is enabled and "date" otherwise. + +:Flog -skip= *:Flog_-skip* + + Passes the "--skip=" argument to "git log". + +:Flog -search= *:Flog_-search* +:Flog -grep= *:Flog_-grep* + + Passes the "--grep=" argument to "git log". + +:Flog -patch-search= *:Flog_-patch-search* +:Flog -patch-grep= *:Flog_-patch-grep* + + Passes the "-G=" argument to "git log". + +:Flog -rev= *:Flog_-rev* + + A git revision to pass to "git log". + Can be specified more than once. + + When |:Flog_-limit| is specified, only the first revision is used. + +:Flog -path= *:Flog_-path* + + A path to pass to "git log". + This option can be specified multiple times for multiple paths. + + When |:Flog_-limit| is specified, this is not used. + +:Flog -open-cmd= *:Flog_-open-cmd* + + The command to use to open the window containing the graph. + Must give initial focus to the window. + Defaults to |tabedit|. + +:Flog -raw-args= *:Flog_-raw-args* + + Additional args to pass to "git log". + No args passed in using this option are guaranteed to work. + Can be specified multiple times. + +:Flog -- *:Flog_--* + + Parse the remaining arguments as if they were passed to |:Flog_-raw-args|. + Does not require escaping spaces. + +:Flogsplit *:Flogsplit* + + Open Flog in a split. Supports ||. Behaves the same as |:Flog| + otherwise. + +------------------------------------------------------------------------------ +FLOG GIT COMMANDS *flog-git-commands* + +These commands are wrappers to |:Git|. :Floggit *:Floggit* - Open a git command via |:Git|. + Open a git command via |:Git| using |flog#Exec()|. + + All arguments supported by |:Git| are supported. Revision, file, and option completion is available for this command. - While in a |:Flog| window, completion is available for the current line or + While in the |:Flog| window, completion is available for the current line or range. + By default, returns focus to the |:Flog| window after running the command. + + It updates the |:Flog| window after returning to it by default. + + If called outside of a |:Flog| window, calls |:Git| and ignores anything + relating to Flog. + :Floggit! *:Floggit!* - Same as |:Floggit|, but open the command using a |flog-temp-window|. + Same as |:Floggit|, but use |:Git!|. + +:Floggit --focus *:Floggit_--focus* *:Floggit_-f* +:Floggit -f + + Instead of returning focus to the |:Flog| window after running the command, + retain focus. + +:Floggit --static *:Floggit_--static* *:Floggit_-s* +:Floggit -s + + Prevent updating |:Flog| after running the command. + +:Floggit --tmp *:Floggit_--tmp* *:Floggit_-t* +:Floggit -t + + Any windows will run in a temporary |flog-side-window|. + +------------------------------------------------------------------------------ +FLOG BUFFER COMMANDS *flog-buffer-commands* + +These commands only work in the |:Flog| buffer. :Flogsetargs *:Flogsetargs* @@ -149,16 +237,12 @@ COMMANDS *flog-commands* Same as |:Flogsetargs|, but overwrite all of the current arguments instead of updating them. -:Flogjump *:Flogjump* - - Jump to the ref name given by the arguments. Must be a ref name currently in - the graph. - - Can only be run in a |:Flog| window. +:Flogsplitcommit *:Flogsplitcommit* - Supports ref name completion. + Open a commit under the cursor using |:Gsplit| in a |flog-temp-window|. Can + only be run in the |:Flog| window. - Sets |flog-''| to the last commit under the cursor. + Sets |flog-'!|. ============================================================================== MAPPINGS *flog-mappings* @@ -171,103 +255,147 @@ MISC. MAPPINGS *flog-misc-mappings* *(FlogHelp)* g? *flog-g?* - Jump to this list of mappings in help. + Show these mappings. *(FlogVSplitCommitRight)* *flog-* - Open the commit under the cursor in a temporary window. + Open the commit under the cursor in a temporary |flog-side-window|. Sets |flog-'!| to the commit under the cursor. *(FlogVSplitCommitRightPath)* *flog-* - Same as |flog-|, but use the "-path" argument to only show the diff for - the currently selected paths. + Same as |flog-|, but use the "-path" or "-limit" argument to only show + the diff for the currently selected paths. - Sets |flog-'!| to the commit under the cursor. + *(FlogGit)* +git *flog-git* + + Enter in the characters ":Floggit" from normal or visual mode in order to + quickly begin a |:Floggit| command. + + *(FlogYank)* +y *flog-y* + + In normal mode, copy the short commit hash under the current line to a + register. Accepts a count. + + In visual mode, copies the selected commit hashes to a register. + + *(FlogUpdate)* +u *flog-u* + + Update/reload the |:Flog| window. + + *(FlogCloseTmpWin)* +dq *flog-dq* + + Close all temporary |flog-side-window|s. + + *(FlogQuit)* +gq *flog-gq* +ZZ *flog-ZZ* + + Quit Flog. + +------------------------------------------------------------------------------ +DIFF MAPPINGS *flog-diff-mappings* *(FlogVDiffSplitRight)* dd *flog-dd* dv *flog-dv* - In normal mode, open a diff in a temporary window comparing the commit under - the cursor with the current commit. + In normal mode, open a diff in a temporary |flog-side-window| comparing the + commit under the cursor with the current HEAD. - Also sets |flog-'!| to the commit under the cursor in normal mode. + In normal mode, also sets |flog-'!| to the commit under the cursor. - In visual mode, open a diff in a temporary window comparing the commits at - the top and bottom of the visual selection. + In visual mode, open a diff in a temporary |flog-side-window| comparing the + commits at the top and bottom of the visual selection. *(FlogVDiffSplitPathsRight)* dp *flog-dp* DD *flog-DD* DV *flog-DV* - Same as |flog-dd|, but use the "-path" argument to only show the diff for - the currently selected paths. + Same as |flog-dd|, but use the "-path" or "-limit" argument to only show the + diff for the currently selected paths. *(FlogVDiffSplitLastCommitRight)* d! *flog-d!* - Diff the commit under the cursor and the last used commit. + Diff the commit under the cursor and the last used commit in a temporary + |flog-side-window|. + See also |flog-'!|. *(FlogVDiffSplitLastCommitPathsRight)* D! *flog-D!* - Same as |flog-d!|, but use the "-path" argument to only show the diff for - the currently selected paths. - - *(FlogCloseTmpWin)* -dq *flog-dq* - - Close all temporary windows. + Same as |flog-d!|, but use the "-path" or "-limit" argument to only show the + diff for the currently selected paths. - *(FlogYank)* -y *flog-y* + *(FlogVSplitStaged)* +gs *flog-gs* - Copy the short commit hash under the current line to a register. Accepts a - count. + Show the currently staged changes. - *(FlogGit)* -git *flog-git* + *(FlogVSplitUntracked)* +gu *flog-gu* - Enter in the characters ":Floggit" from normal or visual mode in order to - quickly begin a |:Floggit| command. + Show the currently untracked and unstaged changes. - *(FlogQuit)* -gq *flog-gq* -ZZ *flog-ZZ* + *(FlogVSplitUnstaged)* +gU *flog-gU* - Quit Flog. + Show the currently unstaged changes. ------------------------------------------------------------------------------ NAVIGATION MAPPINGS *flog-navigation-mappings* + *(FlogJumpToCommitStart)* +^ *flog-^* + + Move the cursor to the start of the current commit branch. + + If the cursor is already on or before the commit branch, jumps to the column + which marks the start of the commit content. + + *(FlogNextCommit)* +) *flog-)* + + Jump to the next commit. + + Accepts a count. + Sets |flog-''| to the last commit under the cursor. + + *(FlogPrevCommit)* +( *flog-(* + + Same as |flog-)|, but jump backwards. + *(FlogVNextCommitRight)* *flog-CTRL-N* -) *flog-)* - Jump to the next commit and open it in a temporary window. Accepts a count. + Jump to the next commit and open it in a temporary |flog-side-window|. + Accepts a count. Sets |flog-''| to the last commit under the cursor. Sets |flog-'!| to the new commit under the cursor. *(FlogVPrevCommitRight)* *flog-CTRL-P* -( *flog-(* Same as |flog-|, but jump backwards. *(FlogVNextRefRight)* ]r *flog-]r* - Jump to the next commit with a ref name and open it in a temporary window. - Accepts a count. + Jump to the next commit with a ref name. + Accepts a count. Sets |flog-''| to the last commit under the cursor. - Sets |flog-'!| to the new commit under the cursor. *(FlogVPrevRefRight)* [r *flog-[r* @@ -277,8 +405,10 @@ NAVIGATION MAPPINGS *flog-navigation-mappings* *(FlogSkipAhead)* ]] *flog-]]* - Go forward in the commit history by "--max-count" commits, if that argument - is set. Accepts a count. + Go forward in the commit history by |:Flog_-max-count| commits, if that + option is set. + + Accepts a count. *(FlogSkipBack)* [[ *flog-[[* @@ -286,22 +416,20 @@ NAVIGATION MAPPINGS *flog-navigation-mappings* Same as |flog-]]|, but go backwards. *(FlogSetSkip)* -go *flog-go* +gcg *flog-gcg* Skip to the commit given by the count, or 0 if no count is given. - *(FlogSetCommitMark)* -m{a-zA-Z'} *flog-m* + *(FlogSetRev)* +gct *flog-gct* - Mark the current commit under the cursor. - See also |flog-commit-marks|. + Set |:Flog_-rev| to the commit under the cursor and clear |:Flog_-skip|. + Similar to |zt|, this will set the commit to the top of the output. - *(FlogJumpToCommitMark)* -'{a-zA-Z'} *flog-'* + *(FlogClearRev)* +gcc *flog-gcc* - Jump to the marked commit. - Sets |flog-''| to the last commit under the cursor. - See also |flog-commit-marks|. + Unset |:Flog-rev|. ------------------------------------------------------------------------------ ARGUMENT MODIFIER MAPPINGS *flog-argument-mappings* @@ -309,75 +437,70 @@ ARGUMENT MODIFIER MAPPINGS *flog-argument-mappings* *(FlogToggleAll)* a *flog-a* - Toggle the "--all" argument. + Toggle |:Flog-all|. *(FlogToggleBisect)* gb *flog-gb* - Toggle the "--bisect" argument. + Toggle |:Flog-bisect|. *(FlogToggleNoMerges)* gm *flog-gm* - Toggle the "--no-merges" argument. + Toggle |:Flog-merges|. *(FlogToggleReflog)* gr *flog-gr* - Toggle the "--reflog" argument. + Toggle |:Flog-reflog|. *(FlogToggleNoGraph)* gx *flog-gx* - Toggle the "--graph" argument. + Toggle |:Flog-graph|. *(FlogToggleNoPatch)* gp *flog-gp* - Toggle the "--no-patch" argument. Useful while the "-limit" option is - specified. + Toggle |:Flog-patch|. -gss *(FlogCycleSort)* - *flog-gss* - - Cycle through the different sort options; "--date-order", - "--author-date-order", and "--topo-order". - -gsd *(FlogSortDate)* - *flog-gsd* + *(FlogSearch)* +g/ *flog-g/* - Set the "--date-order" option. Conflicts with other sort options. + Open the command line with ":Flogsetargs -search=". + See also |:Flog-search|. -gsa *(FlogSortAuthor)* - *flog-gsa* + *(FlogPatchSearch)* +g\ *flog-g\* - Set the "--author-date-order" option. Conflicts with other sort options. + Open the command line with ":Flogsetargs -patch-search=". + See also |:Flog-patch-search|. -gst *(FlogSortTopo)* - *flog-gst* + *(FlogCycleOrder)* +goo *flog-goo* - Set the "--topo-order" option. Conflicts with other sort options. + Cycle through the different |:Flog-order| options. + The order of sort types can be changed with |g:flog_order_types|. -gsr *(FlogToggleReverse)* - *flog-gsr* + *(FlogOrderDate)* +god *flog-god* - Toggle the "--reverse" argument. Does not conflict with other sort options. - Mnemonic: "sort reverse". + Set |:Flog-order| to "date". - *(FlogUpdate)* -u *flog-u* + *(FlogOrderAuthor)* +goa *flog-goa* - Update/reload the graph window. + Set |:Flog-order| to "author". - *(FlogSearch)* -g/ *flog-g/* + *(FlogOrderTopo)* +got *flog-got* - Open the command line with ":Flogsetargs -search=". + Set |:Flog-order| to "topo". - *(FlogPatchSearch)* -g\ *flog-g\* + *(FlogToggleReverse)* +gor *flog-gor* - Open the command line with ":Flogsetargs -patch-search=". + Toggle |:Flog-reverse|. ------------------------------------------------------------------------------ COMMIT AND BRANCH MAPPINGS *flog-commit-mappings* @@ -386,21 +509,25 @@ COMMIT AND BRANCH MAPPINGS *flog-commit-mappings* cf *flog-cf* Create a "--fixup" commit for the commit under the cursor. + See also "git commit --help". *(FlogFixupRebase)* cF *flog-cF* Same as |flog-cf|, but immediately perform a "rebase --autosquash". + See also "git rebase --help". *(FlogSquash)* cs *flog-cs* Create a "--squash" commit for the commit under the cursor. + See also "git commit --help". *(FlogSquashRebase)* cS *flog-cS* Same as |flog-cs|, but immediately perform a "rebase --autosquash". + See also "git rebase --help". *(FlogSquashEdit)* @@ -418,6 +545,7 @@ crc *flog-crc* crn *flog-crn* Same as |flog-crc|, but use the "--no-edit" flag. + See also "git rebase --help". *(FlogCheckout)* coo *flog-coo* @@ -428,41 +556,63 @@ coo *flog-coo* cob *flog-cob* Checkout the first branch name under the cursor, prioritizing local - branches, or use the hash if there is no branch name. + branches. *(FlogCheckoutLocalBranch)* -cot *flog-cot* +col *flog-col* Checkout the first local branch name under the cursor, or checkout the first - upstream branch with the remote name trimmed from the ref, causing it to be - tracked locally. + remote branch with the remote name trimmed from the ref. - If no branch name was found do nothing. + This will cause remote branches to be tracked locally if they are not + already. *(FlogGitCommit)* c *flog-c* Start a command line with ":Floggit commit ". + See also |:Floggit|. *(FlogGitRevert)* cr *flog-cr* Start a command line with ":Floggit revert ". + See also |:Floggit|. *(FlogGitMerge)* cm *flog-cm* Start a command line with ":Floggit merge ". + See also |:Floggit|. *(FlogGitCheckout)* co *flog-co* Start a command line with ":Floggit checkout ". + See also |:Floggit|. *(FlogGitBranch)* cb *flog-cb* Start a command line with ":Floggit branch ". + See also |:Floggit|. + +------------------------------------------------------------------------------ +COMMIT MARK MAPPINGS *flog-mark-mappings* + + *(FlogSetCommitMark)* +m{a-zA-Z'} *flog-m* + + Mark the current commit under the cursor. + See also |flog-commit-marks|. + + *(FlogJumpToCommitMark)* +'{a-zA-Z'} *flog-'* + + Jump to the marked commit. + Sets |flog-''| to the last commit under the cursor. + + See also |flog-commit-marks|. ------------------------------------------------------------------------------ REBASE MAPPINGS *flog-rebase-mappings* @@ -473,41 +623,49 @@ ri *flog-ri* Start an interactive rebase using the root of the commit under the cursor, if it is available. + See also "git rebase --help". + *(FlogRebaseInteractiveAutosquash)* rf *flog-rf* - Same as |flog-ri|, but use the "--autosquash" argument if it is available. - + Perform an autosquash rebase without editing the TODO list. + See also "git rebase --help". *(FlogRebaseInteractiveUpstream)* ru *flog-ru* Perform an interactive rebase against "@{upstream}". + See also "git rebase --help". *(FlogRebaseInteractivePush)* rp *flog-rp* Perform an interactive rebase against "@{push}". + See also "git rebase --help". *(FlogRebaseContinue)* rr *flog-rr* Run "git rebase --continue". + See also "git rebase --help". *(FlogRebaseSkip)* rs *flog-rs* Run "git rebase --skip". + See also "git rebase --help". *(FlogRebaseAbort)* ra *flog-ra* Run "git rebase --abort". + See also "git rebase --help". *(FlogRebaseEditTodo)* re *flog-re* Run "git rebase --edit-todo". + See also "git rebase --help". *(FlogRebaseInteractiveReword)* rw *flog-rw* @@ -515,139 +673,215 @@ rw *flog-rw* Start an interactive rebase with the commit under the cursor set to "reword". + See also "git rebase --help". + *(FlogRebaseInteractiveEdit)* rm *flog-rm* Start an interactive rebase with the commit under the cursor set to "edit". + See also "git rebase --help". *(FlogRebaseInteractiveDrop)* rd *flog-rd* Start an interactive rebase with the commit under the cursor set to "drop". + See also "git rebase --help". *(FlogGitRebase)* r *flog-r* Start a command line with ":Floggit rebase ". + See also |:Floggit|. + See also "git rebase --help". + ============================================================================== -OPTIONS *flog-options* +SETTINGS *flog-settings* + +g:flog_default_opts *g:flog_default_opts* + + A |dict| containing the default values for arguments to |:Flog|. + + Keys in the dictionary use underscores instead of dashes. -g:flog_default_arguments *g:flog_default_arguments* + For example, the option for |:Flog-max-count| is "max_count", not + "max-count". - A |dict| containing the default values for arguments to |:Flog|. Keys in the - dictionary are in underscore format. + Boolean args are always positive. -g:flog_permanent_default_arguments *g:flog_permanent_default_arguments* + For example, the option for |:Flog-merges| is "merges", not "no_merges". - Like |g:flog_default_arguments|, only the arguments are treated as the - plugin defaults and can't be cleared using |:Flog|, |:Flogsetargs|, or - |:Flogsetargs!|. + Example: +> + let g:flog_default_opts = { 'max_count': 2000, 'all': v:true } +< + +g:flog_permanent_default_opts *g:flog_permanent_default_opts* + + Similar to |g:flog_default_opts|, but the arguments are used as internal + defaults when clearing arguments using |:Flogsetargs| or |:Flogsetargs!|. Has lower presidence than |g:flog_default_arguments|. -g:flog_build_log_command_fn *g:flog_build_log_command_fn* +g:flog_write_commit_graph *g:flog_write_commit_graph* - A string referring to a function to use to build a custom "git log" command. - Through this argument, you can use a custom visualizer. + When true, the first time |:Flog| is run for a repo, will run + "git commit-graph write" in order to speed up subsequent calls. - Example: + The arguments are controlled via |g:flog_write_commit_graph_args|. + + Defaults to true. + +g:flog_write_commit_graph_args *g:flog_write_commit_graph_args* + + The arguments to pass to "git commit-graph write" when + |g:flog_write_commit_graph| is enabled. + + Defaults to "--reachable --progress". + + See also "git commit-graph --help". + +g:flog_check_lua_version *g:flog_check_lua_version* + + When enabled, check that the installed version of Lua is supported. + + Defaults to true. + +g:flog_lua_bin *g:flog_lua_bin* + + The Lua interpretter to use when |g:flog_use_internal_lua| is disabled. + + Defaults to "luajit". + +g:flog_use_internal_lua *g:flog_use_internal_lua* + + When enabled, use the version of Lua included with Vim rather than calling + Lua directly. + + In most cases, this requires compiling Vim with: > - " This is the same as the built-in log command (flog#build_log_command) - function! FlogBuildLog() abort - " Same as "git --git-dir=" . flog#get_fugitive_git_dir() - " The git dir will be the current buffer's ".git" directory - let l:command = flog#get_fugitive_git_command() - let l:command .= ' log' - " Args and paths that would normally be passed to "git log" based on Flog options - let l:command .= flog#build_log_args() - let l:command .= ' -- ' - let l:command .= flog#build_log_paths() + ./configure \ + ... + --enable-luainterp=dynamic \ + --with-luajit +< + + See |install| for information on compiling Vim. + See also "./configure --help". + + Defaults to false. + +g:flog_get_author_args *g:flog_get_author_args* + + The arguments to use when completing authors with "git shortlog". + + Defaults to "--all --no-merges --max-count=100000". - return l:command - endfunction + See also "git shortlog --help". - let g:flog_build_log_command_fn = 'FlogBuildLog' +g:flog_commit_start_token *g:flog_commit_start_token* -g:flog_use_ansi_esc *g:flog_use_ansi_esc* + The internal token to use to find commit start. Provided as an option in + case your commit messages contain the default token alone on a line. - By default, Flog uses limited regex syntax highlighting. This option enables - terminal coloring using the AnsiEsc script. + Defaults to "__START". - For more details, help and installation, see AnsiEsc: - +g:flog_order_types *g:flog_order_types* + + An array of dicts describing the different order types. + + Can be set to change the order of order options with |flog-goo|. + + Defaults to: +> + [ + \ { name: 'date', args: '--date-order' }, + \ { name: 'author', args: '--author-date-order' }, + \ { name: 'topo', args: '--topo-order' }, + \ ] +< ============================================================================== FUNCTIONS *flog-functions* - *flog#run_command()* -flog#run_command({command}[, {keep-focus}[, {should-update}[, {is-tmp}]]]) + *flog#Exec()* +flog#Exec({command}[, {focus}[, {static}[, {tmp}]]]) - Runs a command in the graph. + Runs {command}. - {command} is the command that is run in the graph. The command will be - formatted according to |flog-command-format|. + Any window opened by the command will be considered a |flog-side-window|. - By default, focus returns to the graph window after running the command. - {keep-focus} allows the command to retrain focus. + By default, focus returns to the |:Flog| window after running the command. + {focus} allows the command to retain focus. - {should-update} updates the graph after running the command. + By default, updates the |:Flog| window after running the command. + {static} causes the window not to update after running the command. - {is-tmp} causes the window to run in a |flog-temp-window|. + {tmp} causes any windows to run in a temporary |flog-side-window|. Example: > - flog#run_command('Git show %h') + flog#Exec('Git fetch') < - *flog#run_tmp_command()* -flog#run_tmp_command({command}[, {keep-focus}[, {should-update}]]) + *flog#ExecTmp()* +flog#ExecTmp({command}[, {focus}[, {static}]]) - Same as |flog#run_command()|, but always open the command in a - |flog-temp-window|. + Same as |flog#Exec()|, but always open the command in a temporary + |flog-side-window|. - *flog#run_raw_command()* -flog#run_raw_command({command}[, {keep-focus}[, {should-update}[, {is-tmp}]]]) + *flog#Format()* +flog#Format({command}) - Same as |flog#run_command()|, but do not format {command}. + Format a command according to |flog-command-format|. + + Example: +> + flog#Format('Git show %h') +< ============================================================================== AUTOCOMMANDS *flog-autocommands* - *User_FlogCmdBufferSetup* -FlogCmdBufferSetup On initializing any command buffer. - See also |flog#run_command()|. + *User_FlogUpdate* +FlogUpdate On updating a |:Flog| window. + + *User_FlogSideWinSetup* +FlogSideWinSetup On initializing any |flog-side-window|. - *User_FlogTmpCmdBufferSetup* -FlogTmpCmdBufferSetup On initializing a temporary command buffer. - Called after |User_FlogCmdBufferSetup|. - See also |flog-temp-window|, |flog#run_command()|. + *User_FlogTmpSideWinSetup* +FlogTmpSideWinSetup On initializing a temporary |flog-side-window|. + Called after |User_FlogSideWinSetup|. - *User_FlogNonTmpCmdBufferSetup* -FlogNonTmpCmdBufferSetup On initializing a non-temporary command buffer. - Called after |User_FlogCmdBufferSetup|. - See also |flog-temp-window|, |flog#run_command()|. + *User_FlogNonTmpSideWinSetup* +FlogNonTmpSideWinSetup On initializing a non-temporary |flog-side-window|. + Called after |User_FlogSideWinSetup|. ============================================================================== ABOUT *flog-about* ------------------------------------------------------------------------------ -TEMPORARY WINDOWS *flog-temp-window* +SIDE WINDOWS *flog-side-window* - To view more information from commits in the graph, you can open temporary - windows. Opening a command in Flog temp windows closes any previously opened - temp windows. Temp windows will be closed when quitting Flog. +Flog can open windows beside the commit branch graph to show more commit +info. + +Side windows are opened with |flog#Exec()|. + +These windows can be set to temporary. Temporary windows will close when +other temporary windows are opened. Temporary windows close when Flog +closes. ------------------------------------------------------------------------------ COMMIT MARKS *flog-commit-marks* - Flog allows marking commits. Marked commits persist even when the graph - buffer updates. +Flog allows marking commits. Marked commits persist even when the |:Flog| +buffer updates. - Marked commits can be referenced using |flog-command-format|. Marked commits - can be created with |flog-m|. A commit mark can be jumped to with |flog-'|. +Marked commits can be referenced using |flog-command-format|. Marked commits +can be created with |flog-m|. A commit mark can be jumped to with |flog-'|. - The following special commit marks exist. +The following special commit marks exist. *flog-''* '' The last commit before jumping to another commit. @@ -662,26 +896,26 @@ COMMIT MARKS *flog-commit-marks* *flog-'@* '@ The current HEAD. - *flog-'~* - *flog-'^* + *flog-'~* *flog-'^* '~ '^ The parent of the current HEAD. *flog-'!* - '! The commit last used in a command with "%h". + '! The commit last used in a command with |flog-%h|. This mark is set by various commands and mappings. Can be thought of as the "last used commit". ------------------------------------------------------------------------------ COMMAND FORMAT *flog-command-format* - When running commands with |flog#run_command()|, a special syntax similar to + When running commands with |flog#Format()|, a special syntax similar to |printf| is used to transform the command string. Items prefixed by the "%" character are resolved into commit information and Flog options. - If any items cannot be resolved, such as if the graph buffer is empty or the - item uses an option like "-path" that was not set, commands will not be run. + If any items cannot be resolved, such as if the |:Flog| buffer is empty or + the item uses an option like "-path" that was not set, commands will not be + run. The following items are supported: @@ -695,7 +929,7 @@ COMMAND FORMAT *flog-command-format* *flog-%H* %H Same as %h, but do not set |flog-'!|. - *flog-%(h'x)* + *flog-%(h'* *flog-%(h')* %(h'x) The hash at the given commit mark "x". See also |flog-commit-marks|. @@ -704,7 +938,7 @@ COMMAND FORMAT *flog-command-format* any. Local branches prioritized first. Useful for checking out branches. - *flog-%(b'x)* + *flog-%(b'* *flog-%(b')* %(b'x) The branch at the given commit mark "x". *flog-%l* @@ -713,12 +947,16 @@ COMMAND FORMAT *flog-command-format* commit is used, trim the remote name from the branch. Useful for checking out commits for tracking. - *flog-%(l'x)* + *flog-%(l'* *flog-%(l')* %(l'x) The local branch at the given mark "x". *flog-%p* - %p The arguments passed to the |:Flog| "-path" option, if - any, escaped and joined together by spaces. + %p If |:Flog-limit| is set, resolves to the path passed + to the limit, escaping it. + + Otherwise, resolves to the paths passed to + |:Flog-path|, if any, escaping them and joining them + with spaces. *flog-%t* %t A tree for the current index. When this is used, a new diff --git a/ftplugin/floggraph.vim b/ftplugin/floggraph.vim index 2001dc3..34ab81e 100644 --- a/ftplugin/floggraph.vim +++ b/ftplugin/floggraph.vim @@ -1,16 +1,27 @@ -silent setlocal nomodifiable - \ readonly - \ noswapfile - \ nobuflisted - \ nowrap - \ buftype=nofile +" Settings + +silent setlocal \ bufhidden=wipe + \ buftype=nofile \ cursorline + \ nobuflisted \ nomodeline + \ nomodifiable + \ noswapfile + \ nowrap + \ readonly + +" Commands -" Mappings {{{ +command! -buffer -bang -range=0 -complete=customlist,flog#cmd#flog#args#Complete -nargs=* Flogsetargs call flog#cmd#FlogSetArgs([], !empty('')) +command! -buffer Flogsplitcommit call flog#Exec(flog#Format(' Gsplit %h'), 0, 1, 1) +command! -buffer Flogmarks call flog#floggraph#mark#PrintAll() -" Misc. mappings {{{ +" Deprecated commands + +command! -buffer -bang -nargs=* Flogjump call flog#deprecate#Command('Flogjump', '/ or ?') + +" Misc. Mappings if !hasmapto('(FlogHelp)') nmap g? (FlogHelp) @@ -25,7 +36,40 @@ nnoremap (FlogVSplitCommitRight) :vertical belowright Fl if !hasmapto('(FlogVSplitCommitPathsRight)') nmap (FlogVSplitCommitPathsRight) endif -nnoremap (FlogVSplitCommitPathsRight) :call flog#run_tmp_command('vertical belowright Git show %h -- %p') +nnoremap (FlogVSplitCommitPathsRight) :exec flog#Format('vertical belowright Floggit -s -t show %h -- %p') + +if !hasmapto('(FlogGit)') + nmap git (FlogGit) + vmap git (FlogGit) +endif +nnoremap (FlogGit) :Floggit +vnoremap (FlogGit) :Floggit + +if !hasmapto('(FlogYank)') + nmap y (FlogYank) + vmap y (FlogYank) +endif +nnoremap (FlogYank) :call flog#floggraph#reg#YankHash(v:register, '.', max([1, v:count])) +vnoremap (FlogYank) :call flog#floggraph#reg#YankHashRange(v:register, "'<", "'>") + +if !hasmapto('(FlogUpdate)') + nmap u (FlogUpdate) +endif +nnoremap (FlogUpdate) :call flog#floggraph#buf#Update() + +if !hasmapto('(FlogCloseTmpWin)') + nmap dq (FlogCloseTmpWin) +endif + +nnoremap (FlogCloseTmpWin) :call flog#floggraph#side_win#CloseTmp() + +if !hasmapto('(FlogQuit)') + nmap ZZ (FlogQuit) + nmap gq (FlogQuit) +endif +nnoremap (FlogQuit) :call flog#floggraph#buf#Close() + +" Diff mappings if !hasmapto('(FlogVDiffSplitRight)') nmap dd (FlogVDiffSplitRight) @@ -35,19 +79,17 @@ if !hasmapto('(FlogVDiffSplitRight)') endif if !hasmapto('(FlogVDiffSplitPathsRight)') - nmap dp (FlogVDiffSplitPathsRight) - vmap dp (FlogVDiffSplitPathsRight) nmap DD (FlogVDiffSplitPathsRight) vmap DD (FlogVDiffSplitPathsRight) nmap DV (FlogVDiffSplitPathsRight) vmap DV (FlogVDiffSplitPathsRight) endif -nnoremap (FlogVDiffSplitRight) :call flog#run_tmp_command('vertical belowright Git diff HEAD %h') -vnoremap (FlogVDiffSplitRight) :call flog#run_tmp_command("vertical belowright Git diff %(h'>) %(h'<)") +nnoremap (FlogVDiffSplitRight) :exec flog#Format('vertical belowright Floggit -s -t diff HEAD %h') +vnoremap (FlogVDiffSplitRight) :exec flog#Format("vertical belowright Floggit -s -t diff %(h'>) %(h'<)") -nnoremap (FlogVDiffSplitPathsRight) :call flog#run_tmp_command('vertical belowright Git diff HEAD %h -- %p') -vnoremap (FlogVDiffSplitPathsRight) :call flog#run_tmp_command("vertical belowright Git diff HEAD %(h'<) %(h'>) -- %p") +nnoremap (FlogVDiffSplitPathsRight) :exec flog#Format('vertical belowright Floggit -s -t diff HEAD %h -- %p') +vnoremap (FlogVDiffSplitPathsRight) :exec flog#Format("vertical belowright Floggit -s -t diff HEAD %(h'<) %(h'>) -- %p") if !hasmapto('(FlogVDiffSplitLastCommitRight)') nmap d! (FlogVDiffSplitLastCommitRight) @@ -57,50 +99,50 @@ if !hasmapto('(FlogVDiffSplitLastCommitPathsRight)') nmap D! (FlogVDiffSplitLastCommitPathsRight) endif -nnoremap (FlogVDiffSplitLastCommitRight) : call flog#run_tmp_command("vertical belowright Git diff %(h'!) %H") +nnoremap (FlogVDiffSplitLastCommitRight) :exec flog#Format("vertical belowright Floggit -s -t diff %(h'!) %H") -nnoremap (FlogVDiffSplitLastCommitPathsRight) : call flog#run_tmp_command("vertical belowright Git diff %(h'!) %H -- %p") +nnoremap (FlogVDiffSplitLastCommitPathsRight) :exec flog#Format("vertical belowright Floggit -s -t diff %(h'!) %H -- %p") -if !hasmapto('(FlogCloseTmpWin)') - nmap dq (FlogCloseTmpWin) +if !hasmapto('(FlogVSplitStaged)') + nmap gs (FlogVSplitStaged) +endif +if !hasmapto('(FlogVSplitUntracked)') + nmap gu (FlogVSplitUntracked) +endif +if !hasmapto('(FlogVSplitUnstaged)') + nmap gU (FlogVSplitUnstaged) endif -nnoremap (FlogCloseTmpWin) :call flog#close_tmp_win() +nnoremap (FlogVSplitStaged) :vertical belowright Floggit -s -t diff --cached +nnoremap (FlogVSplitUntracked) :exec flog#Format('silent Git add -N . \| vertical belowright Floggit -s -t diff \| silent Git read-tree %t') +nnoremap (FlogVSplitUnstaged) :vertical belowright Floggit -s -t diff -if !hasmapto('(FlogYank)') - nmap y (FlogYank) - vmap y (FlogYank) -endif -nnoremap (FlogYank) :call flog#copy_commits() -vnoremap (FlogYank) :call flog#copy_commits(1) +" Navigation mappings -if !hasmapto('(FlogGit)') - nmap git (FlogGit) - vmap git (FlogGit) +if !hasmapto('(FlogJumpToCommitStart)') + nmap ^ (FlogJumpToCommitStart) + vmap ^ (FlogJumpToCommitStart) endif -nnoremap (FlogGit) :Floggit -vnoremap (FlogGit) :Floggit +nnoremap (FlogJumpToCommitStart) :call flog#floggraph#nav#JumpToCommitStart() +vnoremap (FlogJumpToCommitStart) :call flog#floggraph#nav#JumpToCommitStart() -if !hasmapto('(FlogQuit)') - nmap ZZ (FlogQuit) - nmap gq (FlogQuit) +if !hasmapto('(FlogNextCommit)') + nmap ) (FlogNextCommit) endif -nnoremap (FlogQuit) :call flog#quit() - -" }}} - -" Navigation mappings {{{ +if !hasmapto('(FlogPrevCommit)') + nmap ( (FlogPrevCommit) +endif +nnoremap (FlogNextCommit) :call flog#floggraph#nav#NextCommit(max([1, v:count])) +nnoremap (FlogPrevCommit) :call flog#floggraph#nav#PrevCommit(max([1, v:count])) if !hasmapto('(FlogVNextCommitRight)') nmap (FlogVNextCommitRight) - nmap ) (FlogVNextCommitRight) endif if !hasmapto('(FlogVPrevCommitRight)') nmap (FlogVPrevCommitRight) - nmap ( (FlogVPrevCommitRight) endif -nnoremap (FlogVNextCommitRight) :call flog#next_commit() \| vertical belowright Flogsplitcommit -nnoremap (FlogVPrevCommitRight) :call flog#previous_commit() \| vertical belowright Flogsplitcommit +nnoremap (FlogVNextCommitRight) :call flog#floggraph#nav#NextCommit(max([1, v:count])) \| vertical belowright Flogsplitcommit +nnoremap (FlogVPrevCommitRight) :call flog#floggraph#nav#PrevCommit(max([1, v:count])) \| vertical belowright Flogsplitcommit if !hasmapto('(FlogVNextRefRight)') nmap ]r (FlogVNextRefRight) @@ -108,67 +150,65 @@ endif if !hasmapto('(FlogVPrevRefRight)') nmap [r (FlogVPrevRefRight) endif -nnoremap (FlogVNextRefRight) :call flog#next_ref() \| vertical belowright Flogsplitcommit -nnoremap (FlogVPrevRefRight) :call flog#previous_ref() \| vertical belowright Flogsplitcommit +nnoremap (FlogVNextRefRight) :call flog#floggraph#nav#NextRefCommit(max([1, v:count])) +nnoremap (FlogVPrevRefRight) :call flog#floggraph#nav#PrevRefCommit(max([1, v:count])) if !hasmapto('(FlogSkipAhead)') nmap ]] (FlogSkipAhead) endif -nnoremap (FlogSkipAhead) :call flog#change_skip_by_max_count(1 * max([v:count, 1])) +nnoremap (FlogSkipAhead) :call flog#floggraph#nav#SkipAhead(max([v:count, 1])) if !hasmapto('(FlogSkipBack)') nmap [[ (FlogSkipBack) endif -nnoremap (FlogSkipBack) :call flog#change_skip_by_max_count(-1 * max([v:count, 1])) +nnoremap (FlogSkipBack) :call flog#floggraph#nav#SkipBack(max([v:count, 1])) if !hasmapto('(FlogSetSkip)') - nmap go (FlogSetSkip) + nmap gcg (FlogSetSkip) +endif +nnoremap (FlogSetSkip) :call flog#floggraph#nav#SkipTo(v:count) + +if !hasmapto('(FlogSetRev)') + nmap gct (FlogSetRev) endif -nnoremap (FlogSetSkip) :call flog#set_skip_option(v:count) +nnoremap (FlogSetRev) :call flog#floggraph#nav#SetRevToCommitAtLine('.') -" }}} +if !hasmapto('(FlogClearRev)') + nmap gcc (FlogClearRev) +endif +nnoremap (FlogClearRev) :call flog#floggraph#nav#ClearRev() -" Argument modifier mappings {{{ +" Argument modifier mappings if !hasmapto('(FlogToggleAll)') nmap a (FlogToggleAll) endif -nnoremap (FlogToggleAll) :call flog#toggle_all_refs_option() +nnoremap (FlogToggleAll) :call flog#floggraph#opts#ToggleAll() if !hasmapto('(FlogToggleBisect)') nmap gb (FlogToggleBisect) endif -nnoremap (FlogToggleBisect) :call flog#toggle_bisect_option() +nnoremap (FlogToggleBisect) :call flog#floggraph#opts#ToggleBisect() -if !hasmapto('(FlogToggleNoMerges)') - nmap gm (FlogToggleNoMerges) +if !hasmapto('(FlogToggleMerges)') + nmap gm (FlogToggleMerges) endif -nnoremap (FlogToggleNoMerges) :call flog#toggle_no_merges_option() +nnoremap (FlogToggleMerges) :call flog#floggraph#opts#ToggleMerges() if !hasmapto('(FlogToggleReflog)') nmap gr (FlogToggleReflog) endif -nnoremap (FlogToggleReflog) :call flog#toggle_reflog_option() +nnoremap (FlogToggleReflog) :call flog#floggraph#opts#ToggleReflog() -if !hasmapto('(FlogToggleReverse)') - nmap gsr (FlogToggleReverse) -endif -nnoremap (FlogToggleReverse) :call flog#toggle_reverse_option() - -if !hasmapto('(FlogToggleNoGraph)') - nmap gx (FlogToggleNoGraph) +if !hasmapto('(FlogToggleGraph)') + nmap gx (FlogToggleGraph) endif -nnoremap (FlogToggleNoGraph) :call flog#toggle_no_graph_option() +nnoremap (FlogToggleGraph) :call flog#floggraph#opts#ToggleGraph() -if !hasmapto('(FlogToggleNoPatch)') - nmap gp (FlogToggleNoPatch) +if !hasmapto('(FlogTogglePatch)') + nmap gp (FlogTogglePatch) endif -nnoremap (FlogToggleNoPatch) :call flog#toggle_no_patch_option() - -if !hasmapto('(FlogUpdate)') - nmap u (FlogUpdate) -endif -nnoremap (FlogUpdate) :call flog#populate_graph_buffer() +nnoremap (FlogTogglePatch) :call flog#floggraph#opts#TogglePatch() if !hasmapto('(FlogSearch)') nmap g/ (FlogSearch) @@ -180,29 +220,32 @@ if !hasmapto('(FlogPatchSearch)') endif nnoremap (FlogPatchSearch) :Flogsetargs -patch-search= -if !hasmapto('(FlogCycleSort)') - nmap gss (FlogCycleSort) +if !hasmapto('(FlogCycleOrder)') + nmap goo (FlogCycleOrder) endif -nnoremap (FlogCycleSort) :call flog#cycle_sort_option() +nnoremap (FlogCycleOrder) :call flog#floggraph#opts#CycleOrder() -if !hasmapto('(FlogSortDate)') - nmap gsd (FlogSortDate) +if !hasmapto('(FlogOrderDate)') + nmap god (FlogOrderDate) endif -nnoremap (FlogSortDate) :call flog#set_sort_option('date') +nnoremap (FlogOrderDate) :Flogsetargs -order=date -if !hasmapto('(FlogSortAuthor)') - nmap gsa (FlogSortAuthor) +if !hasmapto('(FlogOrderAuthor)') + nmap goa (FlogOrderAuthor) endif -nnoremap (FlogSortAuthor) :call flog#set_sort_option('author') +nnoremap (FlogOrderAuthor) :Flogsetargs -order=author -if !hasmapto('(FlogSortTopo)') - nmap gst (FlogSortTopo) +if !hasmapto('(FlogOrderTopo)') + nmap got (FlogOrderTopo) endif -nnoremap (FlogSortTopo) :call flog#set_sort_option('topo') +nnoremap (FlogOrderTopo) :Flogsetargs -order=topo -" }}} +if !hasmapto('(FlogToggleReverse)') + nmap gor (FlogToggleReverse) +endif +nnoremap (FlogToggleReverse) :call flog#floggraph#opts#ToggleReverse() -" Commit/branch mappings {{{ +" Commit/branch mappings if !hasmapto('(FlogFixup)') nmap cf (FlogFixup) @@ -210,8 +253,8 @@ endif if !hasmapto('(FlogFixupRebase)') nmap cF (FlogFixupRebase) endif -nnoremap (FlogFixup) :call flog#run_command('Git commit --fixup=%H', 1, 1) -nnoremap (FlogFixupRebase) :call flog#run_command('Git commit --fixup=%H \| Git -c sequence.editor=true rebase --interactive --autosquash %H^', 1, 1) +nnoremap (FlogFixup) :exec flog#Format('Floggit -f commit --fixup=%H') +nnoremap (FlogFixupRebase) :exec flog#Format('Git commit --fixup=%H \| Floggit -f -c sequence.editor=true rebase --interactive --autosquash %H^') if !hasmapto('(FlogSquash)') nmap cs (FlogSquash) @@ -222,9 +265,9 @@ endif if !hasmapto('(FlogSquashEdit)') nmap cA (FlogSquashEdit) endif -nnoremap (FlogSquash) :call flog#run_command('Git commit --no-edit --squash=%H', 1, 1) -nnoremap (FlogSquashRebase) :call flog#run_command('Git commit --no-edit --squash=%H \| Git -c sequence.editor=true rebase --interactive --autosquash %H^', 1, 1) -nnoremap (FlogSquashEdit) :call flog#run_command('Git commit --edit --squash=%H', 1, 1) +nnoremap (FlogSquash) :exec flog#Format('Floggit -f commit --no-edit --squash=%H') +nnoremap (FlogSquashRebase) :exec flog#Format('Git commit --no-edit --squash=%H \| Floggit -f -c sequence.editor=true rebase --interactive --autosquash %H^') +nnoremap (FlogSquashEdit) :exec flog#Format('Floggit -f commit --edit --squash=%H') if !hasmapto('(FlogRevert)') nmap crc (FlogRevert) @@ -236,33 +279,37 @@ if !hasmapto('(FlogRevertNoEdit)') vmap crn (FlogRevertNoEdit) endif -nnoremap (FlogRevert) :call flog#run_command('Git revert %H', 1, 1) -vnoremap (FlogRevert) :call flog#run_command("Git revert %(h'<)^..%(h'>)", 1, 1) +nnoremap (FlogRevert) :exec flog#Format('Floggit -f revert %H') +vnoremap (FlogRevert) :exec flog#Format("Floggit -f revert %(h'<)^..%(h'>)") -nnoremap (FlogRevertNoEdit) :call flog#run_command('Git revert --no-edit %H', 1, 1) -vnoremap (FlogRevertNoEdit) :call flog#run_command("Git revert --no-edit %(h'<)^..%(h'>)", 1, 1) +nnoremap (FlogRevertNoEdit) :exec flog#Format('Floggit -f revert --no-edit %H') +vnoremap (FlogRevertNoEdit) :exec flog#Format("Floggit -f revert --no-edit %(h'<)^..%(h'>)") if !hasmapto('(FlogCheckout)') nmap coo (FlogCheckout) endif -nnoremap (FlogCheckout) :call flog#run_command('Git checkout %H', 0, 1) +nnoremap (FlogCheckout) :exec flog#Format('Floggit checkout %H') if !hasmapto('(FlogCheckoutBranch)') nmap cob (FlogCheckoutBranch) endif -nnoremap (FlogCheckoutBranch) :call flog#run_command('Git checkout %b', 0, 1) +nnoremap (FlogCheckoutBranch) :exec flog#Format('Floggit checkout %b') if !hasmapto('(FlogCheckoutLocalBranch)') - nmap cot (FlogCheckoutLocalBranch) + nmap col (FlogCheckoutLocalBranch) + + if !hasmapto('cot') + nnoremap cot :call flog#deprecate#DefaultMapping('cot', 'col') + endif endif -nnoremap (FlogCheckoutLocalBranch) :call flog#run_command('Git checkout %l', 0, 1) +nnoremap (FlogCheckoutLocalBranch) :exec flog#Format('Floggit checkout %l') if !hasmapto('(FlogGitCommit)') nmap c (FlogGitCommit) vmap c (FlogGitCommit) endif -nnoremap (FlogGitCommit) :Floggit commti -vnoremap (FlogGitCommit) :Floggit commti +nnoremap (FlogGitCommit) :Floggit commit +vnoremap (FlogGitCommit) :Floggit commit if !hasmapto('(FlogGitRevert)') nmap cr (FlogGitRevert) @@ -292,64 +339,78 @@ endif nnoremap (FlogGitBranch) :Floggit branch vnoremap (FlogGitBranch) :Floggit branch -" }}} +" Mark mappings + +if !hasmapto('(FlogSetCommitMark)') + nmap m (FlogSetCommitMark) + vmap m (FlogSetCommitMark) +endif +nnoremap (FlogSetCommitMark) :call flog#floggraph#mark#Set(nr2char(getchar()), '.') +vnoremap (FlogSetCommitMark) :call flog#floggraph#mark#Set(nr2char(getchar()), '.') -" Rebase mappings {{{ +if !hasmapto('(FlogJumpToCommitMark)') + nmap ' (FlogJumpToCommitMark) + vmap ' (FlogJumpToCommitMark) +endif +nnoremap (FlogJumpToCommitMark) :call flog#floggraph#nav#JumpToMark(nr2char(getchar())) +vnoremap (FlogJumpToCommitMark) :call flog#floggraph#nav#JumpToMark(nr2char(getchar())) + +" Rebase mappings if !hasmapto('(FlogRebaseInteractive)') nmap ri (FlogRebaseInteractive) endif -nnoremap (FlogRebaseInteractive) :call flog#run_command('Git rebase --interactive %H^', 1, 1) +nnoremap (FlogRebaseInteractive) :exec flog#Format('Floggit -f rebase --interactive %H^') if !hasmapto('(FlogRebaseInteractiveAutosquash)') nmap rf (FlogRebaseInteractiveAutosquash) endif -nnoremap (FlogRebaseInteractiveAutosquash) :call flog#run_command('Git -c sequence.editor=true rebase --interactive --autosquash %H^', 1, 1) +nnoremap (FlogRebaseInteractiveAutosquash) :exec flog#Format('Floggit -f -c sequence.editor=true rebase --interactive --autosquash %H^') if !hasmapto('(FlogRebaseInteractiveUpstream)') nmap ru (FlogRebaseInteractiveUpstream) endif -nnoremap (FlogRebaseInteractiveUpstream) :call flog#run_command('Git rebase --interactive @{upstream}', 1, 1) +nnoremap (FlogRebaseInteractiveUpstream) :Floggit -f rebase --interactive @{upstream} if !hasmapto('(FlogRebaseInteractivePush)') nmap rp (FlogRebaseInteractivePush) endif -nnoremap (FlogRebaseInteractivePush) :call flog#run_command('Git rebase --interactive @{push}', 1, 1) +nnoremap (FlogRebaseInteractivePush) :Floggit -f rebase --interactive @{push} if !hasmapto('(FlogRebaseContinue)') nmap rr (FlogRebaseContinue) endif -nnoremap (FlogRebaseContinue) :call flog#run_command('Git rebase --continue', 1, 1) +nnoremap (FlogRebaseContinue) :Floggit -f rebase --continue if !hasmapto('(FlogRebaseSkip)') nmap rs (FlogRebaseSkip) endif -nnoremap (FlogRebaseSkip) :call flog#run_command('Git rebase --skip', 1, 1) +nnoremap (FlogRebaseSkip) :Floggit -f rebase --skip if !hasmapto('(FlogRebaseAbort)') nmap ra (FlogRebaseAbort) endif -nnoremap (FlogRebaseAbort) :call flog#run_command('Git rebase --abort', 1, 1) +nnoremap (FlogRebaseAbort) :Floggit -f rebase --abort if !hasmapto('(FlogRebaseEditTodo)') nmap re (FlogRebaseEditTodo) endif -nnoremap (FlogRebaseEditTodo) :call flog#run_command('Git rebase --edit-todo', 1, 1) +nnoremap (FlogRebaseEditTodo) :Floggit -f rebase --edit-todo if !hasmapto('(FlogRebaseInteractiveReword)') nmap rw (FlogRebaseInteractiveReword) endif -nnoremap (FlogRebaseInteractiveReword) :call flog#run_command('Git rebase --interactive %H^ \| s/^pick/reword/e', 1, 1) +nnoremap (FlogRebaseInteractiveReword) :exec flog#Format('Floggit -f rebase --interactive %H^ \| s/^pick/reword/e') if !hasmapto('(FlogRebaseInteractiveEdit)') nmap rm (FlogRebaseInteractiveEdit) endif -nnoremap (FlogRebaseInteractiveEdit) :call flog#run_command('Git rebase --interactive %H^ \| s/^pick/edit/e', 1, 1) +nnoremap (FlogRebaseInteractiveEdit) :exec flog#Format('Floggit -f rebase --interactive %H^ \| s/^pick/edit/e') if !hasmapto('(FlogRebaseInteractiveDrop)') nmap rd (FlogRebaseInteractiveDrop) endif -nnoremap (FlogRebaseInteractiveDrop) :call flog#run_command('Git rebase --interactive %H^ \| s/^pick/drop/e', 1, 1) +nnoremap (FlogRebaseInteractiveDrop) :exec flog#Format('Floggit -f rebase --interactive %H^ \| s/^pick/drop/e') if !hasmapto('(FlogGitRebase)') nmap r (FlogGitRebase) @@ -357,88 +418,3 @@ if !hasmapto('(FlogGitRebase)') endif nnoremap (FlogGitRebase) :Floggit rebase vnoremap (FlogGitRebase) :Floggit rebase - -" }}} - -" Mark mappings {{{ - -if !hasmapto('(FlogSetCommitMark)') - nmap m (FlogSetCommitMark) - vmap m (FlogSetCommitMark) -endif -nnoremap (FlogSetCommitMark) :call flog#set_commit_mark_at_line(nr2char(getchar()), '.') -vnoremap (FlogSetCommitMark) :call flog#set_commit_mark_at_line(nr2char(getchar()), '.') - -if !hasmapto('(FlogJumpToCommitMark)') - nmap ' (FlogJumpToCommitMark) - vmap ' (FlogJumpToCommitMark) -endif -nnoremap (FlogJumpToCommitMark) :call flog#jump_to_commit_mark(nr2char(getchar())) -vnoremap (FlogJumpToCommitMark) :call flog#jump_to_commit_mark(nr2char(getchar())) - -" }}} - -" Deprecated plugin mappings {{{ - -call flog#deprecate_plugin_mapping('Flogvsplitcommitright', '(FlogVSplitCommitRight)', 'nmap') -call flog#deprecate_plugin_mapping('FlogVsplitcommitright', '(FlogVSplitCommitRight)', 'nmap') -call flog#deprecate_plugin_mapping('FlogVnextcommitright', '(FlogVNextCommitRight)', 'nmap') -call flog#deprecate_plugin_mapping('Flogvnextcommitright', '(FlogVNextCommitRight)', 'nmap') -call flog#deprecate_plugin_mapping('FlogVprevcommitright', '(FlogVPrevCommitRight)', 'nmap') -call flog#deprecate_plugin_mapping('Flogvprevcommitright', '(FlogVPrevCommitRight)', 'nmap') -call flog#deprecate_plugin_mapping('FlogVnextrefright', '(FlogVNextRefRight)', 'nmap') -call flog#deprecate_plugin_mapping('FlogVprevrefright', '(FlogVPrevRefRight)', 'nmap') - -call flog#deprecate_plugin_mapping('FlogToggleall', '(FlogToggleAll)', 'nmap') -call flog#deprecate_plugin_mapping('Flogtoggleall', '(FlogToggleAll)', 'nmap') -call flog#deprecate_plugin_mapping('FlogTogglebisect', '(FlogToggleBisect)', 'nmap') -call flog#deprecate_plugin_mapping('Flogtogglebisect', '(FlogToggleBisect)', 'nmap') -call flog#deprecate_plugin_mapping('FlogTogglenomerges', '(FlogToggleNoMerges)', 'nmap') -call flog#deprecate_plugin_mapping('Flogtogglenomerges', '(FlogToggleNoMerges)', 'nmap') -call flog#deprecate_plugin_mapping('FlogTogglereflog', '(FlogToggleReflog)', 'nmap') - -call flog#deprecate_plugin_mapping('FlogUpdate', '(FlogUpdate)', 'nmap') -call flog#deprecate_plugin_mapping('Flogupdate', '(FlogUpdate)', 'nmap') - -call flog#deprecate_plugin_mapping('FlogGit', '(FlogGit)') -call flog#deprecate_plugin_mapping('Floggit', '(FlogGit)') - -call flog#deprecate_plugin_mapping('FlogYank', '(FlogYank)') -call flog#deprecate_plugin_mapping('Flogyank', '(FlogYank)') - -call flog#deprecate_plugin_mapping('FlogSearch', '(FlogSearch)', 'nmap') -call flog#deprecate_plugin_mapping('FlogPatchSearch', '(FlogPatchSearch)', 'nmap') - -call flog#deprecate_plugin_mapping('FlogQuit', '(FlogQuit)', 'nmap') -call flog#deprecate_plugin_mapping('Flogquit', '(FlogQuit)', 'nmap') - -call flog#deprecate_plugin_mapping('FlogHelp', '(FlogHelp)', 'nmap') -call flog#deprecate_plugin_mapping('Floghelp', '(FlogHelp)', 'nmap') - -call flog#deprecate_plugin_mapping('FlogSetskip', '(FlogSetSkip)', 'nmap') -call flog#deprecate_plugin_mapping('FlogSkipahead', '(FlogSkipAhead)', 'nmap') -call flog#deprecate_plugin_mapping('FlogSkipback', '(FlogSkipBack)', 'nmap') - -" }}} - -" }}} - -" Commands {{{ - -command! -buffer Flogsplitcommit call flog#run_tmp_command(' Gsplit %h') - -command! -buffer -bang -complete=customlist,flog#complete -nargs=* Flogsetargs call flog#update_options([], '' ==# '!') - -command! -buffer -bang -complete=customlist,flog#complete_jump -nargs=* Flogjump call flog#jump_to_ref() - -command! -buffer Flogmarks call flog#echo_commit_marks() - -" Deprecated commands {{{ - -command! -buffer -nargs=* Flogupdate call flog#deprecate_command('Flogupdate', 'Flogsetargs') - -" }}} - -" }}} - -" vim: set et sw=2 ts=2 fdm=marker: diff --git a/img/git-forest.png b/img/git-forest.png deleted file mode 100644 index 28b476f..0000000 Binary files a/img/git-forest.png and /dev/null differ diff --git a/img/screen-graph.png b/img/screen-graph.png index bc1e691..1b11fa5 100644 Binary files a/img/screen-graph.png and b/img/screen-graph.png differ diff --git a/lua/flog/graph.lua b/lua/flog/graph.lua new file mode 100644 index 0000000..6b43aba --- /dev/null +++ b/lua/flog/graph.lua @@ -0,0 +1,740 @@ +-- This script parses git log output and produces the commit graph +-- It is optimized for speed, not brevity or readability + +-- Init error strings +local graph_error = 'flog: internal error drawing graph' + +-- Init graph strings +local current_commit_str = '• ' +local commit_branch_str = '│ ' +local commit_empty_str = ' ' +local complex_merge_str_1 = '┬┊' +local complex_merge_str_2 = '╰┤' +local merge_all_str = '┼' +local merge_jump_str = '┊' +local merge_up_down_left_str = '┤' +local merge_up_down_right_str = '├' +local merge_up_down_str = '│' +local merge_up_left_right_str = '┴' +local merge_up_left_str = '╯' +local merge_up_right_str = '╰' +local merge_up_str = ' ' +local merge_down_left_right_str = '┬' +local merge_down_left_str = '╮' +local merge_down_right_str = '╭' +local merge_left_right_str = '─' +local merge_empty_str = ' ' +local missing_parent_str = '┊ ' +local missing_parent_branch_str = '│ ' +local missing_parent_empty_str = ' ' + +local function flog_get_graph( + enable_vim, + enable_nvim, + enable_porcelain, + start_token, + enable_graph, + cmd) + -- Resolve Vim values + enable_graph = enable_graph and enable_graph ~= 0 + + -- Init commit parsing data + local commits = {} + local commit_hashes = {} + local ncommits = 0 + + -- Init Vim output + local vim_out + local vim_out_index = 1 + local vim_commits + local vim_commits_by_hash + local vim_line_commits + + if enable_vim then + vim_out = vim.list() + vim_commits = vim.list() + vim_commits_by_hash = vim.dict() + vim_line_commits = vim.list() + else + vim_out = {} + vim_commits = {} + vim_commits_by_hash = {} + vim_line_commits = {} + end + + -- Run command + local handle = io.popen(cmd) + + -- Skip first line (start token) + handle:read() + + -- Read hashes until EOF + for hash in handle:lines() do + -- Update commit count + ncommits = ncommits + 1 + + -- Save hash + commit_hashes[hash] = 1 + + -- Read and split parents + local parents = {} + local parent_hashes = {} + local nparents = 0 + for parent in handle:read():gmatch('%S+') do + if not parent_hashes[parent] then + nparents = nparents + 1 + parents[nparents] = parent + parent_hashes[parent] = 1 + end + end + + -- Read refs + local refs = handle:read() + + -- Read output until EOF or start token + local out = {} + local nlines = 0 + for line in handle:lines() do + nlines = nlines + 1 + if line == start_token then + break + end + out[nlines] = line + end + + -- Save commit + commits[ncommits] = { + hash = hash, + parents = parents, + parent_hashes = parent_hashes, + refs = refs, + out = out, + } + end + + -- Output number of commits + if not enable_vim and not enable_nvim then + print(ncommits) + end + + -- Init graph data + local branch_hashes = {} + local branch_indexes = {} + local nbranches = 0 + + -- Draw graph + + for commit_index, commit in ipairs(commits) do + -- Get commit data + + local commit_hash = commit.hash + local parents = commit.parents + local parent_hashes = commit.parent_hashes + local nparents = #parents + local commit_out = commit.out + local ncommit_lines = #commit.out + + -- Init commit output + + -- The prefix that goes before the first commit line + local commit_prefix = {} + -- The prefix that goes after multiline commits + local commit_multiline_prefix = {} + -- The number of strings in commit lines + local ncommit_strings = 0 + -- The merge line that goes after the commit + local merge_line = {} + -- The complex merge line that goes after the merge + local complex_merge_line = {} + -- The number of strings in merge lines + local nmerge_strings = 0 + -- The two lines indicating missing parents after the complex line + local missing_parents_line_1 = {} + local missing_parents_line_2 = {} + -- The number of strings in missing parent lines + local nmissing_parents_strings = 0 + + -- Init visual data + + -- The number of columns in the commit output + local ncommit_cols = 0 + -- The visual column which the commit branch is on + local commit_branch_col = 0 + -- The parents in the order they appear in the graph + local visual_parents + -- The number of visual parents + local nvisual_parents = 0 + -- The number of complex merges (octopus) + local ncomplex_merges = 0 + -- The number of missing parents + local nmissing_parents = 0 + + if enable_vim then + visual_parents = vim.list() + else + visual_parents = {} + end + + -- Init graph data + + -- The number of passed merges + local nmerges_left = 0 + -- The number of upcoming merges (parents + commit) + local nmerges_right = nparents + 1 + -- The index of the commit branch + local commit_branch_index = branch_indexes[commit_hash] + -- The index of the moved parent branch (there is only one) + local moved_parent_branch_index = nil + -- The number of branches on the commit line + local ncommit_branches = nbranches + (commit_branch_index and 0 or 1) + + -- Init indexes + + -- The current branch + local branch_index = 1 + -- The current parent + local parent_index = 1 + + -- Find the first empty parent + while parent_index <= nparents and branch_indexes[parents[parent_index]] do + parent_index = parent_index + 1 + end + + -- Traverse old and new branches + + if enable_graph then + while branch_index <= nbranches or nmerges_right > 0 do + -- Get branch data + + local branch_hash = branch_hashes[branch_index] + local is_commit = branch_index == commit_branch_index + + -- Set merge info before updates + + local merge_up = branch_hash or moved_parent_branch_index == branch_index + local merge_left = nmerges_left > 0 and nmerges_right > 0 + local is_complex = false + local is_missing_parent = false + + -- Handle commit + + if not branch_hash and not commit_branch_index then + -- Found empty branch and commit does not have a branch + -- Add the commit in the empty spot + + commit_branch_index = branch_index + is_commit = true + end + + if is_commit then + -- Count commit merge + nmerges_right = nmerges_right - 1 + nmerges_left = nmerges_left + 1 + + -- Record commit col + commit_branch_col = ncommit_cols + 1 + + if branch_hash then + -- End of branch + + -- Remove branch + branch_hashes[commit_branch_index] = nil + branch_indexes[commit_hash] = nil + + -- Trim trailing empty branches + while nbranches > 0 and not branch_hashes[nbranches] do + nbranches = nbranches - 1 + end + + -- Clear branch hash + branch_hash = nil + end + + if parent_index > nparents and nmerges_right == 1 then + -- There is only one remaining parent, to the right + -- Move it under the commit + + -- Find parent to right + parent_index = nparents + while (branch_indexes[parents[parent_index]] or -1) < branch_index do + parent_index = parent_index - 1 + end + + -- Get parent data + local parent_hash = parents[parent_index] + local parent_branch_index = branch_indexes[parent_hash] + + -- Remove old parent branch + branch_hashes[parent_branch_index] = nil + branch_indexes[parent_hash] = nil + + -- Trim trailing empty branches + while nbranches > 0 and not branch_hashes[nbranches] do + nbranches = nbranches - 1 + end + + -- Record the old index + moved_parent_branch_index = parent_branch_index + + -- Count upcoming moved parent as another merge + nmerges_right = nmerges_right + 1 + end + end + + -- Handle parents + + if not branch_hash and parent_index <= nparents then + -- New parent + + -- Get parent data + local parent_hash = parents[parent_index] + + -- Set branch to parent + branch_indexes[parent_hash] = branch_index + branch_hashes[branch_index] = parent_hash + + -- Update branch has + branch_hash = parent_hash + + -- Update the number of branches + if branch_index > nbranches then + nbranches = branch_index + end + + -- Jump to next available parent + parent_index = parent_index + 1 + while parent_index <= nparents and branch_indexes[parents[parent_index]] do + parent_index = parent_index + 1 + end + + -- Count new parent merge + nmerges_right = nmerges_right - 1 + nmerges_left = nmerges_left + 1 + + -- Determine if parent is missing + if branch_hash and not commit_hashes[parent_hash] then + is_missing_parent = true + nmissing_parents = nmissing_parents + 1 + else + -- Record the visual parent if it is not missing + nvisual_parents = nvisual_parents + 1 + visual_parents[nvisual_parents] = parent_hash + end + elseif branch_index == moved_parent_branch_index or (nmerges_right > 0 and parent_hashes[branch_hash]) then + -- Existing parents + + -- Count existing parent merge + nmerges_right = nmerges_right - 1 + nmerges_left = nmerges_left + 1 + + -- Determine if parent has a complex merge + is_complex = merge_left and nmerges_right > 0 + if is_complex then + ncomplex_merges = ncomplex_merges + 1 + end + + -- Determine if parent is missing + if branch_hash and not commit_hashes[branch_hash] then + is_missing_parent = true + nmissing_parents = nmissing_parents + 1 + end + end + + -- Draw commit lines + + if branch_index <= ncommit_branches then + -- Update commit visual info + + ncommit_cols = ncommit_cols + 2 + ncommit_strings = ncommit_strings + 1 + + if is_commit then + -- Draw current commit + + commit_prefix[ncommit_strings] = current_commit_str + + if ncommit_lines > 1 then + if nparents > 0 then + -- Draw branch on multiline commit + commit_multiline_prefix[ncommit_strings] = commit_branch_str + else + -- Draw empty branch on multiline commit + commit_multiline_prefix[ncommit_strings] = commit_empty_str + end + end + elseif merge_up then + -- Draw unrelated branch + + commit_prefix[ncommit_strings] = commit_branch_str + if ncommit_lines > 1 then + commit_multiline_prefix[ncommit_strings] = commit_branch_str + end + else + -- Draw empty branch + + commit_prefix[ncommit_strings] = commit_empty_str + if ncommit_lines > 1 then + commit_multiline_prefix[ncommit_strings] = commit_empty_str + end + end + end + + -- Update merge visual info + + nmerge_strings = nmerge_strings + 1 + + -- Draw merge lines + + if is_complex then + -- Draw merge lines for complex merge + + merge_line[nmerge_strings] = complex_merge_str_1 + complex_merge_line[nmerge_strings] = complex_merge_str_2 + else + -- Draw non-complex merge lines + + -- Update merge info after drawing commit + + merge_up = merge_up or is_commit or branch_index == moved_parent_branch_index + local merge_right = nmerges_left > 0 and nmerges_right > 0 + + -- Draw left character + + if branch_index > 1 then + if merge_left then + -- Draw left merge line + merge_line[nmerge_strings] = merge_left_right_str + else + -- No merge to left + -- Draw empty space + merge_line[nmerge_strings] = merge_empty_str + end + -- Complex merge line always has empty space here + complex_merge_line[nmerge_strings] = merge_empty_str + + -- Update visual merge info + + nmerge_strings = nmerge_strings + 1 + end + + -- Draw right character + + if merge_up then + if branch_hash then + if merge_left then + if merge_right then + if is_commit then + -- Merge up, down, left, right + merge_line[nmerge_strings] = merge_all_str + else + -- Jump over + merge_line[nmerge_strings] = merge_jump_str + end + else + -- Merge up, down, left + merge_line[nmerge_strings] = merge_up_down_left_str + end + else + if merge_right then + -- Merge up, down, right + merge_line[nmerge_strings] = merge_up_down_right_str + else + -- Merge up, down + merge_line[nmerge_strings] = merge_up_down_str + end + end + else + if merge_left then + if merge_right then + -- Merge up, left, right + merge_line[nmerge_strings] = merge_up_left_right_str + else + -- Merge up, left + merge_line[nmerge_strings] = merge_up_left_str + end + else + if merge_right then + -- Merge up, right + merge_line[nmerge_strings] = merge_up_right_str + else + -- Merge up + merge_line[nmerge_strings] = merge_up_str + end + end + end + else + if branch_hash then + if merge_left then + if merge_right then + -- Merge down, left, right + merge_line[nmerge_strings] = merge_down_left_right_str + else + -- Merge down, left + merge_line[nmerge_strings] = merge_down_left_str + end + else + if merge_right then + -- Merge down, right + merge_line[nmerge_strings] = merge_down_right_str + else + -- Merge down + -- Not possible to merge down only + error(graph_error) + end + end + else + if merge_left then + if merge_right then + -- Merge left, right + merge_line[nmerge_strings] = merge_left_right_str + else + -- Merge left + -- Not possible to merge left only + error(graph_error) + end + else + if merge_right then + -- Merge right + -- Not possible to merge right only + error(graph_error) + else + -- No merges + merge_line[nmerge_strings] = merge_empty_str + end + end + end + end + + -- Draw complex right char + + if branch_hash then + complex_merge_line[nmerge_strings] = merge_up_down_str + else + complex_merge_line[nmerge_strings] = merge_empty_str + end + end + + -- Update visual missing parents info + + nmissing_parents_strings = nmissing_parents_strings + 1 + + -- Draw missing parents lines + + if is_missing_parent then + missing_parents_line_1[nmissing_parents_strings] = missing_parent_str + missing_parents_line_2[nmissing_parents_strings] = missing_parent_empty_str + elseif branch_hash then + missing_parents_line_1[nmissing_parents_strings] = missing_parent_branch_str + missing_parents_line_2[nmissing_parents_strings] = missing_parent_branch_str + else + missing_parents_line_1[nmissing_parents_strings] = missing_parent_empty_str + missing_parents_line_2[nmissing_parents_strings] = missing_parent_empty_str + end + + -- Remove missing parent + + if is_missing_parent and branch_index ~= moved_parent_branch_index then + -- Remove branch + branch_hashes[branch_index] = nil + branch_indexes[branch_hash] = nil + + -- Trim trailing empty branches + while nbranches > 0 and not branch_hashes[nbranches] do + nbranches = nbranches - 1 + end + end + + -- Increment + + branch_index = branch_index + 1 + end + end + + -- Output + + -- Calculate format column + + local format_col = ncommit_cols + 1 + + -- Calculate whether certain lines should be outputted + + local should_out_merge = enable_graph and (nparents > 1 + or moved_parent_branch_index + or (nparents == 0 and nbranches == 0) + or (nparents == 1 and branch_indexes[parents[1]] ~= commit_branch_index)) + local should_out_complex = should_out_merge and ncomplex_merges > 0 + local should_out_missing_parents = nmissing_parents > 0 + + if enable_vim or enable_nvim then + -- Output using Vim + + -- Init Vim commit + local vim_commit + if enable_vim then + vim_commit = vim.dict() + else + vim_commit = {} + end + + -- Set commit details + vim_commit.hash = commit_hash + vim_commit.parents = visual_parents + vim_commit.refs = commit.refs + vim_commit.line = vim_out_index + vim_commit.col = commit_branch_col + vim_commit.format_col = format_col + + -- Add commit data + vim_commits[commit_index] = vim_commit + vim_commits_by_hash[commit_hash] = vim_commit + + -- Add commit out + + vim_line_commits[vim_out_index] = vim_commit + vim_out[vim_out_index] = table.concat(commit_prefix, '') .. commit_out[1] + vim_out_index = vim_out_index + 1 + + if ncommit_lines > 1 then + local prefix = table.concat(commit_multiline_prefix, '') + local commit_out_index = 2 + + while commit_out_index <= ncommit_lines do + vim_line_commits[vim_out_index] = vim_commit + vim_out[vim_out_index] = prefix .. commit_out[commit_out_index] + + commit_out_index = commit_out_index + 1 + vim_out_index = vim_out_index + 1 + end + end + + -- Add merge out + + if should_out_merge then + vim_line_commits[vim_out_index] = vim_commit + vim_out[vim_out_index] = table.concat(merge_line, '') + vim_out_index = vim_out_index + 1 + + if should_out_complex then + vim_line_commits[vim_out_index] = vim_commit + vim_out[vim_out_index] = table.concat(complex_merge_line, '') + vim_out_index = vim_out_index + 1 + end + end + + -- Add missing parents out + + if should_out_missing_parents then + vim_line_commits[vim_out_index] = vim_commit + vim_out[vim_out_index] = table.concat(missing_parents_line_1, '') + vim_out_index = vim_out_index + 1 + + vim_line_commits[vim_out_index] = vim_commit + vim_out[vim_out_index] = table.concat(missing_parents_line_2, '') + vim_out_index = vim_out_index + 1 + end + else + -- Output using stdout + + if enable_porcelain then + -- Calculate total lines out + + local total_lines = (ncommit_lines + + (should_out_merge and 1 or 0) + + (should_out_complex and 1 or 0) + + (should_out_missing_parents and 2 or 0)) + + -- Print commit hash + + print(commit_hash) + + -- Print commit visual parents + + print(nvisual_parents) + for _, parent in ipairs(visual_parents) do + print(parent) + end + + -- Print commit refs + + print(commit.refs) + + -- Print commit col + + print(commit_branch_col) + + -- Print commit format start + + print(ncommit_cols + 1) + + -- Print total lines out + + print(total_lines) + end + + -- Print commit out + + for _, str in ipairs(commit_prefix) do + io.write(str) + end + io.write(commit_out[1]) + io.write('\n') + + local commit_line = 2 + while commit_line <= ncommit_lines do + for _, str in ipairs(commit_multiline_prefix) do + io.write(str) + end + io.write(commit_out[commit_line]) + io.write('\n') + commit_line = commit_line + 1 + end + + -- Print merge out + + if should_out_merge then + for _, str in ipairs(merge_line) do + io.write(str) + end + io.write('\n') + + if should_out_complex then + for _, str in ipairs(complex_merge_line) do + io.write(str) + end + io.write('\n') + end + end + + -- Print missing parents out + + if should_out_missing_parents then + for _, str in ipairs(missing_parents_line_1) do + io.write(str) + end + io.write('\n') + + for _, str in ipairs(missing_parents_line_2) do + io.write(str) + end + io.write('\n') + end + end + end + + if enable_vim or enable_nvim then + local dict_out = { + output = vim_out, + commits = vim_commits, + commits_by_hash = vim_commits_by_hash, + line_commits = vim_line_commits, + } + + if enable_vim then + return vim.dict(dict_out) + else + return dict_out + end + end +end + +_G.flog_get_graph = flog_get_graph diff --git a/lua/flog/graph_bin.lua b/lua/flog/graph_bin.lua new file mode 100644 index 0000000..474e26f --- /dev/null +++ b/lua/flog/graph_bin.lua @@ -0,0 +1,17 @@ +-- This file generates the commit graph as a script. + +require('flog/graph') + +flog_get_graph( + -- enable_vim + false, + -- enable_nvim + false, + -- enable_porcelain + true, + -- start_token + arg[1], + -- enable_graph + arg[2] == 'true', + -- cmd + arg[3]) diff --git a/plugin/flog.vim b/plugin/flog.vim index 96d2fe3..78259e2 100644 --- a/plugin/flog.vim +++ b/plugin/flog.vim @@ -1,134 +1,44 @@ -" Plugin boilerplate {{{ +" Errors -if exists('g:loaded_flog') - finish -endif - -let g:loaded_flog = 1 - -" }}} - -" Global state {{{ - -let g:flog_instance_counter = 0 - -" }}} +let g:flog_shell_error = 'flog: encountered shell error' +let g:flog_missing_state = 'flog: could not find state' +let g:flog_not_a_fugitive_buffer = 'flog: not a fugitive buffer' +let g:flog_not_a_flog_buffer = 'flog: not a flog buffer' +let g:flog_no_commits_found = 'flog: error parsing commits: no commits found' +let g:flog_unsupported_argument = 'flog: unrecognized argument' +let g:flog_unsupported_exec_format_item = 'flog: unrecognized exec format item' +let g:flog_graph_draw_error = 'flog: internal error drawing graph' +let g:flog_invalid_commit_mark = 'flog: invalid commit mark' +let g:flog_reverse_requires_no_graph = 'flog: -reverse requires -no-graph' +let g:flog_lua_not_found = 'flog: Lua not found' -" Open command arg data {{{ +" Settings -let g:flog_open_cmds = [ - \ 'edit', - \ 'split', - \ 'vsplit', - \ 'new', - \ 'vnew', - \ 'tabedit', - \ 'tabnew', - \ ] - -let g:flog_open_cmd_modifiers = [ - \ 'aboveleft', - \ 'belowright', - \ 'botright', - \ 'confirm', - \ 'leftabove', - \ 'rightbelow', - \ 'silent', - \ 'tab', - \ 'topleft', - \ 'verbose', - \ 'vertical', - \ ] +let g:flog_write_commit_graph = v:true -" }}} +let g:flog_write_commit_graph_args = '--reachable --progress' -" Log command build data {{{ +let g:flog_enable_status = v:false -" used to delineate format specifiers used to retrieve commit data -let g:flog_format_start = '__FSTART__' -let g:flog_data_start = '__DSTART__' +let g:flog_check_lua_version = v:true -" }}} +let g:flog_get_author_args = '--all --no-merges --max-count=100000' -" Sorting type data {{{ +let g:flog_commit_start_token = '__START' -let g:flog_sort_types = [ +let g:flog_order_types = [ \ { 'name': 'date', 'args': '--date-order' }, \ { 'name': 'author', 'args': '--author-date-order' }, \ { 'name': 'topo', 'args': '--topo-order' }, \ ] -" }}} +" Data -" Completion data {{{ +let g:flog_root_dir = fnamemodify(resolve(expand(':p')), ':h:h') -let g:flog_default_completion = [ - \ '-all ', - \ '-author=', - \ '-bisect ', - \ '-date=', - \ '-format=', - \ '-limit=', - \ '-max-count=', - \ '-no-graph', - \ '-no-merges', - \ '-no-patch', - \ '-open-cmd=', - \ '-patch-search=', - \ '-path=', - \ '-raw-args=', - \ '-reflog ', - \ '-rev=', - \ '-reverse', - \ '-search=', - \ '-skip=', - \ '-sort=', - \ ] - -let g:flog_date_formats = [ - \ 'iso8601', - \ 'short', - \ ] - -" Format specifier data {{{ - -function! s:LongSpecifierPattern() - let l:long_specifiers = [] - for l:specifier in g:flog_long_specifiers - for i in range(1, len(l:specifier) - 2) - let l:long_specifiers += [specifier[:i]] - endfor - endfor - return '\(' . join(l:long_specifiers, '\|') . '\)' -endfunction - -let g:flog_eat_specifier_pattern = '^\(%.\|[^%]\)*' -let g:flog_specifier_partial_char = '[acgGC(]' -let g:flog_specifier_hex_start = 'x[0-9]\?' -let g:flog_specifier_bracket_start = '\([Cw<>]\|<|\|>>\|><\)' -let g:flog_specifier_partial_bracket = '\(\([Cw<>]\|<|\|>>\|><\)(\|(trailers:\)[^\)]*' -let g:flog_long_specifiers = [ - \ 'Cred', - \ 'Cgreen', - \ 'Cblue', - \ 'Creset', - \ '(trailers:', - \ '(trailers)', - \ ] -let g:flog_specifier_long_pattern = s:LongSpecifierPattern() -let g:flog_completable_partials = [ - \ g:flog_specifier_partial_char, - \ g:flog_specifier_bracket_start, - \ g:flog_specifier_long_pattern, - \ ] -let g:flog_noncompletable_partials = [ - \ g:flog_specifier_hex_start, - \ g:flog_specifier_partial_bracket, - \ ] -let g:flog_completable_specifier_pattern = '\(' . join(g:flog_completable_partials, '\|') . '\)' -let g:flog_noncompletable_specifier_pattern = '\(' . join(g:flog_noncompletable_partials, '\|') . '\)' +let g:flog_lua_dir = g:flog_root_dir .. '/lua' -let g:flog_completion_specifiers = [ +let g:flog_format_specifiers = [ \ '%H', \ '%h', \ '%T', @@ -174,11 +84,6 @@ let g:flog_completion_specifiers = [ \ '%ge', \ '%gE', \ '%gs', - \ '%Cred', - \ '%Cgreen', - \ '%Cblue', - \ '%Creset', - \ '%C(', \ '%m', \ '%n', \ '%%', @@ -193,113 +98,43 @@ let g:flog_completion_specifiers = [ \ '%(trailers)', \ ] -" }}} - -" }}} - -" Errors {{{ - -let g:flog_shell_error = 'flog: encountered shell error' -let g:flog_missing_state = 'flog: could not find state' -let g:flog_not_a_fugitive_buffer = 'flog: not a fugitive buffer' -let g:flog_no_commits = 'flog: error parsing commits: no commits found' -let g:flog_missing_commit_start = 'flog: error parsing commits: could not find start of commit' -let g:flog_unsupported_argument = 'flog: unrecognized argument' -let g:flog_unsupported_command_format_item = 'flog: unrecognized command format item' -let g:flog_invalid_mark = 'flog: invalid mark' - -" }}} - -" Deprecation warnings {{{ - -let g:flog_shown_deprecation_warnings = {} - -" }}} - -" Git command data {{{ - -let g:flog_git_command_spec = { - \ 'bisect': { - \ 'subcommands': [ - \ 'start', - \ 'new', - \ 'bad', - \ 'old', - \ 'terms', - \ 'skip', - \ 'reset', - \ 'replay', - \ 'run', - \ 'help', - \ ], - \ }, - \ 'rebase': { - \ 'subcommands': [ - \ '--continue', - \ '--skip', - \ '--abort', - \ '--quit', - \ '--show-current-patch', - \ ], - \ 'options': [ - \ '-i', - \ '--interactive', - \ '--autosquash', - \ '--edit-todo', - \ '--exec', - \ ], - \ }, - \ 'merge': { - \ 'subcommands': [ - \ '--continue', - \ '--abort', - \ '--quit', - \ ], - \ 'options': [ - \ '--squash', - \ '--edit', - \ '--no-edit', - \ '--no-verify', - \ '-m', - \ '-F', - \ ], - \ }, - \ 'cherry-pick': { - \ 'subcommands': [ - \ '--continue', - \ '--skip', - \ '--abort', - \ '--quit', - \ ], - \ }, - \ 'push': { - \ 'options': [ - \ '--all', - \ '--mirror', - \ '--tags', - \ '--atomic', - \ '--no-atomic', - \ '--dry-run', - \ '--force', - \ '--delete', - \ '--prune', - \ '--verbose', - \ '--upstream', - \ '--no-verify', - \ ], - \ }, - \ } - -" }}} - -" Commands {{{ - -command! -range -bang -complete=customlist,flog#complete_git -nargs=* Floggit call flog#run_raw_command(' Git ' . , 1, 1, !empty('')) +let g:flog_date_formats = [ + \ 'human', + \ 'local', + \ 'relative', + \ 'short', + \ 'iso', + \ 'iso-strict', + \ 'rfc', + \ 'format:', + \ ] -command! -range=0 -complete=customlist,flog#complete -nargs=* Flog call flog#open(( ? ['-limit=,:' . expand('%:p')] : []) + []) +let g:flog_open_cmds = [ + \ 'edit', + \ 'split', + \ 'vsplit', + \ 'new', + \ 'vnew', + \ 'tabedit', + \ 'tabnew', + \ ] -command! -range=0 -complete=customlist,flog#complete -nargs=* Flogsplit call flog#open(( ? ['-limit=,:' . expand('%:p')] : []) + ['-open-cmd= split', ]) +let g:flog_open_cmd_modifiers = [ + \ 'aboveleft', + \ 'belowright', + \ 'botright', + \ 'confirm', + \ 'leftabove', + \ 'rightbelow', + \ 'silent', + \ 'tab', + \ 'topleft', + \ 'verbose', + \ 'vertical', + \ ] -" }}} +" Commands -" vim: set et sw=2 ts=2 fdm=marker: +command! -range=0 -complete=customlist,flog#cmd#flog#args#Complete -nargs=* Flog call flog#cmd#Flog(( > 1 ? ['-limit=,:' .. expand('%:p')] : []) + []) +command! -range=0 -complete=customlist,flog#cmd#flog#args#Complete -nargs=* Flogsplit call flog#cmd#Flog(( > 1 ? ['-limit=,:' .. expand('%:p')] : []) + ['-open-cmd= split', ]) +command! -range -bang -complete=customlist,flog#cmd#floggit#args#Complete -nargs=* Floggit call flog#cmd#Floggit('', '', '') diff --git a/syntax/floggraph.vim b/syntax/floggraph.vim index 30b65ae..32f69b2 100644 --- a/syntax/floggraph.vim +++ b/syntax/floggraph.vim @@ -4,28 +4,46 @@ endif let b:current_syntax = 'floggraph' -if get(g:, 'flog_use_ansi_esc') - finish -endif - runtime! syntax/diff.vim -" Commit {{{ +syntax match flogEmptyStart /^/ nextgroup=@flogDiff,@flogCommitInfo + +" Commit Highlighting + +syntax match flogCommitInfo contained nextgroup=@flogCommitInfo /\v%(%U2022.{-})@<= / +syntax cluster flogCommitInfo contains=flogHash,flogAuthor,flogRef,flogDate + +syntax match flogHash contained nextgroup=flogAuthor,flogRef,flogDate /\v%(\].*)@ \|, \|[^ \\)?*[]\+\)\+)\%( \|$\)/ + +" Date patterns +let weekday_name_pattern = '%(Mon|Monday|Tue|Tuesday|Wed|Wednesday|Thu|Thursday|Fri|Friday|Sat|Saturday|Sun|Sunday)' +let month_name_pattern = '%(Jan|January|Feb|February|Mar|March|Apr|April|May|Jun|June|Jul|July|Aug|August|Sep|September|Oct|October|Nov|November|Dec|December)' +let iso_date_pattern = '%(\d{4}-\d\d-\d\d|%())' +let iso_time_pattern = '%(\d\d:\d\d%(:\d\d%( ?[+-]\d\d:?\d\d)?)?)' -syntax match flogHash / \[[0-9a-f]\+\]/ -syntax match flogAuthor / {[^}]\+}/ -syntax match flogRef / (\(tag: \| -> \|, \|[^ \\)?*[]\+\)\+)/ -syntax match flogDate /\v<\zs\d{4}-\d\d-\d\d( \d\d:\d\d(:\d\d( [+-]\d{4})?)?)?/ +" ISO format +exec 'syntax match flogDate contained nextgroup=flogHash,flogAuthor,flogRef /\v' . iso_date_pattern . '%([T ]' . iso_time_pattern . ')?%( |$)/' +" RFC format +exec 'syntax match flogDate contained nextgroup=flogHash,flogAuthor,flogRef /\v' . weekday_name_pattern . ', \d{1,2} ' . month_name_pattern . ' \d{4}%( ' . iso_time_pattern . ')?%( |$)/' +" Local format +exec 'syntax match flogDate contained nextgroup=flogHash,flogAuthor,flogRef /\v' . weekday_name_pattern . ' ' . month_name_pattern . ' \d{1,2}%( ' . iso_time_pattern . ')? \d{4}' . '%( |$)/' +" Relative format +exec 'syntax match flogDate contained nextgroup=flogHash,flogAuthor,flogRef /\v%(\d+ %(year|month|week|day|hour|minute|second)s? ago)%( |$)/' +" Human formats +exec 'syntax match flogDate contained nextgroup=flogHash,flogAuthor,flogRef /\v' . weekday_name_pattern . ' ' . iso_time_pattern . '%( |$)/' +exec 'syntax match flogDate contained nextgroup=flogHash,flogAuthor,flogRef /\v' . month_name_pattern . ' \d{1,2} \d{4}' . '%( |$)/' highlight default link flogHash Statement highlight default link flogAuthor String highlight default link flogRef Directory highlight default link flogDate Number -" Ref {{{ +" Ref Highlighting -syntax match flogRefTag contained containedin=flogRef /\vtag: \zs.{-}\ze(, |)\)/ -syntax match flogRefRemote contained containedin=flogRef /\vremotes\/\zs.{-}\ze(, |)\)/ +syntax match flogRefTag contained containedin=flogRef /\vtag: \zs.{-}\ze%(, |)\)/ +syntax match flogRefRemote contained containedin=flogRef /\vremotes\/\zs.{-}\ze%(, |)\)/ highlight default link flogRefTag String highlight default link flogRefRemote Statement @@ -38,49 +56,46 @@ highlight default link flogRefHead Keyword highlight default link flogRefHeadArrow flogRef highlight default link flogRefHeadBranch Special -" }}} - -" Diff {{{ +" Diff Highlighting +" Copied from syntax/diff.vim +syntax match flogDiff contained nextgroup=@flogDiff /\v%(%U2022.*)@.*/ -syn match flogDiffChanged contained / ! .*/ +syn match flogDiffOnly contained /Only in .*/ +syn match flogDiffIdentical contained /Files .* and .* are identical$/ +syn match flogDiffDiffer contained /Files .* and .* differ$/ +syn match flogDiffBDiffer contained /Binary files .* and .* differ$/ +syn match flogDiffIsA contained /File .* is a .* while file .* is a .*/ +syn match flogDiffNoEOL contained /\\ No newline at end of file .*/ +syn match flogDiffCommon contained /Common subdirectories: .*/ -syn match flogDiffSubname contained containedin=flogDiffSubname / @@..*/ms=s+3 -syn match flogDiffLine contained / @.*/ +syn match flogDiffRemoved contained /-.*/ +syn match flogDiffRemoved contained /<.*/ +syn match flogDiffAdded contained /+.*/ +syn match flogDiffAdded contained />.*/ +syn match flogDiffChanged contained /! .*/ -syn match flogDiffLine contained / \*\*\*\*.*/ -syn match flogDiffLine contained / ---$/ -syn match flogDiffLine contained / \d\+\(,\d\+\)\=[cda]\d\+\>.*/ +syn match flogDiffSubname contained containedin=flogDiffSubname /@@..*/ms=s+3 +syn match flogDiffLine contained /@.*/ -syn match flogDiffFile contained / diff\>.*/ -syn match flogDiffFile contained / +++ .*/ -syn match flogDiffFile contained / Index: .*/ -syn match flogDiffFile contained / ==== .*/ -syn match flogDiffOldFile contained / \*\*\* .*/ -syn match flogDiffNewFile contained / --- .*/ +syn match flogDiffLine contained /\*\*\*\*.*/ +syn match flogDiffLine contained /---$/ +syn match flogDiffLine contained /\d\+\%(,\d\+\)\=[cda]\d\+\>.*/ -syn match flogDiffIndexLine contained / index \x\x\x\x.*/ -syn match flogDiffComment contained / #.*/ +syn match flogDiffFile contained /diff\>.*/ +syn match flogDiffFile contained /+++ .*/ +syn match flogDiffFile contained /Index: .*/ +syn match flogDiffFile contained /==== .*/ +syn match flogDiffOldFile contained /\*\*\* .*/ +syn match flogDiffNewFile contained /--- .*/ -" link to original highlight groups +syn match flogDiffIndexLine contained /index \x\x\x\x.*/ +syn match flogDiffComment contained /#.*/ +" Link to original highlight groups hi default link flogDiffAdded diffAdded hi default link flogDiffBDiffer diffBDiffer hi default link flogDiffChanged diffChanged @@ -98,54 +113,47 @@ hi default link flogDiffOldFile diffOldFile hi default link flogDiffOnly diffOnly hi default link flogDiffRemoved diffRemoved -" }}} +" Graph Highlighting -" }}} +" Entry point for branches +syntax match flogGraphBranch0 nextgroup=flogGraphBranch2,flogDiff,flogCommitInfo /\v^%( |%u2502|%u250a|%u251c|%u256d|%u2570|%U2022)/ -" Graph {{{ +" Color cycle for branches +let branch_pattern = '/\v%( |%u2500|%u252c|%u2570)%( |%u2500|%u2502|%u250a|%u251c|%u2524|%u252c|%u2534|%u253c|%u256d|%u256e|%u256f|%u2570|%U2022)/' +exec 'syntax match flogGraphBranch9 contained nextgroup=flogGraphBranch1,flogDiff,flogCommitInfo ' . branch_pattern +exec 'syntax match flogGraphBranch8 contained nextgroup=flogGraphBranch9,flogDiff,flogCommitInfo ' . branch_pattern +exec 'syntax match flogGraphBranch7 contained nextgroup=flogGraphBranch8,flogDiff,flogCommitInfo ' . branch_pattern +exec 'syntax match flogGraphBranch6 contained nextgroup=flogGraphBranch7,flogDiff,flogCommitInfo ' . branch_pattern +exec 'syntax match flogGraphBranch5 contained nextgroup=flogGraphBranch6,flogDiff,flogCommitInfo ' . branch_pattern +exec 'syntax match flogGraphBranch4 contained nextgroup=flogGraphBranch5,flogDiff,flogCommitInfo ' . branch_pattern +exec 'syntax match flogGraphBranch3 contained nextgroup=flogGraphBranch4,flogDiff,flogCommitInfo ' . branch_pattern +exec 'syntax match flogGraphBranch2 contained nextgroup=flogGraphBranch3,flogDiff,flogCommitInfo ' . branch_pattern +exec 'syntax match flogGraphBranch1 contained nextgroup=flogGraphBranch2,flogDiff,flogCommitInfo ' . branch_pattern -" these syntax regex match all possible graph characters -" they will match one vertical column of graph characters from left to right ignoring whitespace -" this makes all graph characters in a column highlighted in the same way -syntax match flogGraphEdge9 /[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge1,@flogDiff contained -syntax match flogGraphEdge8 /[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge9,@flogDiff contained -syntax match flogGraphEdge7 /[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge8,@flogDiff contained -syntax match flogGraphEdge6 /[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge7,@flogDiff contained -syntax match flogGraphEdge5 /[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge6,@flogDiff contained -syntax match flogGraphEdge4 /[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge5,@flogDiff contained -syntax match flogGraphEdge3 /[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge4,@flogDiff contained -syntax match flogGraphEdge2 /[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge3,@flogDiff contained -syntax match flogGraphEdge1 /[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge2,@flogDiff contained -syntax match flogGraphEdge0 /^[_/ ]\?[|/\\*]/ nextgroup=flogGraphEdge2,@flogDiff +syntax cluster flogGraphBranch contains=flogGraphBranch0,flogGraphBranch1,flogGraphBranch2,flogGraphBranch3,flogGraphBranch4,flogGraphBranch5,flogGraphBranch6,flogGraphBranch7,flogGraphBranch8,flogGraphBranch9 -syntax cluster flogGraphEdge contains=flogGraphEdge0,flogGraphEdge1,flogGraphEdge2,flogGraphEdge3,flogGraphEdge4,flogGraphEdge5,flogGraphEdge6,flogGraphEdge7,flogGraphEdge8,flogGraphEdge9 - -syntax match flogGraphCrossing /_\|\/\ze|/ contained containedin=@flogGraphEdge -syntax match flogGraphCommit /\*/ contained containedin=@flogGraphEdge +syntax match flogGraphCommit /\v%U2022/ contained containedin=@flogGraphBranch +syntax match flogGraphMerge /\v%(%u2500|%u252c\ze%U2022|%u2534)/ contained containedin=@flogGraphBranch if &background ==# 'dark' - highlight default flogGraphEdge1 ctermfg=magenta guifg=green1 - highlight link flogGraphEdge0 flogGraphEdge1 - highlight default flogGraphEdge2 ctermfg=green guifg=yellow1 - highlight default flogGraphEdge3 ctermfg=yellow guifg=orange1 - highlight default flogGraphEdge4 ctermfg=cyan guifg=greenyellow - highlight default flogGraphEdge5 ctermfg=red guifg=springgreen1 - highlight default flogGraphEdge6 ctermfg=yellow guifg=cyan1 - highlight default flogGraphEdge7 ctermfg=green guifg=slateblue1 - highlight default flogGraphEdge8 ctermfg=cyan guifg=magenta1 - highlight default flogGraphEdge9 ctermfg=magenta guifg=purple1 + highlight default flogGraphBranch1 ctermfg=magenta guifg=green1 + highlight link flogGraphBranch0 flogGraphBranch1 + highlight default flogGraphBranch2 ctermfg=green guifg=yellow1 + highlight default flogGraphBranch3 ctermfg=yellow guifg=orange1 + highlight default flogGraphBranch4 ctermfg=cyan guifg=greenyellow + highlight default flogGraphBranch5 ctermfg=red guifg=springgreen1 + highlight default flogGraphBranch6 ctermfg=yellow guifg=cyan1 + highlight default flogGraphBranch7 ctermfg=green guifg=slateblue1 + highlight default flogGraphBranch8 ctermfg=cyan guifg=magenta1 + highlight default flogGraphBranch9 ctermfg=magenta guifg=purple1 else - highlight default flogGraphEdge1 ctermfg=darkyellow guifg=orangered3 - highlight default flogGraphEdge2 ctermfg=darkgreen guifg=orange2 - highlight default flogGraphEdge3 ctermfg=blue guifg=yellow3 - highlight default flogGraphEdge4 ctermfg=darkmagenta guifg=olivedrab4 - highlight default flogGraphEdge5 ctermfg=red guifg=green4 - highlight default flogGraphEdge6 ctermfg=darkyellow guifg=paleturquoise3 - highlight default flogGraphEdge7 ctermfg=darkgreen guifg=deepskyblue4 - highlight default flogGraphEdge8 ctermfg=blue guifg=darkslateblue - highlight default flogGraphEdge9 ctermfg=darkmagenta guifg=darkviolet + highlight default flogGraphBranch1 ctermfg=darkyellow guifg=orangered3 + highlight default flogGraphBranch2 ctermfg=darkgreen guifg=orange2 + highlight default flogGraphBranch3 ctermfg=blue guifg=yellow3 + highlight default flogGraphBranch4 ctermfg=darkmagenta guifg=olivedrab4 + highlight default flogGraphBranch5 ctermfg=red guifg=green4 + highlight default flogGraphBranch6 ctermfg=darkyellow guifg=paleturquoise3 + highlight default flogGraphBranch7 ctermfg=darkgreen guifg=deepskyblue4 + highlight default flogGraphBranch8 ctermfg=blue guifg=darkslateblue + highlight default flogGraphBranch9 ctermfg=darkmagenta guifg=darkviolet endif - -" }}} - -" vim: set et sw=2 ts=2 fdm=marker: diff --git a/t/data/graph_branch_end_out b/t/data/graph_branch_end_out new file mode 100644 index 0000000..97c850c --- /dev/null +++ b/t/data/graph_branch_end_out @@ -0,0 +1,17 @@ +• 1-e +├─┬─┬─╮ +│ • │ │ 2-b +│ │ • │ 3-b +│ │ │ • 4-b +│ • │ │ 2-a +├─┴┬┊─┤ +│ ╰┤ │ +• │ │ 1-c +│ • │ 3-a +│ ╭─╯ │ +│ │ • 4-a +│ ├───╯ +• │ 1-b +├─╯ +• 1-a + diff --git a/t/data/graph_merge_complex_out b/t/data/graph_merge_complex_out new file mode 100644 index 0000000..13b7458 --- /dev/null +++ b/t/data/graph_merge_complex_out @@ -0,0 +1,14 @@ +• 1-d +├─┬─╮ +│ │ • 3-b +│ • │ 2-b +• │ │ 1-c +├┬┊─┤ +│╰┤ │ +│ │ • 3-a +│ • │ 2-a +├─╯ │ +• │ 1-b +├───╯ +• 1-a + diff --git a/t/data/graph_merge_cross_out b/t/data/graph_merge_cross_out new file mode 100644 index 0000000..8c087ca --- /dev/null +++ b/t/data/graph_merge_cross_out @@ -0,0 +1,16 @@ +• 1-e +├─┬─╮ +• │ │ 1-d +│ • │ 2-c +│ │ • 3-b +│ • │ 2-b +├─┼─┤ +│ • │ 2-a +• │ │ 1-c +├─╯ │ +│ • 3-a +│ ╭─╯ +• │ 1-b +├─╯ +• 1-a + diff --git a/t/data/graph_merge_multiline_out b/t/data/graph_merge_multiline_out new file mode 100644 index 0000000..33c9774 --- /dev/null +++ b/t/data/graph_merge_multiline_out @@ -0,0 +1,17 @@ +• 1-e +│ 1-e +• 1-d +│ 1-d +├─╮ +│ • 2-b +│ │ 2-b +│ • 2-a +│ │ 2-a +• │ 1-c +│ │ 1-c +├─╯ +• 1-b +│ 1-b +• 1-a + 1-a + diff --git a/t/data/graph_merge_out b/t/data/graph_merge_out new file mode 100644 index 0000000..4a36a66 --- /dev/null +++ b/t/data/graph_merge_out @@ -0,0 +1,10 @@ +• 1-e +• 1-d +├─╮ +│ • 2-b +│ • 2-a +• │ 1-c +├─╯ +• 1-b +• 1-a + diff --git a/t/data/graph_octopus_crossover_left_out b/t/data/graph_octopus_crossover_left_out new file mode 100644 index 0000000..3daee27 --- /dev/null +++ b/t/data/graph_octopus_crossover_left_out @@ -0,0 +1,12 @@ +• 1-c +│ • 2-b +├─┼─┬─╮ +• │ │ │ 1-b +│ │ │ • 4-a +├─┊─┊─╯ +│ │ • 3-a +├─┊─╯ +│ • 2-a +├─╯ +• 1-a + diff --git a/t/data/graph_octopus_crossover_out b/t/data/graph_octopus_crossover_out new file mode 100644 index 0000000..68947f7 --- /dev/null +++ b/t/data/graph_octopus_crossover_out @@ -0,0 +1,13 @@ +• 1-b +│ • 2-b +│ ├─┬─┬─╮ +│ │ │ │ • 5-a +├─┊─┊─┊─╯ +│ │ │ • 4-a +├─┊─┊─╯ +│ │ • 3-a +├─┊─╯ +│ • 2-a +├─╯ +• 1-a + diff --git a/t/data/graph_octopus_left_out b/t/data/graph_octopus_left_out new file mode 100644 index 0000000..e415598 --- /dev/null +++ b/t/data/graph_octopus_left_out @@ -0,0 +1,12 @@ +• 1-c +│ • 2-b +├─┼─┬─╮ +│ │ │ • 4-a +│ │ • │ 3-a +│ │ ├─╯ +│ • │ 2-a +│ ├─╯ +• │ 1-b +├─╯ +• 1-a + diff --git a/t/data/graph_octopus_out b/t/data/graph_octopus_out new file mode 100644 index 0000000..aad9ed1 --- /dev/null +++ b/t/data/graph_octopus_out @@ -0,0 +1,11 @@ +• 1-c +├─┬─┬─╮ +│ │ │ • 4-a +│ │ • │ 3-a +│ │ ├─╯ +│ • │ 2-a +│ ├─╯ +• │ 1-b +├─╯ +• 1-a + diff --git a/t/data/graph_simple_out b/t/data/graph_simple_out new file mode 100644 index 0000000..f5c28ff --- /dev/null +++ b/t/data/graph_simple_out @@ -0,0 +1,5 @@ +• 1-d +• 1-c +• 1-b +• 1-a + diff --git a/t/data/graph_tangle_out b/t/data/graph_tangle_out new file mode 100644 index 0000000..d3af603 --- /dev/null +++ b/t/data/graph_tangle_out @@ -0,0 +1,34 @@ +• 1-l +├─╮ +• │ 1-k +├─┊─┬─╮ +│ • │ │ 2-d +• │ │ │ 1-j +├─╯ │ │ +│ • │ 3-c +├───╯ │ +│ • 4-a +├─────╯ +• 1-i +├─╮ +• │ 1-h +├─┊─╮ +│ • │ 2-c +│ ├─┊─╮ +• │ │ │ 1-g +│ │ • │ 3-b +│ │ ├─╯ +│ • │ 2-b +│ ├─┊─╮ +• │ │ │ 1-f +├─┊─┊─╯ +│ │ • 3-a +│ • │ 2-a +• │ │ 1-e +• │ │ 1-d +├─┊─╯ +• │ 1-c +├─╯ +• 1-b +• 1-a + diff --git a/t/data/graph_tangle_range_out b/t/data/graph_tangle_range_out new file mode 100644 index 0000000..814afd4 --- /dev/null +++ b/t/data/graph_tangle_range_out @@ -0,0 +1,16 @@ +• 2-d +• 1-i +├─╮ +│ ┊ +│ +• 1-h +├─╮ +│ • 3-b +│ │ +│ ┊ +│ +• 1-g +• 1-f +│ +┊ + diff --git a/t/flog.vim b/t/flog.vim deleted file mode 100644 index 37bfa40..0000000 --- a/t/flog.vim +++ /dev/null @@ -1,184 +0,0 @@ -scriptencoding utf-8 - -runtime! plugin/fugitive.vim -runtime! plugin/flog.vim - -describe ':Flog' - before - Flog - end - - after - if &ft ==# 'floggraph' - call flog#quit() - endif - end - - it 'allows quitting' - call flog#quit() - Expect &ft !=# 'floggraph' - end - - it 'sets filetype' - Expect &ft ==# 'floggraph' - end - - it 'shows output' - Expect line('$') > 1 - end - - it 'opens in a tab' - Expect winnr('$') == 1 - Expect tabpagenr() == 2 - end - - it 'has empty temporary windows' - Expect flog#get_state().tmp_cmd_window_ids == [] - end -end - -" returns empty log every time -describe ':Flog -- --not --glob="*"' - before - Flog -- --not --glob="*" - end - - after - if &ft ==# 'floggraph' - call flog#quit() - endif - end - - it 'does not crash on opening temp commit' - call flog#run_tmp_command('Gsplit %h') - Expect winnr('$') == 1 - end -end - -describe ':Flogsplit' - before - Flogsplit - end - - after - if &ft ==# 'floggraph' - call flog#quit() - endif - end - - it 'sets filetype' - Expect &ft ==# 'floggraph' - end - - it 'shows output' - Expect line('$') > 1 - end - - it 'opens in a split' - Expect winnr('$') == 2 - Expect tabpagenr() == 1 - end - - it 'has empty temp windows' - Expect flog#get_state().tmp_cmd_window_ids == [] - end -end - -" opens the commit window -describe 'flog#run_tmp_command("Gsplit %h", 1)' - before - Flog - call flog#run_tmp_command('Gsplit %h', 1) - end - - after - if &ft ==# 'git' - close! - endif - if &ft ==# 'floggraph' - call flog#quit() - endif - end - - it 'opens in a temp window' - Expect &ft !=# 'floggraph' - Expect winnr('$') == 2 - Expect flog#get_state().tmp_cmd_window_ids == [win_getid()] - end -end - -" try to open a temp window when no window is generated -describe 'flog#run_tmp_command("!git status", 1)' - before - Flog - call flog#run_tmp_command('!git status', 1) - end - - after - if &ft ==# 'floggraph' - call flog#quit() - endif - end - - it 'does not open in a window' - Expect &ft ==# 'floggraph' - Expect winnr('$') == 1 - Expect flog#get_state().tmp_cmd_window_ids == [] - end -end - -" open a temp window with Git -p, which implicitly opens a window -describe 'flog#run_tmp_command("Git -p status", 1)' - before - Flog - call flog#run_tmp_command('Git -p status', 1) - end - - after - call flog#quit() - end - - it 'opens in a temp window' - Expect &ft !=# 'floggraph' - Expect winnr('$') == 2 - Expect flog#get_state().tmp_cmd_window_ids == [win_getid()] - end -end - -" open a temp window with Git diff, which explicitly opens a window -describe 'flog#run_tmp_command("Git diff", 1)' - before - Flog - call flog#run_tmp_command('Git diff', 1) - end - - after - call flog#quit() - end - - it 'opens in a temp window' - Expect &ft !=# 'floggraph' - Expect winnr('$') == 2 - Expect flog#get_state().tmp_cmd_window_ids == [win_getid()] - end -end - -" open a temp window with Git -p diff, which implicitly and explicitly opens a window -describe 'flog#run_tmp_command("Git -p diff", 1)' - before - Flog - call flog#run_tmp_command('Git -p diff', 1) - end - - after - call flog#quit() - end - - it 'opens in a temp window' - Expect &ft !=# 'floggraph' - Expect winnr('$') == 2 - Expect flog#get_state().tmp_cmd_window_ids == [win_getid()] - end -end - -" vim: set et sw=2 ts=2 fdm=marker: diff --git a/t/lib_diff.sh b/t/lib_diff.sh new file mode 100644 index 0000000..ab081ac --- /dev/null +++ b/t/lib_diff.sh @@ -0,0 +1,7 @@ +TEST_DIR=$(realpath -- "$(dirname -- "$0")") + +. "$TEST_DIR/lib_dir.sh" + +diff_data() { + diff --color --strip-trailing-cr -- "$1" "$DATA_DIR/$2" +} diff --git a/t/lib_dir.sh b/t/lib_dir.sh new file mode 100644 index 0000000..ea2d6d1 --- /dev/null +++ b/t/lib_dir.sh @@ -0,0 +1,32 @@ +export TEST_DIR=$(realpath -- "$(dirname -- "$0")") +export BASE_DIR=$(realpath -- "$(dirname -- "$0")/..") +export DATA_DIR=$(realpath -- "$(dirname -- "$0")/data") + +get_dir() { + echo "$BASE_DIR/.test/$1" +} + +get_tmp_dir() { + echo "$BASE_DIR/.test/tmp/$1" +} + +create_abs_dir() { + mkdir -p "$1" + echo "$1" +} + +create_dir() { + create_abs_dir "$(get_dir "$1")" +} + +create_tmp_dir() { + create_dir "tmp/$1" +} + +remove_dir() { + rm -rf "$BASE_DIR/.test/$1" +} + +remove_tmp_dirs() { + remove_dir "tmp/" +} diff --git a/t/lib_git.sh b/t/lib_git.sh new file mode 100644 index 0000000..0b686fd --- /dev/null +++ b/t/lib_git.sh @@ -0,0 +1,36 @@ +TEST_DIR=$(realpath -- "$(dirname -- "$0")") + +. "$TEST_DIR/lib_dir.sh" + +git_init() { + _WORKTREE=$(create_tmp_dir "repo/$1") + _GIT_DIR="$_WORKTREE/.git" + git --git-dir="$_GIT_DIR" init -q -b main + git --git-dir="$_GIT_DIR" config user.email flog@test.com + git --git-dir="$_GIT_DIR" config user.name flog + echo $_WORKTREE +} + +git_checkout() { + git checkout -q "$@" +} + +git_commit() { + git commit -q --allow-empty "$@" +} + +git_merge() { + git merge -q --no-edit --no-ff "$@" +} + +git_tag() { + git tag "$@" +} + +# Create and tag multiple commits +git_commit_tag() { + for commit in $@; do + git_commit -m "$commit" + git_tag "$commit" + done +} diff --git a/t/lib_print.sh b/t/lib_print.sh new file mode 100644 index 0000000..1b53d1a --- /dev/null +++ b/t/lib_print.sh @@ -0,0 +1,16 @@ +COLOR_GREEN="\033[1m\033[32m" +COLOR_RED="\033[1m\033[31m" +COLOR_CYAN="\033[1m\033[36m" +COLOR_OFF="\033[0m" + +print_success() { + echo -e "${COLOR_GREEN}success${COLOR_OFF}" +} + +print_fail() { + echo -e "${COLOR_RED}fail${COLOR_OFF}" +} + +print_title() { + echo -e "${COLOR_CYAN}running test $1...${COLOR_OFF}" +} diff --git a/t/lib_vim.sh b/t/lib_vim.sh new file mode 100644 index 0000000..cff3ce2 --- /dev/null +++ b/t/lib_vim.sh @@ -0,0 +1,61 @@ +TEST_DIR=$(realpath -- "$(dirname -- "$0")") + +. "$TEST_DIR/lib_dir.sh" + +export VIM_DIR=$(get_dir "vim/") +export FLOG_DIR="${VIM_DIR}/vim-flog" +export FUGITIVE_DIR="${VIM_DIR}/vim-fugitive" + +export VIMRC="$VIM_DIR/.vimrc" + +install_vim() { + echo "setting up vim..." + + remove_dir "vim/" + create_dir "vim/vim-flog" > /dev/null + + cd "$BASE_DIR" + cp -rf autoload ftplugin plugin syntax lua "$FLOG_DIR" + + git clone -q --depth 1 "https://github.com/tpope/vim-fugitive" "$FUGITIVE_DIR" +} + +run_vim_command() { + _TMP=$(create_tmp_dir "vim/") + + _SCRIPT=$_TMP/_script.vim + _OUT="$_TMP/_out" + + cat > "$_SCRIPT" + + cat < "$VIMRC" +set nocompatible +filetype plugin indent on +exec 'set rtp+=' . fnameescape("$FLOG_DIR") +exec 'set rtp+=' . fnameescape("$FUGITIVE_DIR") +EOF + + _VIM=vim + if [ "$NVIM" = "true" ]; then + _VIM=nvim + fi + + set +e + $_VIM \ + -u "$VIMRC" \ + -e \ + -c "redir > $_OUT" \ + -S "$_SCRIPT" \ + -c "qa!" + STATUS=$? + set -e + + if [ -s "$_OUT" ]; then + tail -n +2 "$_OUT" + echo + fi + + rm -f "$_OUT" + + return $STATUS +} diff --git a/t/run.sh b/t/run.sh new file mode 100755 index 0000000..affe854 --- /dev/null +++ b/t/run.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -e + +TEST_DIR=$(realpath -- "$(dirname -- "$0")") + +. "$TEST_DIR/lib_dir.sh" +. "$TEST_DIR/lib_print.sh" +. "$TEST_DIR/lib_vim.sh" + +# Setup +cd "$BASE_DIR" +install_vim + +# Get args +if [ "$1" != "" ]; then + TESTS="$TEST_DIR/$1" +else + TESTS=$(ls "$TEST_DIR"/t_*) +fi + +# Run tests +FAILED_TESTS=0 +for TEST in $TESTS; do + # Reset + cd "$BASE_DIR" + remove_tmp_dirs + + # Run the test + print_title "${TEST}" + set +e + "${TEST}" + RESULT=$? + set -e + + # Process result + if [ $RESULT -eq 0 ]; then + print_success + else + print_fail + FAILED_TESTS=$(expr "$FAILED_TESTS" + 1) + fi +done + +if [ $FAILED_TESTS -gt 0 ]; then + exit 1 +fi diff --git a/t/t_flog_cmd.sh b/t/t_flog_cmd.sh new file mode 100755 index 0000000..6a83fc7 --- /dev/null +++ b/t/t_flog_cmd.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +set -e + +TEST_DIR=$(realpath -- "$(dirname -- "$0")") + +. "$TEST_DIR/lib_dir.sh" +. "$TEST_DIR/lib_diff.sh" +. "$TEST_DIR/lib_git.sh" +. "$TEST_DIR/lib_vim.sh" + +TMP=$(create_tmp_dir flog_cmd) + +WORKTREE=$(git_init flog_cmd) +cd "$WORKTREE" + +git_commit_tag 1-a + +run_vim_command <" + +call flog#test#Assert('winnr("$") == 1') +EOF diff --git a/t/t_graph_branch_end.sh b/t/t_graph_branch_end.sh new file mode 100755 index 0000000..882f23b --- /dev/null +++ b/t/t_graph_branch_end.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -e + +TEST_DIR=$(realpath -- "$(dirname -- "$0")") + +. "$TEST_DIR/lib_dir.sh" +. "$TEST_DIR/lib_diff.sh" +. "$TEST_DIR/lib_git.sh" +. "$TEST_DIR/lib_vim.sh" + +TMP=$(create_tmp_dir graph_branch_end) + +WORKTREE=$(git_init graph_branch_end) +cd "$WORKTREE" + +git_commit_tag 1-a 1-b + +git_checkout 1-a +git_commit_tag 3-a + +git_checkout 1-a +git_commit_tag 4-a + +git_checkout 1-b +git_commit_tag 1-c +git_merge -m 2-a 3-a 4-a +git_tag 2-a + +git_checkout 4-a +git_commit_tag 4-b + +git_checkout 3-a +git_commit_tag 3-b + +git_checkout 2-a +git_commit_tag 2-b + +git_checkout 1-c +git_merge -m 1-e 2-b 3-b 4-b + +VIM_OUT="$TMP/out" +run_vim_command <