From 8fd7f9bfed753dbaa5543747569b4c2543aff03d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 17 Jul 2021 09:26:34 +0200 Subject: Merging upstream version 0.15.1. Signed-off-by: Daniel Baumann --- .gita-completion.bash | 18 +- .github/dependabot.yml | 7 + MANIFEST.in | 2 +- README.md | 210 +++++++++++--- doc/README_CN.md | 30 +- doc/screenshot.png | Bin 225737 -> 262997 bytes gita/__main__.py | 235 +++++++++++---- gita/cmds.json | 89 ++++++ gita/cmds.yml | 65 ----- gita/common.py | 17 +- gita/info.py | 103 ++++--- gita/utils.py | 261 +++++++++++++---- setup.py | 3 +- tests/clash_path_file | 4 +- tests/conftest.py | 1 + tests/main_path_file | 2 + tests/mock_group_file | 4 +- tests/mock_path_file | 2 +- tests/test_info.py | 4 +- tests/test_main.py | 271 +++++++++++++---- tests/test_utils.py | 85 ++++-- work.vim | 766 +++++++++++++++++++++++++++++++++++++++++++++++++ 22 files changed, 1806 insertions(+), 373 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 gita/cmds.json delete mode 100644 gita/cmds.yml create mode 100644 tests/main_path_file create mode 100644 work.vim diff --git a/.gita-completion.bash b/.gita-completion.bash index 5090bb7..493db05 100644 --- a/.gita-completion.bash +++ b/.gita-completion.bash @@ -7,30 +7,36 @@ _gita_completions() cur=${COMP_WORDS[COMP_CWORD]} cmd=${COMP_WORDS[1]} - # FIXME: this is somewhat slow - commands=`gita -h | sed '2q;d' |sed 's/[{}.,]/ /g'` - - repos=`gita ls` # this doesn't work for two repos with the same basename #gita_path=${XDG_CONFIG_HOME:-$HOME/.config}/gita/repo_path #repos=`awk '{split($0, paths, ":")} END {for (i in paths) {n=split(paths[i],b, /\//); print b[n]}}' ${gita_path}` if [ $COMP_CWORD -eq 1 ]; then + # FIXME: this is somewhat slow + commands=`gita -h | sed '2q;d' |sed 's/[{}.,]/ /g'` COMPREPLY=($(compgen -W "${commands}" ${cur})) elif [ $COMP_CWORD -gt 1 ]; then case $cmd in add) COMPREPLY=($(compgen -d ${cur})) ;; - ll) + clone) + COMPREPLY=($(compgen -f ${cur})) + ;; + color | flags) + COMPREPLY=($(compgen -W "ll set" ${cur})) + ;; + ll | context) + groups=`gita group ls` + COMPREPLY=($(compgen -W "${groups}" ${cur})) return ;; *) + repos=`gita ls` COMPREPLY=($(compgen -W "${repos}" ${cur})) ;; esac fi - } complete -F _gita_completions gita diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..491deae --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/MANIFEST.in b/MANIFEST.in index e50bea9..18fb062 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include gita/cmds.yml +include gita/cmds.json diff --git a/README.md b/README.md index 1c9b321..4b4c516 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ | | ____ | | | | | ___ | | | \_ ) | | | | | ( ) | | (___) |__) (___ | | | ) ( | -(_______)_______/ )_( |/ \| v0.12 +(_______)_______/ )_( |/ \| v0.15 ``` # Gita: a command-line tool to manage multiple git repos @@ -29,11 +29,13 @@ I also hate to change directories to execute git commands. ![gita screenshot](https://github.com/nosarthur/gita/raw/master/doc/screenshot.png) -In the screenshot, the `gita remote nowhub` command translates to `git remote -v` -for the `nowhub` repo, even though we are at the `blog` repo. -To see the pre-defined sub-commands, run `gita -h` or take a look at -[cmds.yml](https://github.com/nosarthur/gita/blob/master/gita/cmds.yml). -To add your own sub-commands, see the [customization section](#custom). +In this screenshot, the `gita ll` command displays the status of all repos. +The `gita remote dotfiles` command translates to `git remote -v` +for the `dotfiles` repo, even though we are not in the repo. +The `gita fetch` command fetches from all repos and two of them have updates. +To see the pre-defined commands, run `gita -h` or take a look at +[cmds.json](https://github.com/nosarthur/gita/blob/master/gita/cmds.json). +To add your own commands, see the [customization section](#custom). To run arbitrary `git` command, see the [superman mode section](#superman). To run arbitrary shell command, see the [shell mode section](#shell). @@ -48,7 +50,7 @@ The branch color distinguishes 5 situations between local and remote branches: The choice of purple for ahead and yellow for behind is motivated by [blueshift](https://en.wikipedia.org/wiki/Blueshift) and [redshift](https://en.wikipedia.org/wiki/Redshift), using green as baseline. -You can change the color scheme using the `gita color` sub-command. +You can change the color scheme using the `gita color` command. See the [customization section](#custom). The additional status symbols denote @@ -60,7 +62,13 @@ The additional status symbols denote The bookkeeping sub-commands are - `gita add `: add repo(s) to `gita` +- `gita add -a `: add repo(s) in recursively + and automatically generate hierarchical groups. See the [customization section](#custom) for more details. +- `gita add -b `: add bare repo(s) to `gita`. See the [customization section](#custom) for more details on setting custom worktree. +- `gita add -m `: add main repo(s) to `gita`. See the [customization section](#custom) for more details. +- `gita add -r `: add repo(s) in recursively - `gita clone `: clone repos in `config-file` (generated by `gita freeze`) to current directory. +- `gita clone -p `: clone repos in `config-file` to prescribed paths. - `gita context`: context sub-command - `gita context`: show current context - `gita context none`: remove context @@ -68,9 +76,12 @@ The bookkeeping sub-commands are - `gita color`: color sub-command - `gita color [ll]`: Show available colors and the current coloring scheme - `gita color set `: Use the specified color for the local-remote situation +- `gita flags`: flags sub-command + - `gita flags set `: add custom `flags` to repo + - `gita flags [ll]`: display repos with custom flags - `gita freeze`: print information of all repos such as URL, name, and path. - `gita group`: group sub-command - - `gita group add -n `: add repo(s) to a new group or existing group + - `gita group add -n `: add repo(s) to a new or existing group - `gita group [ll]`: display existing groups with repos - `gita group ls`: display existing group names - `gita group rename `: change group name @@ -85,7 +96,7 @@ The bookkeeping sub-commands are - `gita ls`: display the names of all repos - `gita ls `: display the absolute path of one repo - `gita rename `: rename a repo -- `gita rm `: remove repo(s) from `gita` (won't remove files from disk) +- `gita rm `: remove repo(s) from `gita` (won't remove files on disk) - `gita -v`: display gita version The `git` delegating sub-commands are of two formats @@ -99,7 +110,7 @@ They translate to `git ` for the corresponding repos. By default, only `fetch` and `pull` take optional input. In other words, `gita fetch` and `gita pull` apply to all repos. To see the pre-defined sub-commands, run `gita -h` or take a look at -[cmds.yml](https://github.com/nosarthur/gita/blob/master/gita/cmds.yml). +[cmds.json](https://github.com/nosarthur/gita/blob/master/gita/cmds.json). To add your own sub-commands or override the default behaviors, see the [customization section](#custom). To run arbitrary `git` command, see the [superman mode section](#superman). @@ -107,7 +118,8 @@ If more than one repos are specified, the `git` command runs asynchronously, with the exception of `log`, `difftool` and `mergetool`, which require non-trivial user input. -Repo paths are saved in `$XDG_CONFIG_HOME/gita/repo_path` (most likely `~/.config/gita/repo_path`). +Repo configuration is saved in `$XDG_CONFIG_HOME/gita/repos.csv` +(most likely `~/.config/gita/repos.csv`). ## Installation @@ -124,7 +136,7 @@ pip3 install -e ``` In either case, calling `gita` in terminal may not work, -then you can put the following line in the `.bashrc` file. +then put the following line in the `.bashrc` file. ``` alias gita="python3 -m gita" @@ -140,7 +152,7 @@ Download [.gita-completion.bash](https://github.com/nosarthur/gita/blob/master/.gita-completion.bash) or [.gita-completion.zsh](https://github.com/nosarthur/gita/blob/master/.gita-completion.zsh) -and source it in the corresponding rc file. +and source it in shell. ## Superman mode @@ -171,65 +183,178 @@ Here `repo-name(s)` or `group-name(s)` are optional, and their absence means all For example, - `gita shell ll` lists contents for all repos -- `gita shell repo1 mkdir docs` create a new directory `docs` in repo1 +- `gita shell repo1 repo2 mkdir docs` create a new directory `docs` in `repo1` and `repo2` +- `gita shell "git describe --abbrev=0 --tags | xargs git checkout"`: check out the latest tag for all repos ## Customization -### user-defined sub-command using yaml file +### define repo group and context + +When the project contains several independent but related repos, +we can define a group and execute `gita` command on this group. +For example, + +``` +gita group add repo1 repo2 -n my-group +gita ll my-group +gita pull my-group +``` + +To save more typing, one can set a group as context, then any `gita` command +is scoped to the group + +``` +gita context my-group +gita ll +gita pull +``` + +It is also possible to recursively add repos within a directory and +generate hierarchical groups automatically. For example, running + +``` +gita add -a src +``` +on the following folder structure +``` +src +├── project1 +│   ├── repo1 +│   └── repo2 +├── repo3 +├── project2 +│   ├── repo4 +│   └── repo5 +└── repo6 +``` +gives rise to +``` +src:repo1,repo2,repo3,repo4,repo5,repo6 +src-project1:repo1,repo2 +src-project2:repo4,repo5 +``` + + +### define main repos and shadow the global configuration setting with local setting + +The so-called main repos contain `.gita` folder for local configurations. +It works best for the repos-within-repo project structure, for example, + +``` +main-repo +├── sub-repo1 +│   └── sub-sub-repo +├── sub-repo2 +└── sub-repo3 +``` -Custom delegating sub-commands can be defined in `$XDG_CONFIG_HOME/gita/cmds.yml` -(most likely `~/.config/gita/cmds.yml`). +When executing `gita` commands within/relative to a main repo, local configurations +are used. And only repos within the current main repos are in the scope. + +To add a main repo, run + +``` +gita add -m main-repo-path +``` + +Subordinate repos are added recursively to the local configuration. +Only the main repo is saved to the global configuration. + +In the `gita ll` display, the main repos are underlined. + +### add user-defined sub-command using json file + +Custom delegating sub-commands can be defined in `$XDG_CONFIG_HOME/gita/cmds.json` +(most likely `~/.config/gita/cmds.json`) And they shadow the default ones if name collisions exist. Default delegating sub-commands are defined in -[cmds.yml](https://github.com/nosarthur/gita/blob/master/gita/cmds.yml). +[cmds.json](https://github.com/nosarthur/gita/blob/master/gita/cmds.json). For example, `gita stat ` is registered as -```yaml -stat: - cmd: diff --stat - help: show edit statistics +```json +"stat":{ + "cmd": "git diff --stat", + "help": "show edit statistics" +} ``` which executes `git diff --stat` for the specified repo(s). -If the delegated `git` command is a single word, the `cmd` tag can be omitted. -See `push` for an example. -To disable asynchronous execution, set the `disable_async` tag to be `true`. -See `difftool` for an example. +To disable asynchronous execution, set `disable_async` to be `true`. +See the `difftool` example: + +```json +"difftool":{ + "cmd": "git difftool", + "disable_async": true, + "help": "show differences using a tool" +} +``` -If you want a custom command to behave like `gita fetch`, i.e., to apply the -command to all repos when no repo is specified, -set the `allow_all` option to be `true`. +If you want a custom command to behave like `gita fetch`, i.e., to apply to all +repos when no repo is specified, set `allow_all` to be `true`. For example, the following snippet creates a new command `gita comaster [repo-name(s)]` with optional repo name input. -```yaml -comaster: - cmd: checkout master - allow_all: true - help: checkout the master branch +```json +"comaster":{ + "cmd": "checkout master", + "allow_all": true, + "help": "checkout the master branch" +} +``` + +Any command that runs in the [superman mode](#superman) mode or the +[shell mode](#shell) can be defined in this json format. +For example, the following command runs in shell mode and fetches only the +current branch from upstream. + +```json +"fetchcrt":{ + "cmd": "git rev-parse --abbrev-ref HEAD | xargs git fetch --prune upstream", + "allow_all": true, + "shell": true, + "help": "fetch current branch only" +} ``` ### customize the local/remote relationship coloring displayed by the `gita ll` command You can see the default color scheme and the available colors via `gita color`. To change the color coding, use `gita color set `. -The configuration is saved in `$XDG_CONFIG_HOME/gita/color.yml`. +The configuration is saved in `$XDG_CONFIG_HOME/gita/color.csv`. ### customize information displayed by the `gita ll` command You can customize the information displayed by `gita ll`. The used and unused information items are shown with `gita info`, and the -configuration is saved in `$XDG_CONFIG_HOME/gita/info.yml`. +configuration is saved in `$XDG_CONFIG_HOME/gita/info.csv`. -For example, the default information items setting corresponds to +For example, the default setting corresponds to -```yaml -- branch -- commit_msg +```csv +branch,commit_msg,commit_time ``` +### customize git command flags + +One can set custom flags to run `git` commands. For example + +``` +gita flags set my-repo --git-dir=$HOME/somefolder --work-tree=$HOME +``` + +Then any `git` command/alias triggered from `gita` on `my-repo` will use these flags. +Note that the flags are applied immediately after `git`. For example, +`gita st my-repo` translates to + +``` +git --git-dir=$HOME/somefolder --work-tree=$HOME status +``` + +running from the `my-repo` directory. + ## Requirements Gita requires Python 3.6 or higher, due to the use of @@ -249,9 +374,12 @@ To contribute, you can - request/implement features - star/recommend this project +Read [this article](https://www.dataschool.io/how-to-contribute-on-github/) if you have never contribute code to open source project before. + Chat room is available on [![Join the chat at https://gitter.im/nosarthur/gita](https://badges.gitter.im/nosarthur/gita.svg)](https://gitter.im/nosarthur/gita?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -To run tests locally, simply `pytest`. +To run tests locally, simply `pytest` in the source code folder. +Note that context should be set as `none`. More implementation details are in [design.md](https://github.com/nosarthur/gita/blob/master/doc/design.md). A step-by-step guide to reproduce this project is [here](https://nosarthur.github.io/side%20project/2019/05/27/gita-breakdown.html). diff --git a/doc/README_CN.md b/doc/README_CN.md index 80d1ccd..03d96a7 100644 --- a/doc/README_CN.md +++ b/doc/README_CN.md @@ -14,12 +14,12 @@ | | ____ | | | | | ___ | | | \_ ) | | | | | ( ) | | (___) |__) (___ | | | ) ( | -(_______)_______/ )_( |/ \| v0.12 +(_______)_______/ )_( |/ \| v0.15 ``` # Gita:一个管理多个 git 库的命令行工具 -这个工具有两个作用: +这个工具有两个功能: - 并排显示多个库的状态信息,比如分支名,编辑状态,提交信息等 - 在任何目录下(批处理)代理执行 git 指令 @@ -46,17 +46,34 @@ 基础指令: - `gita add `: 添加库 +- `gita add -a `: +- `gita add -b `: +- `gita add -m `: +- `gita add -r `: +- `gita clone `: +- `gita clone -p `: - `gita context`: 情境命令 - `gita context`: 显示当前的情境 - `gita context none`: 去除情境 - `gita context `: 把情境设置成`group-name`, 之后所有的操作只作用到这个组里的库 +- `gita color`: + - `gita color [ll]`: + - `gita color set `: +- `gita flags`: + - `gita flags set `: + - `gita flags [ll]`: +- `gita freeze`: - `gita group`: 组群命令 - `gita group add `: 把库加入新的或者已经存在的组 - `gita group [ll]`: 显示已有的组和它们的库 - `gita group ls`: 显示已有的组名 - `gita group rename `: 改组名 - `gita group rm group(s): 删除组 + - `gita group rmrepo -n : - `gita info`: 显示已用的和未用的信息项 + - `gita info [ll]` + - `gita info add ` + - `gita info rm ` - `gita ll`: 显示所有库的状态信息 - `gita ll `: 显示一个组群中库的状态信息 - `gita ls`: 显示所有库的名字 @@ -65,7 +82,7 @@ - `gita rm `: 移除库(不会删除文件) - `gita -v`: 显示版本号 -库的路径存在`$XDG_CONFIG_HOME/gita/repo_path` (多半是`~/.config/gita/repo_path`)。 +库的路径存在`$XDG_CONFIG_HOME/gita/repos.csv` (多半是`~/.config/gita/repos.csv`)。 代理执行的子命令有两种格式: @@ -150,11 +167,10 @@ comaster: help: checkout the master branch ``` 另一个自定义功能是针对`gita ll`展示的信息项。 -`gita info`可以展示所有用到的和没用到的信息项,并且可以通过修改`$XDG_CONFIG_HOME/gita/info.yml`支持自定义。举个栗子,默认的信息项显示配置相当于是: +`gita info`可以展示所有用到的和没用到的信息项,并且可以通过修改`$XDG_CONFIG_HOME/gita/info.csv`支持自定义。举个栗子,默认的信息项显示配置相当于是: -```yaml -- branch -- commit_msg +```csv +branch,commit_msg,commit_time ``` 为了创建自己的信息项,命名一个目录为`extra_info_items`。 在`$XDG_CONFIG_HOME/gita/extra_repo_info.py`中,要把信息项的名字作为字符串映射到方法中,该方法将库的路径作为输入参数。举个栗子: diff --git a/doc/screenshot.png b/doc/screenshot.png index d5941d7..df967e9 100644 Binary files a/doc/screenshot.png and b/doc/screenshot.png differ diff --git a/gita/__main__.py b/gita/__main__.py index beecab2..ee4e7e7 100644 --- a/gita/__main__.py +++ b/gita/__main__.py @@ -16,22 +16,52 @@ https://github.com/nosarthur/gita/blob/master/.gita-completion.bash import os import sys -import yaml +import csv import argparse import subprocess import pkg_resources from itertools import chain from pathlib import Path +import glob from . import utils, info, common +def _group_name(name: str) -> str: + """ + + """ + repos = utils.get_repos() + if name in repos: + print(f"Cannot use group name {name} since it's a repo name.") + sys.exit(1) + return name + + def f_add(args: argparse.Namespace): repos = utils.get_repos() paths = args.paths - if args.recursive: - paths = chain.from_iterable(Path(p).glob('**') for p in args.paths) - utils.add_repos(repos, paths) + if args.main: + # add to global and tag as main + main_repos = utils.add_repos(repos, paths, repo_type='m') + # add sub-repo recursively and save to local config + for name, prop in main_repos.items(): + main_path = prop['path'] + print('Inside main repo:', name) + #sub_paths = Path(main_path).glob('**') + sub_paths = glob.glob(os.path.join(main_path,'**/'), recursive=True) + utils.add_repos({}, sub_paths, root=main_path) + else: + if args.recursive or args.auto_group: + paths = chain.from_iterable( + glob.glob(os.path.join(p, '**/'), recursive=True) + for p in args.paths) + new_repos = utils.add_repos(repos, paths, is_bare=args.bare) + if args.auto_group: + new_groups = utils.auto_group(new_repos, args.paths) + if new_groups: + print(f'Created {len(new_groups)} new group(s).') + utils.write_to_groups_file(new_groups, 'a+') def f_rename(args: argparse.Namespace): @@ -39,16 +69,32 @@ def f_rename(args: argparse.Namespace): utils.rename_repo(repos, args.repo[0], args.new_name) +def f_flags(args: argparse.Namespace): + cmd = args.flags_cmd or 'll' + repos = utils.get_repos() + if cmd == 'll': + for r, prop in repos.items(): + if prop['flags']: + print(f"{r}: {prop['flags']}") + elif cmd == 'set': + # when in memory, flags are List[str], when on disk, they are space + # delimited str + repos[args.repo]['flags'] = args.flags + utils.write_to_repo_file(repos, 'w') + + def f_color(args: argparse.Namespace): cmd = args.color_cmd or 'll' if cmd == 'll': # pragma: no cover info.show_colors() elif cmd == 'set': colors = info.get_color_encoding() - colors[args.situation] = info.Color[args.color].value - yml_config = common.get_config_fname('color.yml') - with open(yml_config, 'w') as f: - yaml.dump(colors, f, default_flow_style=None) + colors[args.situation] = args.color + csv_config = common.get_config_fname('color.csv') + with open(csv_config, 'w', newline='') as f: + writer = csv.DictWriter(f, fieldnames=colors) + writer.writeheader() + writer.writerow(colors) def f_info(args: argparse.Namespace): @@ -56,37 +102,53 @@ def f_info(args: argparse.Namespace): cmd = args.info_cmd or 'll' if cmd == 'll': print('In use:', ','.join(to_display)) - unused = set(info.ALL_INFO_ITEMS) - set(to_display) + unused = sorted(list(set(info.ALL_INFO_ITEMS) - set(to_display))) if unused: - print('Unused:', ' '.join(unused)) + print('Unused:', ','.join(unused)) return if cmd == 'add' and args.info_item not in to_display: to_display.append(args.info_item) - yml_config = common.get_config_fname('info.yml') - with open(yml_config, 'w') as f: - yaml.dump(to_display, f, default_flow_style=None) + csv_config = common.get_config_fname('info.csv') + with open(csv_config, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(to_display) elif cmd == 'rm' and args.info_item in to_display: to_display.remove(args.info_item) - yml_config = common.get_config_fname('info.yml') - with open(yml_config, 'w') as f: - yaml.dump(to_display, f, default_flow_style=None) + csv_config = common.get_config_fname('info.csv') + with open(csv_config, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(to_display) def f_clone(args: argparse.Namespace): path = Path.cwd() - errors = utils.exec_async_tasks( + if args.preserve_path: + utils.exec_async_tasks( + utils.run_async(repo_name, path, ['git', 'clone', url, abs_path]) + for url, repo_name, abs_path in utils.parse_clone_config(args.fname)) + else: + utils.exec_async_tasks( utils.run_async(repo_name, path, ['git', 'clone', url]) for url, repo_name, _ in utils.parse_clone_config(args.fname)) def f_freeze(_): repos = utils.get_repos() - for name, path in repos.items(): + seen = {''} + for name, prop in repos.items(): + path = prop['path'] + # TODO: What do we do with main repos? Maybe give an option to print + # their sub-repos too. url = '' cp = subprocess.run(['git', 'remote', '-v'], cwd=path, capture_output=True) - if cp.returncode == 0: - url = cp.stdout.decode('utf-8').split('\n')[0].split()[1] - print(f'{url},{name},{path}') + lines = cp.stdout.decode('utf-8').split('\n') + if cp.returncode == 0 and len(lines) > 0: + parts = lines[0].split() + if len(parts)>1: + url = parts[1] + if url not in seen: + seen.add(url) + print(f'{url},{name},{path}') def f_ll(args: argparse.Namespace): @@ -107,7 +169,7 @@ def f_ll(args: argparse.Namespace): def f_ls(args: argparse.Namespace): repos = utils.get_repos() if args.repo: # one repo, show its path - print(repos[args.repo]) + print(repos[args.repo]['path']) else: # show names of all repos print(' '.join(repos)) @@ -128,6 +190,11 @@ def f_group(args: argparse.Namespace): groups[new_name] = groups[gname] del groups[gname] utils.write_to_groups_file(groups, 'w') + # change context + ctx = utils.get_context() + if ctx and str(ctx.stem) == gname: + # ctx.rename(ctx.with_stem(new_name)) # only works in py3.9 + ctx.rename(ctx.with_name(f'{new_name}.context')) elif cmd == 'rm': ctx = utils.get_context() for name in args.to_ungroup: @@ -178,12 +245,22 @@ def f_rm(args: argparse.Namespace): """ Unregister repo(s) from gita """ - path_file = common.get_config_fname('repo_path') + path_file = common.get_config_fname('repos.csv') if os.path.isfile(path_file): repos = utils.get_repos() + main_paths = [prop['path'] for prop in repos.values() if prop['type'] == 'm'] + # TODO: add test case to delete main repo from main repo + # only local setting should be affected instead of the global one for repo in args.repo: del repos[repo] - utils.write_to_repo_file(repos, 'w') + # If cwd is relative to any main repo, write to local config + cwd = os.getcwd() + for p in main_paths: + if utils.is_relative_to(cwd, p): + utils.write_to_repo_file(repos, 'w', p) + break + else: # global config + utils.write_to_repo_file(repos, 'w') def f_git_cmd(args: argparse.Namespace): @@ -205,21 +282,33 @@ def f_git_cmd(args: argparse.Namespace): for r in groups[k]: chosen[r] = repos[r] repos = chosen - cmds = ['git'] + args.cmd - if len(repos) == 1 or cmds[1] in args.async_blacklist: - for path in repos.values(): + per_repo_cmds = [] + for prop in repos.values(): + cmds = args.cmd.copy() + if cmds[0] == 'git' and prop['flags']: + cmds[1:1] = prop['flags'] + per_repo_cmds.append(cmds) + + # This async blacklist mechanism is broken if the git command name does + # not match with the gita command name. + if len(repos) == 1 or args.cmd[1] in args.async_blacklist: + for prop, cmds in zip(repos.values(), per_repo_cmds): + path = prop['path'] print(path) - subprocess.run(cmds, cwd=path) + subprocess.run(cmds, cwd=path, shell=args.shell) else: # run concurrent subprocesses # Async execution cannot deal with multiple repos' user name/password. # Here we shut off any user input in the async execution, and re-run # the failed ones synchronously. errors = utils.exec_async_tasks( - utils.run_async(repo_name, path, cmds) for repo_name, path in repos.items()) + utils.run_async(repo_name, prop['path'], cmds) + for cmds, (repo_name, prop) in zip(per_repo_cmds, repos.items())) for path in errors: if path: print(path) - subprocess.run(cmds, cwd=path) + # FIXME: This is broken, flags are missing. But probably few + # people will use `gita flags` + subprocess.run(args.cmd, cwd=path) def f_shell(args): @@ -249,10 +338,10 @@ def f_shell(args): for r in groups[k]: chosen[r] = repos[r] repos = chosen - cmds = args.man[i:] - for name, path in repos.items(): + cmds = ' '.join(args.man[i:]) # join the shell command into a single string + for name, prop in repos.items(): # TODO: pull this out as a function - got = subprocess.run(cmds, cwd=path, check=True, + got = subprocess.run(cmds, cwd=prop['path'], check=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) print(utils.format_output(got.stdout.decode(), name)) @@ -271,8 +360,9 @@ def f_super(args): names.append(word) else: break - args.cmd = args.man[i:] + args.cmd = ['git'] + args.man[i:] args.repo = names + args.shell = False f_git_cmd(args) @@ -292,9 +382,17 @@ def main(argv=None): # bookkeeping sub-commands p_add = subparsers.add_parser('add', description='add repo(s)', help='add repo(s)') - p_add.add_argument('paths', nargs='+', help="repo(s) to add") - p_add.add_argument('-r', dest='recursive', action='store_true', - help="recursively add repo(s) in the given path.") + p_add.add_argument('paths', nargs='+', type=os.path.abspath, help="repo(s) to add") + xgroup = p_add.add_mutually_exclusive_group() + xgroup.add_argument('-r', '--recursive', action='store_true', + help="recursively add repo(s) in the given path(s).") + xgroup.add_argument('-m', '--main', action='store_true', + help="make main repo(s), sub-repos are recursively added.") + xgroup.add_argument('-a', '--auto-group', action='store_true', + help="recursively add repo(s) in the given path(s) " + "and create hierarchical groups based on folder structure.") + xgroup.add_argument('-b', '--bare', action='store_true', + help="add bare repo(s)") p_add.set_defaults(func=f_add) p_rm = subparsers.add_parser('rm', description='remove repo(s)', @@ -305,15 +403,22 @@ def main(argv=None): help="remove the chosen repo(s)") p_rm.set_defaults(func=f_rm) - p_freeze = subparsers.add_parser('freeze', description='print all repo information') + p_freeze = subparsers.add_parser('freeze', + description='print all repo information', + help='print all repo information') p_freeze.set_defaults(func=f_freeze) - p_clone = subparsers.add_parser('clone', description='clone repos from config file') + p_clone = subparsers.add_parser('clone', + description='clone repos from config file', + help='clone repos from config file') p_clone.add_argument('fname', help='config file. Its content should be the output of `gita freeze`.') + p_clone.add_argument('-p', '--preserve-path', dest='preserve_path', action='store_true', + help="clone repo(s) in their original paths") p_clone.set_defaults(func=f_clone) - p_rename = subparsers.add_parser('rename', description='rename a repo') + p_rename = subparsers.add_parser('rename', description='rename a repo', + help='rename a repo') p_rename.add_argument( 'repo', nargs=1, @@ -322,8 +427,25 @@ def main(argv=None): p_rename.add_argument('new_name', help="new name") p_rename.set_defaults(func=f_rename) + p_flags = subparsers.add_parser('flags', + description='Set custom git flags for repo.', + help='git flags configuration') + p_flags.set_defaults(func=f_flags) + flags_cmds = p_flags.add_subparsers(dest='flags_cmd', + help='additional help with sub-command -h') + flags_cmds.add_parser('ll', + description='display repos with custom flags') + pf_set = flags_cmds.add_parser('set', + description='Set flags for repo.') + pf_set.add_argument('repo', choices=utils.get_repos(), + help="repo name") + pf_set.add_argument('flags', + nargs=argparse.REMAINDER, + help="custom flags, use quotes") + p_color = subparsers.add_parser('color', - description='display and modify branch coloring of the ll sub-command.') + description='display and modify branch coloring of the ll sub-command.', + help='color configuration') p_color.set_defaults(func=f_color) color_cmds = p_color.add_subparsers(dest='color_cmd', help='additional help with sub-command -h') @@ -339,7 +461,8 @@ def main(argv=None): help="available colors") p_info = subparsers.add_parser('info', - description='list, add, or remove information items of the ll sub-command.') + description='list, add, or remove information items of the ll sub-command.', + help='information setting') p_info.set_defaults(func=f_info) info_cmds = p_info.add_subparsers(dest='info_cmd', help='additional help with sub-command -h') @@ -347,11 +470,11 @@ def main(argv=None): description='show used and unused information items of the ll sub-command') info_cmds.add_parser('add', description='Enable information item.' ).add_argument('info_item', - choices=('branch', 'commit_msg', 'path'), + choices=info.ALL_INFO_ITEMS, help="information item to add") info_cmds.add_parser('rm', description='Disable information item.' ).add_argument('info_item', - choices=('branch', 'commit_msg', 'path'), + choices=info.ALL_INFO_ITEMS, help="information item to delete") @@ -379,6 +502,7 @@ def main(argv=None): p_ll.set_defaults(func=f_ll) p_context = subparsers.add_parser('context', + help='set context', description='Set and remove context. A context is a group.' ' When set, all operations apply only to repos in that group.') p_context.add_argument('choice', @@ -388,7 +512,8 @@ def main(argv=None): p_context.set_defaults(func=f_context) p_ls = subparsers.add_parser( - 'ls', description='display names of all repos, or path of a chosen repo') + 'ls', help='show repo(s) or repo path', + description='display names of all repos, or path of a chosen repo') p_ls.add_argument('repo', nargs='?', choices=utils.get_repos(), @@ -396,7 +521,8 @@ def main(argv=None): p_ls.set_defaults(func=f_ls) p_group = subparsers.add_parser( - 'group', description='list, add, or remove repo group(s)') + 'group', description='list, add, or remove repo group(s)', + help='group repos') p_group.set_defaults(func=f_group) group_cmds = p_group.add_subparsers(dest='group_cmd', help='additional help with sub-command -h') @@ -410,6 +536,7 @@ def main(argv=None): help="repo(s) to be grouped") pg_add.add_argument('-n', '--name', dest='gname', + type=_group_name, metavar='group-name', required=True, help="group name") @@ -429,6 +556,7 @@ def main(argv=None): choices=utils.get_groups(), help="existing group to rename") pg_rename.add_argument('new_name', metavar='new-name', + type=_group_name, help="new group name") group_cmds.add_parser('rm', description='Remove group(s).').add_argument('to_ungroup', @@ -439,6 +567,7 @@ def main(argv=None): # superman mode p_super = subparsers.add_parser( 'super', + help='run any git command/alias', description='Superman mode: delegate any git command/alias in specified or ' 'all repo(s).\n' 'Examples:\n \t gita super myrepo1 commit -am "fix a bug"\n' @@ -446,7 +575,7 @@ def main(argv=None): p_super.add_argument( 'man', nargs=argparse.REMAINDER, - help="execute arbitrary git command/alias for specified or all repos " + help="execute arbitrary git command/alias for specified or all repos\n" "Example: gita super myrepo1 diff --name-only --staged " "Another: gita super checkout master ") p_super.set_defaults(func=f_super) @@ -454,6 +583,7 @@ def main(argv=None): # shell mode p_shell = subparsers.add_parser( 'shell', + help='run any shell command', description='shell mode: delegate any shell command in specified or ' 'all repo(s).\n' 'Examples:\n \t gita shell pwd\n' @@ -470,7 +600,7 @@ def main(argv=None): cmds = utils.get_cmds_from_files() for name, data in cmds.items(): help = data.get('help') - cmd = data.get('cmd') or name + cmd = data['cmd'] if data.get('allow_all'): choices = utils.get_choices() nargs = '*' @@ -481,7 +611,14 @@ def main(argv=None): help += ' for the chosen repo(s) or group(s)' sp = subparsers.add_parser(name, description=help) sp.add_argument('repo', nargs=nargs, choices=choices, help=help) - sp.set_defaults(func=f_git_cmd, cmd=cmd.split()) + is_shell = bool(data.get('shell')) + sp.add_argument('-s', '--shell', default=is_shell, type=bool, + help='If set, run in shell mode') + if is_shell: + cmd = [cmd] + else: + cmd = cmd.split() + sp.set_defaults(func=f_git_cmd, cmd=cmd) args = p.parse_args(argv) diff --git a/gita/cmds.json b/gita/cmds.json new file mode 100644 index 0000000..947a4dd --- /dev/null +++ b/gita/cmds.json @@ -0,0 +1,89 @@ +{ +"br":{ + "cmd": "git branch -vv", + "help":"show local branches"}, +"clean":{ + "cmd": "git clean -dfx", + "help": "remove all untracked files/folders"}, +"diff":{ + "cmd": "git diff", + "help": "git show differences"}, +"difftool":{ + "cmd": "git difftool", + "disable_async": true, + "help": "show differences using a tool" + }, +"fetch":{ + "cmd": "git fetch", + "allow_all": true, + "help": "fetch remote update" + }, +"last":{ + "cmd": "git log -1 HEAD", + "help": "show log information of HEAD" + }, +"log": + {"cmd": "git log", + "disable_async": true, + "help": "show logs" + }, +"merge":{ + "cmd": "git merge @{u}", + "help": "merge remote updates" + }, +"mergetool":{ + "cmd": "git mergetool", + "disable_async": true, + "help": "merge updates with a tool" + }, +"patch":{ + "cmd": "git format-patch HEAD~", + "help": "make a patch" + }, +"pull":{ + "cmd": "git pull", + "allow_all": true, + "help": "pull remote updates" + }, +"push":{ + "cmd": "git push", + "help": "push the local updates" + }, +"rebase":{ + "cmd": "git rebase", + "help": "rebase from master" + }, +"reflog":{ + "cmd": "git reflog", + "help": "show ref logs" + }, +"remote":{ + "cmd": "git remote -v", + "help": "show remote settings" + }, +"reset":{ + "cmd": "git reset", + "help": "reset repo(s)" + }, +"show":{ + "cmd": "git show", + "disable_async": true, + "help": "show detailed commit information" + }, +"stash":{ + "cmd": "git stash", + "help": "store uncommited changes" + }, +"stat":{ + "cmd": "git diff --stat", + "help": "show edit statistics" + }, +"st":{ + "cmd": "git status", + "help": "show status" + }, +"tag":{ + "cmd": "git tag -n", + "help": "show tags" + } +} diff --git a/gita/cmds.yml b/gita/cmds.yml deleted file mode 100644 index 8db932e..0000000 --- a/gita/cmds.yml +++ /dev/null @@ -1,65 +0,0 @@ -br: - cmd: branch -vv - help: show local branches -clean: - cmd: clean -dfx - help: remove all untracked files/folders -diff: - help: show differences -difftool: - disable_async: true - help: show differences using a tool -fetch: - allow_all: true - help: fetch remote update -last: - cmd: log -1 HEAD - help: show log information of HEAD -log: - disable_async: true - help: show logs -merge: - cmd: merge @{u} - help: merge remote updates -mergetool: - disable_async: true - help: merge updates with a tool -patch: - cmd: format-patch HEAD~ - help: make a patch -pull: - allow_all: true - help: pull remote updates -push: - help: push the local updates -rebase: - help: rebase from master -reflog: - help: show ref logs -remote: - cmd: remote -v - help: show remote settings -reset: - help: reset repo(s) -shortlog: - disable_async: true - help: show short log -show: - disable_async: true - help: show detailed commit information -show-branch: - disable_async: true - help: show detailed branch information -stash: - help: store uncommited changes -stat: - cmd: diff --stat - help: show edit statistics -st: - help: show status -tag: - cmd: tag -n - help: show tags -whatchanged: - disable_async: true - help: show detailed log diff --git a/gita/common.py b/gita/common.py index ef3933d..abbef5f 100644 --- a/gita/common.py +++ b/gita/common.py @@ -1,16 +1,17 @@ import os -def get_config_dir() -> str: - parent = os.environ.get('XDG_CONFIG_HOME') or os.path.join( - os.path.expanduser('~'), '.config') - root = os.path.join(parent, "gita") - return root +def get_config_dir(root=None) -> str: + if root is None: + root = os.environ.get('XDG_CONFIG_HOME') or os.path.join( + os.path.expanduser('~'), '.config') + return os.path.join(root, "gita") + else: + return os.path.join(root, ".gita") -def get_config_fname(fname: str) -> str: +def get_config_fname(fname: str, root=None) -> str: """ Return the file name that stores the repo locations. """ - root = get_config_dir() - return os.path.join(root, fname) + return os.path.join(get_config_dir(root), fname) diff --git a/gita/info.py b/gita/info.py index a8044e9..d18a097 100644 --- a/gita/info.py +++ b/gita/info.py @@ -1,5 +1,5 @@ import os -import sys +import csv import yaml import subprocess from enum import Enum @@ -31,40 +31,42 @@ class Color(str, Enum): b_purple = '\x1b[35;1m' b_cyan = '\x1b[36;1m' b_white = '\x1b[37;1m' + underline = '\x1B[4m' def show_colors(): # pragma: no cover """ """ - names = {c.value: c.name for c in Color} for i, c in enumerate(Color, start=1): - if c != Color.end: + if c != Color.end and c != Color.underline: print(f'{c.value}{c.name:<8} ', end='') if i % 9 == 0: print() print(f'{Color.end}') for situation, c in sorted(get_color_encoding().items()): - print(f'{situation:<12}: {c}{names[c]:<8}{Color.end} ') + print(f'{situation:<12}: {Color[c].value}{c:<8}{Color.end} ') @lru_cache() def get_color_encoding() -> Dict[str, str]: """ Return color scheme for different local/remote situations. + In the format of {situation: color name} """ # custom settings - yml_config = Path(common.get_config_fname('color.yml')) - if yml_config.is_file(): - with open(yml_config, 'r') as stream: - colors = yaml.load(stream, Loader=yaml.FullLoader) + csv_config = Path(common.get_config_fname('color.csv')) + if csv_config.is_file(): + with open(csv_config, 'r') as f: + reader = csv.DictReader(f) + colors = next(reader) else: colors = { - 'no-remote': Color.white.value, - 'in-sync': Color.green.value, - 'diverged': Color.red.value, - 'local-ahead': Color.purple.value, - 'remote-ahead': Color.yellow.value, + 'no-remote': Color.white.name, + 'in-sync': Color.green.name, + 'diverged': Color.red.name, + 'local-ahead': Color.purple.name, + 'remote-ahead': Color.yellow.name, } return colors @@ -80,6 +82,7 @@ def get_info_funcs() -> List[Callable[[str], str]]: all_info_items = { 'branch': get_repo_status, 'commit_msg': get_commit_msg, + 'commit_time': get_commit_time, 'path': get_path, } return [all_info_items[k] for k in to_display] @@ -90,23 +93,26 @@ def get_info_items() -> List[str]: Return the information items to be displayed in the `gita ll` command. """ # custom settings - yml_config = Path(common.get_config_fname('info.yml')) - if yml_config.is_file(): - with open(yml_config, 'r') as stream: - display_items = yaml.load(stream, Loader=yaml.FullLoader) + csv_config = Path(common.get_config_fname('info.csv')) + if csv_config.is_file(): + with open(csv_config, 'r') as f: + reader = csv.reader(f) + display_items = next(reader) display_items = [x for x in display_items if x in ALL_INFO_ITEMS] else: # default settings - display_items = ['branch', 'commit_msg'] + display_items = ['branch', 'commit_msg', 'commit_time'] return display_items -def get_path(path): - return f'{Color.cyan}{path}{Color.end}' +def get_path(prop: Dict[str, str]) -> str: + return f'{Color.cyan}{prop["path"]}{Color.end}' +# TODO: do we need to add the flags here too? def get_head(path: str) -> str: - result = subprocess.run('git rev-parse --abbrev-ref HEAD'.split(), + result = subprocess.run('git symbolic-ref -q --short HEAD || git describe --tags --exact-match', + shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, universal_newlines=True, @@ -114,12 +120,12 @@ def get_head(path: str) -> str: return result.stdout.strip() -def run_quiet_diff(args: List[str]) -> bool: +def run_quiet_diff(flags: List[str], args: List[str]) -> int: """ Return the return code of git diff `args` in quiet mode """ result = subprocess.run( - ['git', 'diff', '--quiet'] + args, + ['git'] + flags + ['diff', '--quiet'] + args, stderr=subprocess.DEVNULL, ) return result.returncode @@ -135,50 +141,68 @@ def get_common_commit() -> str: return result.stdout.strip() -def has_untracked() -> bool: +def has_untracked(flags: List[str]) -> bool: """ Return True if untracked file/folder exists """ - result = subprocess.run('git ls-files -zo --exclude-standard'.split(), + cmd = ['git'] + flags + 'ls-files -zo --exclude-standard'.split() + result = subprocess.run(cmd, stdout=subprocess.PIPE) return bool(result.stdout) -def get_commit_msg(path: str) -> str: +def get_commit_msg(prop: Dict[str, str]) -> str: """ Return the last commit message. """ # `git show-branch --no-name HEAD` is faster than `git show -s --format=%s` - result = subprocess.run('git show-branch --no-name HEAD'.split(), + cmd = ['git'] + prop['flags'] + 'show-branch --no-name HEAD'.split() + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, universal_newlines=True, - cwd=path) + cwd=prop['path']) return result.stdout.strip() -def get_repo_status(path: str, no_colors=False) -> str: - head = get_head(path) - dirty, staged, untracked, color = _get_repo_status(path, no_colors) +def get_commit_time(prop: Dict[str, str]) -> str: + """ + Return the last commit time in parenthesis. + """ + cmd = ['git'] + prop['flags'] + 'log -1 --format=%cd --date=relative'.split() + result = subprocess.run(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + universal_newlines=True, + cwd=prop['path']) + return f"({result.stdout.strip()})" + + +def get_repo_status(prop: Dict[str, str], no_colors=False) -> str: + head = get_head(prop['path']) + dirty, staged, untracked, color = _get_repo_status(prop, no_colors) if color: return f'{color}{head+" "+dirty+staged+untracked:<10}{Color.end}' return f'{head+" "+dirty+staged+untracked:<10}' -def _get_repo_status(path: str, no_colors: bool) -> Tuple[str]: +def _get_repo_status(prop: Dict[str, str], no_colors: bool) -> Tuple[str]: """ Return the status of one repo """ + path = prop['path'] + flags = prop['flags'] os.chdir(path) - dirty = '*' if run_quiet_diff([]) else '' - staged = '+' if run_quiet_diff(['--cached']) else '' - untracked = '_' if has_untracked() else '' + dirty = '*' if run_quiet_diff(flags, []) else '' + staged = '+' if run_quiet_diff(flags, ['--cached']) else '' + untracked = '_' if has_untracked(flags) else '' if no_colors: return dirty, staged, untracked, '' - colors = get_color_encoding() - diff_returncode = run_quiet_diff(['@{u}', '@{0}']) + colors = {situ: Color[name].value + for situ, name in get_color_encoding().items()} + diff_returncode = run_quiet_diff(flags, ['@{u}', '@{0}']) has_no_remote = diff_returncode == 128 has_no_diff = diff_returncode == 0 if has_no_remote: @@ -187,9 +211,9 @@ def _get_repo_status(path: str, no_colors: bool) -> Tuple[str]: color = colors['in-sync'] else: common_commit = get_common_commit() - outdated = run_quiet_diff(['@{u}', common_commit]) + outdated = run_quiet_diff(flags, ['@{u}', common_commit]) if outdated: - diverged = run_quiet_diff(['@{0}', common_commit]) + diverged = run_quiet_diff(flags, ['@{0}', common_commit]) color = colors['diverged'] if diverged else colors['remote-ahead'] else: # local is ahead of remote color = colors['local-ahead'] @@ -199,5 +223,6 @@ def _get_repo_status(path: str, no_colors: bool) -> Tuple[str]: ALL_INFO_ITEMS = { 'branch': get_repo_status, 'commit_msg': get_commit_msg, + 'commit_time': get_commit_time, 'path': get_path, } diff --git a/gita/utils.py b/gita/utils.py index 9572f02..34fc435 100644 --- a/gita/utils.py +++ b/gita/utils.py @@ -1,62 +1,76 @@ import os -import yaml +import json +import csv import asyncio import platform +import subprocess from functools import lru_cache, partial from pathlib import Path -from typing import List, Dict, Coroutine, Union, Iterator +from typing import List, Dict, Coroutine, Union, Iterator, Tuple +from collections import Counter, defaultdict from . import info from . import common -@lru_cache() -def get_context() -> Union[Path, None]: +# TODO: python3.9 pathlib has is_relative_to() function +def is_relative_to(kid: str, parent: str) -> bool: """ - Return the context: either a group name or 'none' + Both the `kid` and `parent` should be absolute path """ - config_dir = Path(common.get_config_dir()) - matches = list(config_dir.glob('*.context')) - assert len(matches) < 2, "Cannot have multiple .context file" - return matches[0] if matches else None + return parent == os.path.commonpath((kid, parent)) @lru_cache() -def get_repos() -> Dict[str, str]: +def get_repos(root=None) -> Dict[str, Dict[str, str]]: """ - Return a `dict` of repo name to repo absolute path + Return a `dict` of repo name to repo absolute path and repo type + + @param root: Use local config if set. If None, use either global or local + config depending on cwd. """ - path_file = common.get_config_fname('repo_path') + path_file = common.get_config_fname('repos.csv', root) repos = {} - # Each line is a repo path and repo name separated by , if os.path.isfile(path_file) and os.stat(path_file).st_size > 0: with open(path_file) as f: - for line in f: - line = line.rstrip() - if not line: # blank line - continue - path, name = line.split(',') - if not is_git(path): - continue - if name not in repos: - repos[name] = path - else: # repo name collision for different paths: include parent path name - par_name = os.path.basename(os.path.dirname(path)) - repos[os.path.join(par_name, name)] = path + rows = csv.DictReader(f, ['path', 'name', 'type', 'flags'], + restval='') # it's actually a reader + repos = {r['name']: + {'path': r['path'], 'type': r['type'], + 'flags': r['flags'].split()} + for r in rows if is_git(r['path'], is_bare=True)} + if root is None: # detect if inside a main path + cwd = os.getcwd() + for prop in repos.values(): + path = prop['path'] + if prop['type'] == 'm' and is_relative_to(cwd, path): + return get_repos(path) return repos +@lru_cache() +def get_context() -> Union[Path, None]: + """ + Return the context: either a group name or 'none' + """ + config_dir = Path(common.get_config_dir()) + matches = list(config_dir.glob('*.context')) + assert len(matches) < 2, "Cannot have multiple .context file" + return matches[0] if matches else None + + @lru_cache() def get_groups() -> Dict[str, List[str]]: """ Return a `dict` of group name to repo names. """ - fname = common.get_config_fname('groups.yml') + fname = common.get_config_fname('groups.csv') groups = {} # Each line is a repo path and repo name separated by , if os.path.isfile(fname) and os.stat(fname).st_size > 0: with open(fname, 'r') as f: - groups = yaml.load(f, Loader=yaml.FullLoader) + rows = csv.reader(f, delimiter=':') + groups = {r[0]: r[1].split() for r in rows} return groups @@ -75,10 +89,12 @@ def get_choices() -> List[Union[str, None]]: return choices -def is_git(path: str) -> bool: +def is_git(path: str, is_bare=False) -> bool: """ Return True if the path is a git repo. """ + if not os.path.exists(path): + return False # An alternative is to call `git rev-parse --is-inside-work-tree` # I don't see why that one is better yet. # For a regular git repo, .git is a folder, for a worktree repo, .git is a file. @@ -88,59 +104,172 @@ def is_git(path: str) -> bool: # `git rev-parse --git-common-dir` loc = os.path.join(path, '.git') # TODO: we can display the worktree repos in a different font. - return os.path.exists(loc) - - -def rename_repo(repos: Dict[str, str], repo: str, new_name: str): + if os.path.exists(loc): + return True + if not is_bare: + return False + # detect bare repo + got = subprocess.run('git rev-parse --is-bare-repository'.split(), + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + cwd=path + ) + if got.returncode == 0 and got.stdout == b'true\n': + return True + return False + +def rename_repo(repos: Dict[str, Dict[str, str]], repo: str, new_name: str): """ Write new repo name to file """ - path = repos[repo] + if new_name in repos: + print(f"{new_name} is already in use!") + return + prop = repos[repo] del repos[repo] - repos[new_name] = path - write_to_repo_file(repos, 'w') - - -def write_to_repo_file(repos: Dict[str, str], mode: str): + repos[new_name] = prop + # write to local config if inside a main path + main_paths = (prop['path'] for prop in repos.values() if prop['type'] == 'm') + cwd = os.getcwd() + is_local_config = True + for p in main_paths: + if is_relative_to(cwd, p): + write_to_repo_file(repos, 'w', p) + break + else: # global config + write_to_repo_file(repos, 'w') + is_local_config = False + # update groups only when outside any main repos + if is_local_config: + return + groups = get_groups() + for g, members in groups.items(): + if repo in members: + members.remove(repo) + members.append(new_name) + groups[g] = sorted(members) + write_to_groups_file(groups, 'w') + + +def write_to_repo_file(repos: Dict[str, Dict[str, str]], mode: str, root=None): """ + @param repos: each repo is {name: {properties}} """ - data = ''.join(f'{path},{name}\n' for name, path in repos.items()) - fname = common.get_config_fname('repo_path') + data = [(prop['path'], name, prop['type'], ' '.join(prop['flags'])) + for name, prop in repos.items()] + fname = common.get_config_fname('repos.csv', root) os.makedirs(os.path.dirname(fname), exist_ok=True) - with open(fname, mode) as f: - f.write(data) + with open(fname, mode, newline='') as f: + writer = csv.writer(f, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + writer.writerows(data) def write_to_groups_file(groups: Dict[str, List[str]], mode: str): """ """ - fname = common.get_config_fname('groups.yml') + fname = common.get_config_fname('groups.csv') os.makedirs(os.path.dirname(fname), exist_ok=True) if not groups: # all groups are deleted open(fname, 'w').close() else: - with open(fname, mode) as f: - yaml.dump(groups, f, default_flow_style=None) + with open(fname, mode, newline='') as f: + data = [ + (group, ' '.join(repos)) + for group, repos in groups.items() + ] + writer = csv.writer(f, delimiter=':', quotechar='"', quoting=csv.QUOTE_MINIMAL) + writer.writerows(data) + + +def _make_name(path: str, repos: Dict[str, Dict[str, str]], + name_counts: Counter) -> str: + """ + Given a new repo `path`, create a repo name. By default, basename is used. + If name collision exists, further include parent path name. + + @param path: It should not be in `repos` and is absolute + """ + name = os.path.basename(os.path.normpath(path)) + if name in repos or name_counts[name] > 1: + par_name = os.path.basename(os.path.dirname(path)) + return os.path.join(par_name, name) + return name -def add_repos(repos: Dict[str, str], new_paths: List[str]): +def _get_repo_type(path, repo_type, root) -> str: """ - Write new repo paths to file + + """ + if repo_type != '': # explicitly set + return repo_type + if root is not None and os.path.normpath(root) == os.path.normpath(path): + return 'm' + return '' + + +def add_repos(repos: Dict[str, Dict[str, str]], new_paths: List[str], + repo_type='', root=None, is_bare=False) -> Dict[str, Dict[str, str]]: + """ + Write new repo paths to file; return the added repos. @param repos: name -> path """ - existing_paths = set(repos.values()) - new_paths = set(os.path.abspath(p) for p in new_paths if is_git(p)) + existing_paths = {prop['path'] for prop in repos.values()} + new_paths = {p for p in new_paths if is_git(p, is_bare)} new_paths = new_paths - existing_paths + new_repos = {} if new_paths: print(f"Found {len(new_paths)} new repo(s).") - new_repos = { - os.path.basename(os.path.normpath(path)): path - for path in new_paths} - write_to_repo_file(new_repos, 'a+') + name_counts = Counter( + os.path.basename(os.path.normpath(p)) for p in new_paths + ) + new_repos = {_make_name(path, repos, name_counts): { + 'path': path, + 'type': _get_repo_type(path, repo_type, root), + 'flags': '', + } for path in new_paths} + # When root is not None, we could optionally set its type to 'm', i.e., + # main repo. + write_to_repo_file(new_repos, 'a+', root) else: print('No new repos found!') + return new_repos + + +def _generate_dir_hash(repo_path: str, paths: List[str]) -> Tuple[str, ...]: + """ + Return relative parent strings + + For example, if `repo_path` is /a/b/c/d/here, and one of `paths` is /a/b/ + then return (b, c, d) + """ + for p in paths: + if is_relative_to(repo_path, p): + break + else: + return () + return (os.path.basename(p), + *os.path.normpath(os.path.relpath(repo_path, p)).split(os.sep)[:-1]) + + +def auto_group(repos: Dict[str, Dict[str, str]], paths: List[str] + ) -> Dict[str, List[str]]: + """ + + """ + # FIXME: the upstream code should make sure that paths are all independent + # i.e., each repo should be contained in one and only one path + new_groups = defaultdict(list) + for repo_name, prop in repos.items(): + hash = _generate_dir_hash(prop['path'], paths) + if not hash: + continue + for i in range(1, len(hash)+1): + group_name = '-'.join(hash[:i]) + new_groups[group_name].append(repo_name) + # FIXME: need to make sure the new group names don't clash with old ones + # or repo names + return new_groups def parse_clone_config(fname: str) -> Iterator[List[str]]: @@ -157,6 +286,7 @@ async def run_async(repo_name: str, path: str, cmds: List[str]) -> Union[None, s Run `cmds` asynchronously in `path` directory. Return the `path` if execution fails. """ + # TODO: deprecated since 3.8, will be removed in 3.10 process = await asyncio.create_subprocess_exec( *cmds, stdin=asyncio.subprocess.DEVNULL, @@ -199,7 +329,7 @@ def exec_async_tasks(tasks: List[Coroutine]) -> List[Union[None, str]]: return errors -def describe(repos: Dict[str, str], no_colors: bool=False) -> str: +def describe(repos: Dict[str, Dict[str, str]], no_colors: bool = False) -> str: """ Return the status of all repos """ @@ -213,9 +343,14 @@ def describe(repos: Dict[str, str], no_colors: bool=False) -> str: funcs[idx] = partial(get_repo_status, no_colors=True) for name in sorted(repos): - path = repos[name] - info_items = ' '.join(f(path) for f in funcs) - yield f'{name:<{name_width}}{info_items}' + info_items = ' '.join(f(repos[name]) for f in funcs) + if repos[name]['type'] == 'm': + # ANSI color code also takes length in Python + name = f'{info.Color.underline}{name}{info.Color.end}' + width = name_width + 8 + yield f'{name:<{width}}{info_items}' + else: + yield f'{name:<{name_width}}{info_items}' def get_cmds_from_files() -> Dict[str, Dict[str, str]]: @@ -231,17 +366,17 @@ def get_cmds_from_files() -> Dict[str, Dict[str, str]]: } """ # default config file - fname = os.path.join(os.path.dirname(__file__), "cmds.yml") - with open(fname, 'r') as stream: - cmds = yaml.load(stream, Loader=yaml.FullLoader) + fname = os.path.join(os.path.dirname(__file__), "cmds.json") + with open(fname, 'r') as f: + cmds = json.load(f) # custom config file root = common.get_config_dir() - fname = os.path.join(root, 'cmds.yml') + fname = os.path.join(root, 'cmds.json') custom_cmds = {} if os.path.isfile(fname) and os.path.getsize(fname): - with open(fname, 'r') as stream: - custom_cmds = yaml.load(stream, Loader=yaml.FullLoader) + with open(fname, 'r') as f: + custom_cmds = json.load(f) # custom commands shadow default ones cmds.update(custom_cmds) diff --git a/setup.py b/setup.py index c729963..6fb92eb 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open('README.md', encoding='utf-8') as f: setup( name='gita', packages=['gita'], - version='0.12.7', + version='0.15.1', license='MIT', description='Manage multiple git repos with sanity', long_description=long_description, @@ -18,7 +18,6 @@ setup( author='Dong Zhou', author_email='zhou.dong@gmail.com', entry_points={'console_scripts': ['gita = gita.__main__:main']}, - install_requires=['pyyaml>=5.1'], python_requires='~=3.6', classifiers=[ "Development Status :: 4 - Beta", diff --git a/tests/clash_path_file b/tests/clash_path_file index 4abbfca..33eeae2 100644 --- a/tests/clash_path_file +++ b/tests/clash_path_file @@ -1,3 +1,3 @@ -/a/bcd/repo1,repo1 -/e/fgh/repo2,repo2 +/a/bcd/repo1,repo1, +/e/fgh/repo2,repo2,,--haha --pp /root/x/repo1,repo1 diff --git a/tests/conftest.py b/tests/conftest.py index b3e59ed..93576d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ def fullpath(fname: str): PATH_FNAME = fullpath('mock_path_file') PATH_FNAME_EMPTY = fullpath('empty_path_file') PATH_FNAME_CLASH = fullpath('clash_path_file') +PATH_FNAME_MAIN = fullpath('main_path_file') GROUP_FNAME = fullpath('mock_group_file') def async_mock(): diff --git a/tests/main_path_file b/tests/main_path_file new file mode 100644 index 0000000..8e87ffb --- /dev/null +++ b/tests/main_path_file @@ -0,0 +1,2 @@ +/path/to/main/,main1,m +/xxx/xx,xx, diff --git a/tests/mock_group_file b/tests/mock_group_file index 32f0a64..1187366 100644 --- a/tests/mock_group_file +++ b/tests/mock_group_file @@ -1,2 +1,2 @@ -xx: [a, b] -yy: [a, c, d] +xx:a b +yy:a c d diff --git a/tests/mock_path_file b/tests/mock_path_file index 2a5f9f9..81dc9ef 100644 --- a/tests/mock_path_file +++ b/tests/mock_path_file @@ -1,4 +1,4 @@ /a/bcd/repo1,repo1 -/a/b/c/repo3,xxx +/a/b/c/repo3,xxx,, /e/fgh/repo2,repo2 diff --git a/tests/test_info.py b/tests/test_info.py index 025aedc..c234d78 100644 --- a/tests/test_info.py +++ b/tests/test_info.py @@ -8,9 +8,9 @@ from gita import info def test_run_quiet_diff(mock_run): mock_return = MagicMock() mock_run.return_value = mock_return - got = info.run_quiet_diff(['my', 'args']) + got = info.run_quiet_diff(['--flags'], ['my', 'args']) mock_run.assert_called_once_with( - ['git', 'diff', '--quiet', 'my', 'args'], + ['git', '--flags', 'diff', '--quiet', 'my', 'args'], stderr=subprocess.DEVNULL, ) assert got == mock_return.returncode diff --git a/tests/test_main.py b/tests/test_main.py index ad501f7..490f5d2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,28 +1,102 @@ +import os import pytest -from unittest.mock import patch, mock_open +from unittest.mock import patch from pathlib import Path import argparse +import asyncio import shlex from gita import __main__ -from gita import utils, info +from gita import utils, info, common from conftest import ( - PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME, + PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME, PATH_FNAME_MAIN, async_mock, TEST_DIR, ) +@patch('gita.utils.get_repos', return_value={'aa'}) +def test_group_name(_): + got = __main__._group_name('xx') + assert got == 'xx' + with pytest.raises(SystemExit): + __main__._group_name('aa') + + +class TestAdd: + + @pytest.mark.parametrize('input, expected', [ + (['add', '.'], ''), + (['add', '-m', '.'], 'm'), + ]) + @patch('gita.common.get_config_fname') + def test_add(self, mock_path_fname, tmp_path, input, expected): + def side_effect(input, _=None): + return tmp_path / f'{input}.txt' + mock_path_fname.side_effect = side_effect + utils.get_repos.cache_clear() + __main__.main(input) + utils.get_repos.cache_clear() + got = utils.get_repos() + assert len(got) == 1 + assert got['gita']['type'] == expected + + @patch('gita.utils.is_git', return_value=True) + def test_add_main(self, _, tmp_path, monkeypatch, tmpdir): + def side_effect(root=None): + if root is None: + return os.path.join(tmp_path, "gita") + else: + return os.path.join(root, ".gita") + + def desc(repos, **_): + print(len(repos), repos.keys()) + assert len(repos) > 0 + for r, prop in repos.items(): + if prop['type'] == 'm': + assert 'test_add_main' in r + break + else: + assert 0, 'no main repo found' + return '' + + monkeypatch.setattr(common, 'get_config_dir', side_effect) + monkeypatch.setattr(utils, 'describe', desc) + + utils.get_repos.cache_clear() + + with tmpdir.as_cwd(): + __main__.main(['add', '-m', '.']) + utils.get_repos.cache_clear() + __main__.main(['ll']) + + +@pytest.mark.parametrize('path_fname, expected', [ + (PATH_FNAME, ''), + (PATH_FNAME_CLASH, "repo2: ['--haha', '--pp']\n"), + ]) +@patch('gita.utils.is_git', return_value=True) +@patch('gita.utils.get_groups', return_value={}) +@patch('gita.common.get_config_fname') +def test_flags(mock_path_fname, _, __, path_fname, expected, capfd): + mock_path_fname.return_value = path_fname + utils.get_repos.cache_clear() + __main__.main(['flags']) + out, err = capfd.readouterr() + assert err == '' + assert out == expected + + class TestLsLl: @patch('gita.common.get_config_fname') - def testLl(self, mock_path_fname, capfd, tmp_path): + def test_ll(self, mock_path_fname, capfd, tmp_path): """ functional test """ # avoid modifying the local configuration - def side_effect(input): + def side_effect(input, _=None): return tmp_path / f'{input}.txt' - #mock_path_fname.return_value = tmp_path / 'path_config.txt' mock_path_fname.side_effect = side_effect + utils.get_repos.cache_clear() __main__.main(['add', '.']) out, err = capfd.readouterr() assert err == '' @@ -52,11 +126,11 @@ class TestLsLl: __main__.main(['ls', 'gita']) out, err = capfd.readouterr() assert err == '' - assert out.strip() == utils.get_repos()['gita'] + assert out.strip() == utils.get_repos()['gita']['path'] - def testLs(self, monkeypatch, capfd): + def test_ls(self, monkeypatch, capfd): monkeypatch.setattr(utils, 'get_repos', - lambda: {'repo1': '/a/', 'repo2': '/b/'}) + lambda: {'repo1': {'path': '/a/'}, 'repo2': {'path': '/b/'}}) monkeypatch.setattr(utils, 'describe', lambda x: x) __main__.main(['ls']) out, err = capfd.readouterr() @@ -69,21 +143,24 @@ class TestLsLl: @pytest.mark.parametrize('path_fname, expected', [ (PATH_FNAME, - "repo1 cmaster dsu\x1b[0m msg\nrepo2 cmaster dsu\x1b[0m msg\nxxx cmaster dsu\x1b[0m msg\n"), + "repo1 cmaster dsu\x1b[0m msg \nrepo2 cmaster dsu\x1b[0m msg \nxxx cmaster dsu\x1b[0m msg \n"), (PATH_FNAME_EMPTY, ""), + (PATH_FNAME_MAIN, + '\x1b[4mmain1\x1b[0m cmaster dsu\x1b[0m msg \nxx cmaster dsu\x1b[0m msg \n'), (PATH_FNAME_CLASH, - "repo1 cmaster dsu\x1b[0m msg\nrepo2 cmaster dsu\x1b[0m msg\nx/repo1 cmaster dsu\x1b[0m msg\n" + "repo1 cmaster dsu\x1b[0m msg \nrepo2 cmaster dsu\x1b[0m msg \n" ), ]) @patch('gita.utils.is_git', return_value=True) @patch('gita.info.get_head', return_value="master") @patch('gita.info._get_repo_status', return_value=("d", "s", "u", "c")) @patch('gita.info.get_commit_msg', return_value="msg") + @patch('gita.info.get_commit_time', return_value="") @patch('gita.common.get_config_fname') - def testWithPathFiles(self, mock_path_fname, _0, _1, _2, _3, path_fname, + def test_with_path_files(self, mock_path_fname, _0, _1, _2, _3, _4, path_fname, expected, capfd): - def side_effect(input): - if input == 'repo_path': + def side_effect(input, _=None): + if input == 'repos.csv': return path_fname return f'/{input}' mock_path_fname.side_effect = side_effect @@ -95,25 +172,63 @@ class TestLsLl: assert out == expected +@pytest.mark.parametrize('input, expected', [ + ({'repo1': {'path': '/a/'}, 'repo2': {'path': '/b/'}}, ''), + ]) @patch('subprocess.run') -@patch('gita.utils.get_repos', return_value={'repo1': '/a/', 'repo2': '/b/'}) -def test_freeze(_, mock_run, capfd): +@patch('gita.utils.get_repos') +def test_freeze(mock_repos, mock_run, input, expected, capfd): + mock_repos.return_value = input __main__.main(['freeze']) assert mock_run.call_count == 2 out, err = capfd.readouterr() assert err == '' - assert out == ',repo1,/a/\n,repo2,/b/\n' + assert out == expected + + +@patch('gita.utils.parse_clone_config', return_value=[ + ['git@github.com:user/repo.git', 'repo', '/a/repo']]) +@patch('gita.utils.run_async', new=async_mock()) +@patch('subprocess.run') +def test_clone(*_): + asyncio.set_event_loop(asyncio.new_event_loop()) + args = argparse.Namespace() + args.fname = ['freeze_filename'] + args.preserve_path = None + __main__.f_clone(args) + mock_run = utils.run_async.mock + assert mock_run.call_count == 1 + cmds = ['git', 'clone', 'git@github.com:user/repo.git'] + mock_run.assert_called_once_with('repo', Path.cwd(), cmds) + + +@patch('gita.utils.parse_clone_config', return_value=[ + ['git@github.com:user/repo.git', 'repo', '/a/repo']]) +@patch('gita.utils.run_async', new=async_mock()) +@patch('subprocess.run') +def test_clone_with_preserve_path(*_): + asyncio.set_event_loop(asyncio.new_event_loop()) + args = argparse.Namespace() + args.fname = ['freeze_filename'] + args.preserve_path = True + __main__.f_clone(args) + mock_run = utils.run_async.mock + assert mock_run.call_count == 1 + cmds = ['git', 'clone', 'git@github.com:user/repo.git', '/a/repo'] + mock_run.assert_called_once_with('repo', Path.cwd(), cmds) @patch('os.path.isfile', return_value=True) @patch('gita.common.get_config_fname', return_value='some path') -@patch('gita.utils.get_repos', return_value={'repo1': '/a/', 'repo2': '/b/'}) +@patch('gita.utils.get_repos', return_value={'repo1': {'path': '/a/', 'type': ''}, + 'repo2': {'path': '/b/', 'type': ''}}) @patch('gita.utils.write_to_repo_file') def test_rm(mock_write, *_): args = argparse.Namespace() args.repo = ['repo1'] __main__.f_rm(args) - mock_write.assert_called_once_with({'repo2': '/b/'}, 'w') + mock_write.assert_called_once_with( + {'repo2': {'path': '/b/', 'type': ''}}, 'w') def test_not_add(): @@ -121,17 +236,19 @@ def test_not_add(): __main__.main(['add', '/home/some/repo/']) -@patch('gita.utils.get_repos', return_value={'repo2': '/d/efg'}) +@patch('gita.utils.get_repos', return_value={'repo2': {'path': '/d/efg', + 'flags': []}}) @patch('subprocess.run') def test_fetch(mock_run, *_): + asyncio.set_event_loop(asyncio.new_event_loop()) __main__.main(['fetch']) - mock_run.assert_called_once_with(['git', 'fetch'], cwd='/d/efg') + mock_run.assert_called_once_with(['git', 'fetch'], cwd='/d/efg', shell=False) @patch( 'gita.utils.get_repos', return_value={ - 'repo1': '/a/bc', - 'repo2': '/d/efg' + 'repo1': {'path': '/a/bc', 'flags': []}, + 'repo2': {'path': '/d/efg', 'flags': []} }) @patch('gita.utils.run_async', new=async_mock()) @patch('subprocess.run') @@ -149,28 +266,28 @@ def test_async_fetch(*_): 'diff --name-only --staged', "commit -am 'lala kaka'", ]) -@patch('gita.utils.get_repos', return_value={'repo7': 'path7'}) +@patch('gita.utils.get_repos', return_value={'repo7': {'path': 'path7', 'flags': []}}) @patch('subprocess.run') def test_superman(mock_run, _, input): mock_run.reset_mock() args = ['super', 'repo7'] + shlex.split(input) __main__.main(args) expected_cmds = ['git'] + shlex.split(input) - mock_run.assert_called_once_with(expected_cmds, cwd='path7') + mock_run.assert_called_once_with(expected_cmds, cwd='path7', shell=False) @pytest.mark.parametrize('input', [ 'diff --name-only --staged', "commit -am 'lala kaka'", ]) -@patch('gita.utils.get_repos', return_value={'repo7': 'path7'}) +@patch('gita.utils.get_repos', return_value={'repo7': {'path': 'path7', 'flags': []}}) @patch('subprocess.run') def test_shell(mock_run, _, input): mock_run.reset_mock() - args = ['shell', 'repo7'] + shlex.split(input) + args = ['shell', 'repo7', input] __main__.main(args) - expected_cmds = shlex.split(input) - mock_run.assert_called_once_with(expected_cmds, cwd='path7', check=True, stderr=-2, stdout=-1) + expected_cmds = input + mock_run.assert_called_once_with(expected_cmds, cwd='path7', check=True, shell=True, stderr=-2, stdout=-1) class TestContext: @@ -184,21 +301,21 @@ class TestContext: @patch('gita.utils.get_context', return_value=Path('gname.context')) @patch('gita.utils.get_groups', return_value={'gname': ['a', 'b']}) - def testDisplayContext(self, _, __, capfd): + def test_display_context(self, _, __, capfd): __main__.main(['context']) out, err = capfd.readouterr() assert err == '' assert 'gname: a b\n' == out @patch('gita.utils.get_context') - def testReset(self, mock_ctx): + def test_reset(self, mock_ctx): __main__.main(['context', 'none']) mock_ctx.return_value.unlink.assert_called() @patch('gita.utils.get_context', return_value=None) @patch('gita.common.get_config_dir', return_value=TEST_DIR) @patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []}) - def testSetFirstTime(self, *_): + def test_set_first_time(self, *_): ctx = TEST_DIR / 'lala.context' assert not ctx.is_file() __main__.main(['context', 'lala']) @@ -208,7 +325,7 @@ class TestContext: @patch('gita.common.get_config_dir', return_value=TEST_DIR) @patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []}) @patch('gita.utils.get_context') - def testSetSecondTime(self, mock_ctx, *_): + def test_set_second_time(self, mock_ctx, *_): __main__.main(['context', 'kaka']) mock_ctx.return_value.rename.assert_called() @@ -216,7 +333,7 @@ class TestContext: class TestGroupCmd: @patch('gita.common.get_config_fname', return_value=GROUP_FNAME) - def testLs(self, _, capfd): + def test_ls(self, _, capfd): args = argparse.Namespace() args.to_group = None args.group_cmd = 'ls' @@ -227,7 +344,7 @@ class TestGroupCmd: assert 'xx yy\n' == out @patch('gita.common.get_config_fname', return_value=GROUP_FNAME) - def testLl(self, _, capfd): + def test_ll(self, _, capfd): args = argparse.Namespace() args.to_group = None args.group_cmd = None @@ -239,7 +356,7 @@ class TestGroupCmd: @patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.utils.write_to_groups_file') - def testRename(self, mock_write, _): + def test_rename(self, mock_write, _): args = argparse.Namespace() args.gname = 'xx' args.new_name = 'zz' @@ -250,7 +367,7 @@ class TestGroupCmd: mock_write.assert_called_once_with(expected, 'w') @patch('gita.common.get_config_fname', return_value=GROUP_FNAME) - def testRenameError(self, *_): + def test_rename_error(self, *_): args = argparse.Namespace() args.gname = 'xx' args.new_name = 'yy' @@ -266,7 +383,7 @@ class TestGroupCmd: @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''}) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.utils.write_to_groups_file') - def testRm(self, mock_write, _, __, input, expected): + def test_rm(self, mock_write, _, __, input, expected): utils.get_groups.cache_clear() args = ['group', 'rm'] + shlex.split(input) __main__.main(args) @@ -275,7 +392,7 @@ class TestGroupCmd: @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''}) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.utils.write_to_groups_file') - def testAdd(self, mock_write, *_): + def test_add(self, mock_write, *_): args = argparse.Namespace() args.to_group = ['a', 'c'] args.group_cmd = 'add' @@ -287,7 +404,7 @@ class TestGroupCmd: @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''}) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.utils.write_to_groups_file') - def testAddToExisting(self, mock_write, *_): + def test_add_to_existing(self, mock_write, *_): args = argparse.Namespace() args.to_group = ['a', 'c'] args.group_cmd = 'add' @@ -300,7 +417,7 @@ class TestGroupCmd: @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''}) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.utils.write_to_groups_file') - def testRmRepo(self, mock_write, *_): + def test_rm_repo(self, mock_write, *_): args = argparse.Namespace() args.from_group = ['a', 'c'] args.group_cmd = 'rmrepo' @@ -310,6 +427,21 @@ class TestGroupCmd: mock_write.assert_called_once_with( {'xx': ['b'], 'yy': ['a', 'c', 'd']}, 'w') + @patch('gita.common.get_config_fname') + def test_integration(self, mock_path_fname, tmp_path, capfd): + def side_effect(input, _=None): + return tmp_path / f'{input}.csv' + mock_path_fname.side_effect = side_effect + + __main__.main('add .'.split()) + utils.get_repos.cache_clear() + __main__.main('group add gita -n test'.split()) + utils.get_groups.cache_clear() + __main__.main('ll test'.split()) + out, err = capfd.readouterr() + assert err == '' + assert 'gita' in out + @patch('gita.utils.is_git', return_value=True) @patch('gita.common.get_config_fname', return_value=PATH_FNAME) @@ -319,44 +451,61 @@ def test_rename(mock_rename, _, __): args = ['rename', 'repo1', 'abc'] __main__.main(args) mock_rename.assert_called_once_with( - {'repo1': '/a/bcd/repo1', 'repo2': '/e/fgh/repo2', - 'xxx': '/a/b/c/repo3'}, + {'repo1': {'path': '/a/bcd/repo1', 'type': '', 'flags': []}, + 'xxx': {'path': '/a/b/c/repo3', 'type': '', 'flags': []}, + 'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': []}}, 'repo1', 'abc') class TestInfo: @patch('gita.common.get_config_fname', return_value='') - def testLl(self, _, capfd): + def test_ll(self, _, capfd): args = argparse.Namespace() args.info_cmd = None __main__.f_info(args) out, err = capfd.readouterr() - assert 'In use: branch,commit_msg\nUnused: path\n' == out + assert 'In use: branch,commit_msg,commit_time\nUnused: path\n' == out assert err == '' - @patch('gita.common.get_config_fname', return_value='') - @patch('yaml.dump') - def testAdd(self, mock_dump, _): + @patch('gita.common.get_config_fname') + def test_add(self, mock_get_fname, tmpdir): args = argparse.Namespace() args.info_cmd = 'add' args.info_item = 'path' - with patch('builtins.open', mock_open(), create=True): + with tmpdir.as_cwd(): + csv_config = Path.cwd() / 'info.csv' + mock_get_fname.return_value = csv_config __main__.f_info(args) - mock_dump.assert_called_once() - args, kwargs = mock_dump.call_args - assert args[0] == ['branch', 'commit_msg', 'path'] - assert kwargs == {'default_flow_style': None} + items = info.get_info_items() + assert items == ['branch', 'commit_msg', 'commit_time', 'path'] - @patch('gita.common.get_config_fname', return_value='') - @patch('yaml.dump') - def testRm(self, mock_dump, _): + @patch('gita.common.get_config_fname') + def test_rm(self, mock_get_fname, tmpdir): args = argparse.Namespace() args.info_cmd = 'rm' args.info_item = 'commit_msg' - with patch('builtins.open', mock_open(), create=True): + with tmpdir.as_cwd(): + csv_config = Path.cwd() / 'info.csv' + mock_get_fname.return_value = csv_config __main__.f_info(args) - mock_dump.assert_called_once() - args, kwargs = mock_dump.call_args - assert args[0] == ['branch'] - assert kwargs == {'default_flow_style': None} + items = info.get_info_items() + assert items == ['branch', 'commit_time'] + + +@patch('gita.common.get_config_fname') +def test_set_color(mock_get_fname, tmpdir): + args = argparse.Namespace() + args.color_cmd = 'set' + args.color = 'redrum' # this color doesn't exist + args.situation = 'in-sync' + with tmpdir.as_cwd(): + csv_config = Path.cwd() / 'colors.csv' + mock_get_fname.return_value = csv_config + __main__.f_color(args) + info.get_color_encoding.cache_clear() # avoid side effect + items = info.get_color_encoding() + info.get_color_encoding.cache_clear() # avoid side effect + assert items == {'no-remote': 'white', 'in-sync': 'redrum', + 'diverged': 'red', 'local-ahead': 'purple', + 'remote-ahead': 'yellow'} diff --git a/tests/test_utils.py b/tests/test_utils.py index 886ddb9..39430b0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,7 @@ import pytest import asyncio +import subprocess +from pathlib import Path from unittest.mock import patch, mock_open from gita import utils, info @@ -8,16 +10,41 @@ from conftest import ( ) +@pytest.mark.parametrize('repo_path, paths, expected', [ + ('/a/b/c/repo', ['/a/b'], ('b', 'c')), + ]) +def test_generate_dir_hash(repo_path, paths, expected): + got = utils._generate_dir_hash(repo_path, paths) + assert got == expected + + +@pytest.mark.parametrize('repos, paths, expected', [ + ({'r1': {'path': '/a/b//repo1'}, 'r2': {'path': '/a/b/repo2'}}, + ['/a/b'], {'b': ['r1', 'r2']}), + ({'r1': {'path': '/a/b//repo1'}, 'r2': {'path': '/a/b/c/repo2'}}, + ['/a/b'], {'b': ['r1', 'r2'], 'b-c': ['r2']}), + ({'r1': {'path': '/a/b/c/repo1'}, 'r2': {'path': '/a/b/c/repo2'}}, + ['/a/b'], {'b-c': ['r1', 'r2'], 'b': ['r1', 'r2']}), + ]) +def test_auto_group(repos, paths, expected): + got = utils.auto_group(repos, paths) + assert got == expected + + @pytest.mark.parametrize('test_input, diff_return, expected', [ - ([{'abc': '/root/repo/'}, False], True, 'abc \x1b[31mrepo *+_ \x1b[0m msg'), - ([{'abc': '/root/repo/'}, True], True, 'abc repo *+_ msg'), - ([{'repo': '/root/repo2/'}, False], False, 'repo \x1b[32mrepo _ \x1b[0m msg'), + ([{'abc': {'path': '/root/repo/', 'type': '', 'flags': []}}, False], + True, 'abc \x1b[31mrepo *+_ \x1b[0m msg xx'), + ([{'abc': {'path': '/root/repo/', 'type': '', 'flags': []}}, True], + True, 'abc repo *+_ msg xx'), + ([{'repo': {'path': '/root/repo2/', 'type': '', 'flags': []}}, False], + False, 'repo \x1b[32mrepo _ \x1b[0m msg xx'), ]) def test_describe(test_input, diff_return, expected, monkeypatch): monkeypatch.setattr(info, 'get_head', lambda x: 'repo') - monkeypatch.setattr(info, 'run_quiet_diff', lambda _: diff_return) - monkeypatch.setattr(info, 'get_commit_msg', lambda _: "msg") - monkeypatch.setattr(info, 'has_untracked', lambda: True) + monkeypatch.setattr(info, 'run_quiet_diff', lambda *_: diff_return) + monkeypatch.setattr(info, 'get_commit_msg', lambda *_: "msg") + monkeypatch.setattr(info, 'get_commit_time', lambda *_: "xx") + monkeypatch.setattr(info, 'has_untracked', lambda *_: True) monkeypatch.setattr('os.chdir', lambda x: None) print('expected: ', repr(expected)) print('got: ', repr(next(utils.describe(*test_input)))) @@ -26,15 +53,14 @@ def test_describe(test_input, diff_return, expected, monkeypatch): @pytest.mark.parametrize('path_fname, expected', [ (PATH_FNAME, { - 'repo1': '/a/bcd/repo1', - 'repo2': '/e/fgh/repo2', - 'xxx': '/a/b/c/repo3', + 'repo1': {'path': '/a/bcd/repo1', 'type': '', 'flags': []}, + 'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': []}, + 'xxx': {'path': '/a/b/c/repo3', 'type': '', 'flags': []}, }), (PATH_FNAME_EMPTY, {}), (PATH_FNAME_CLASH, { - 'repo1': '/a/bcd/repo1', - 'repo2': '/e/fgh/repo2', - 'x/repo1': '/root/x/repo1' + 'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': ['--haha', '--pp']}, + 'repo1': {'path': '/root/x/repo1', 'type': '', 'flags': []} }), ]) @patch('gita.utils.is_git', return_value=True) @@ -70,42 +96,46 @@ def test_get_groups(mock_group_fname, group_fname, expected): @patch('os.path.getsize', return_value=True) def test_custom_push_cmd(*_): with patch('builtins.open', - mock_open(read_data='push:\n cmd: hand\n help: me')): + mock_open(read_data='{"push":{"cmd":"hand","help":"me","allow_all":true}}')): cmds = utils.get_cmds_from_files() - assert cmds['push'] == {'cmd': 'hand', 'help': 'me'} + assert cmds['push'] == {'cmd': 'hand', 'help': 'me', 'allow_all': True} @pytest.mark.parametrize( 'path_input, expected', [ - (['/home/some/repo/'], '/home/some/repo,repo\n'), # add one new + (['/home/some/repo'], '/home/some/repo,some/repo,,\r\n'), # add one new (['/home/some/repo1', '/repo2'], - {'/repo2,repo2\n/home/some/repo1,repo1\n', # add two new - '/home/some/repo1,repo1\n/repo2,repo2\n'}), # add two new + {'/repo2,repo2,,\r\n', # add two new + '/home/some/repo1,repo1,,\r\n'}), # add two new (['/home/some/repo1', '/nos/repo'], - '/home/some/repo1,repo1\n'), # add one old one new + '/home/some/repo1,repo1,,\r\n'), # add one old one new ]) @patch('os.makedirs') @patch('gita.utils.is_git', return_value=True) def test_add_repos(_0, _1, path_input, expected, monkeypatch): monkeypatch.setenv('XDG_CONFIG_HOME', '/config') with patch('builtins.open', mock_open()) as mock_file: - utils.add_repos({'repo': '/nos/repo'}, path_input) - mock_file.assert_called_with('/config/gita/repo_path', 'a+') + utils.add_repos({'repo': {'path': '/nos/repo'}}, path_input) + mock_file.assert_called_with('/config/gita/repos.csv', 'a+', newline='') handle = mock_file() if type(expected) == str: handle.write.assert_called_once_with(expected) else: - handle.write.assert_called_once() + # the write order is random + assert handle.write.call_count == 2 args, kwargs = handle.write.call_args assert args[0] in expected assert not kwargs +@patch('gita.utils.write_to_groups_file') @patch('gita.utils.write_to_repo_file') -def test_rename_repo(mock_write): - utils.rename_repo({'r1': '/a/b', 'r2': '/c/c'}, 'r2', 'xxx') - mock_write.assert_called_once_with({'r1': '/a/b', 'xxx': '/c/c'}, 'w') +def test_rename_repo(mock_write, _): + repos = {'r1': {'path': '/a/b', 'type': None}, + 'r2': {'path': '/c/c', 'type': None}} + utils.rename_repo(repos, 'r2', 'xxx') + mock_write.assert_called_once_with(repos, 'w') def test_async_output(capfd): @@ -124,3 +154,10 @@ def test_async_output(capfd): out, err = capfd.readouterr() assert err == '' assert out == 'myrepo: 0\nmyrepo: 0\n\nmyrepo: 1\nmyrepo: 1\n\nmyrepo: 2\nmyrepo: 2\n\nmyrepo: 3\nmyrepo: 3\n\n' + + +def test_is_git(tmpdir): + with tmpdir.as_cwd(): + subprocess.run('git init --bare .'.split()) + assert utils.is_git(Path.cwd()) is False + assert utils.is_git(Path.cwd(), is_bare=True) is True diff --git a/work.vim b/work.vim new file mode 100644 index 0000000..25ad90f --- /dev/null +++ b/work.vim @@ -0,0 +1,766 @@ +let SessionLoad = 1 +if &cp | set nocp | endif +let s:cpo_save=&cpo +set cpo&vim +inoremap (-fzf-complete-finish) l +inoremap CocRefresh =coc#_complete() +inoremap (fzf-maps-i) :call fzf#vim#maps('i', 0) +inoremap (fzf-complete-buffer-line) fzf#vim#complete#buffer_line() +inoremap (fzf-complete-line) fzf#vim#complete#line() +inoremap (fzf-complete-file-ag) fzf#vim#complete#path('ag -l -g ""') +inoremap (fzf-complete-file) fzf#vim#complete#path("find . -path '*/\.*' -prune -o -type f -print -o -type l -print | sed 's:^..::'") +inoremap (fzf-complete-path) fzf#vim#complete#path("find . -path '*/\.*' -prune -o -print | sed '1d;s:^..::'") +inoremap (fzf-complete-word) fzf#vim#complete#word() +inoremap 20_AutoPairsReturn =AutoPairsReturn() +inoremap coc#refresh() +inoremap pumvisible() ? "\" : "\" +map! * +nnoremap * *`` +nmap ,ig IndentGuidesToggle +noremap ,4 4gt +noremap ,3 3gt +noremap ,2 2gt +noremap ,1 1gt +nmap ,d :GitGutterFold +nmap ,r :Rg! +nmap ,b :Buffer +nmap ,l :Lines! +nmap ,w :BLines +nmap ,o :Files! +nmap ,f :GFiles! +nmap ,a :CtrlSF -R "" +nmap ,t :TagbarToggle +noremap , :noh :call clearmatches() +vnoremap ,s :sort +nnoremap ,s :w +noremap ,e :qa! " Quit all windows +noremap ,q :q " Quit current windows +vnoremap <  >gv +nnoremap N Nzzzv +vmap gx NetrwBrowseXVis +nmap gx NetrwBrowseX +nmap g> (swap-next) +nmap g< (swap-prev) +xmap gs (swap-interactive) +nmap gs (swap-interactive) +nmap gr (coc-references) +nmap gi (coc-implementation) +nmap gy (coc-type-definition) +nmap gd (coc-definition) +nnoremap n nzzzv +nnoremap (-fzf-complete-finish) a +nnoremap (-fzf-:) : +nnoremap (-fzf-/) / +nnoremap (-fzf-vim-do) :execute g:__fzf_command +vnoremap NetrwBrowseXVis :call netrw#BrowseXVis() +nnoremap NetrwBrowseX :call netrw#BrowseX(netrw#GX(),netrw#CheckIfRemote(netrw#GX())) +onoremap (coc-classobj-a) :call coc#rpc#request('selectSymbolRange', [v:false, '', ['Interface', 'Struct', 'Class']]) +onoremap (coc-classobj-i) :call coc#rpc#request('selectSymbolRange', [v:true, '', ['Interface', 'Struct', 'Class']]) +vnoremap (coc-classobj-a) :call coc#rpc#request('selectSymbolRange', [v:false, visualmode(), ['Interface', 'Struct', 'Class']]) +vnoremap (coc-classobj-i) :call coc#rpc#request('selectSymbolRange', [v:true, visualmode(), ['Interface', 'Struct', 'Class']]) +onoremap (coc-funcobj-a) :call coc#rpc#request('selectSymbolRange', [v:false, '', ['Method', 'Function']]) +onoremap (coc-funcobj-i) :call coc#rpc#request('selectSymbolRange', [v:true, '', ['Method', 'Function']]) +vnoremap (coc-funcobj-a) :call coc#rpc#request('selectSymbolRange', [v:false, visualmode(), ['Method', 'Function']]) +vnoremap (coc-funcobj-i) :call coc#rpc#request('selectSymbolRange', [v:true, visualmode(), ['Method', 'Function']]) +nnoremap (coc-cursors-position) :call coc#rpc#request('cursorsSelect', [bufnr('%'), 'position', 'n']) +nnoremap (coc-cursors-word) :call coc#rpc#request('cursorsSelect', [bufnr('%'), 'word', 'n']) +vnoremap (coc-cursors-range) :call coc#rpc#request('cursorsSelect', [bufnr('%'), 'range', visualmode()]) +nnoremap (coc-refactor) :call CocActionAsync('refactor') +nnoremap (coc-command-repeat) :call CocAction('repeatCommand') +nnoremap (coc-float-jump) :call coc#float#jump() +nnoremap (coc-float-hide) :call coc#float#close_all() +nnoremap (coc-fix-current) :call CocActionAsync('doQuickfix') +nnoremap (coc-openlink) :call CocActionAsync('openLink') +nnoremap (coc-references-used) :call CocActionAsync('jumpUsed') +nnoremap (coc-references) :call CocActionAsync('jumpReferences') +nnoremap (coc-type-definition) :call CocActionAsync('jumpTypeDefinition') +nnoremap (coc-implementation) :call CocActionAsync('jumpImplementation') +nnoremap (coc-declaration) :call CocActionAsync('jumpDeclaration') +nnoremap (coc-definition) :call CocActionAsync('jumpDefinition') +nnoremap (coc-diagnostic-prev-error) :call CocActionAsync('diagnosticPrevious', 'error') +nnoremap (coc-diagnostic-next-error) :call CocActionAsync('diagnosticNext', 'error') +nnoremap (coc-diagnostic-prev) :call CocActionAsync('diagnosticPrevious') +nnoremap (coc-diagnostic-next) :call CocActionAsync('diagnosticNext') +nnoremap (coc-diagnostic-info) :call CocActionAsync('diagnosticInfo') +nnoremap (coc-format) :call CocActionAsync('format') +nnoremap (coc-rename) :call CocActionAsync('rename') +nnoremap (coc-codeaction-cursor) :call CocActionAsync('codeAction', 'cursor') +nnoremap (coc-codeaction-line) :call CocActionAsync('codeAction', 'line') +nnoremap (coc-codeaction) :call CocActionAsync('codeAction', '') +vnoremap (coc-codeaction-selected) :call CocActionAsync('codeAction', visualmode()) +vnoremap (coc-format-selected) :call CocActionAsync('formatSelected', visualmode()) +nnoremap (coc-codelens-action) :call CocActionAsync('codeLensAction') +nnoremap (coc-range-select) :call CocActionAsync('rangeSelect', '', v:true) +vnoremap (coc-range-select-backward) :call CocActionAsync('rangeSelect', visualmode(), v:false) +vnoremap (coc-range-select) :call CocActionAsync('rangeSelect', visualmode(), v:true) +noremap (swap-textobject-a) :call swap#textobj#select('a') +noremap (swap-textobject-i) :call swap#textobj#select('i') +nnoremap (swap-next) :call swap#prerequisite('n', repeat([['#', '#+1']], v:count1)) g@l +nnoremap (swap-prev) :call swap#prerequisite('n', repeat([['#', '#-1']], v:count1)) g@l +xnoremap (swap-interactive) :call swap#prerequisite('x') gvg@ +nnoremap (swap-interactive) :call swap#prerequisite('n') g@l +onoremap (fzf-maps-o) :call fzf#vim#maps('o', 0) +xnoremap (fzf-maps-x) :call fzf#vim#maps('x', 0) +nnoremap (fzf-maps-n) :call fzf#vim#maps('n', 0) +tnoremap (fzf-normal)  +tnoremap (fzf-insert) i +nnoremap (fzf-normal) +nnoremap (fzf-insert) i +nnoremap CtrlSFQuickfixPrompt :CtrlSFQuickfix +nnoremap CtrlSFPrompt :CtrlSF +nnoremap GitGutterPreviewHunk :call gitgutter#utility#warn('Please change your map GitGutterPreviewHunk to (GitGutterPreviewHunk)') +nnoremap (GitGutterPreviewHunk) :GitGutterPreviewHunk +nnoremap GitGutterUndoHunk :call gitgutter#utility#warn('Please change your map GitGutterUndoHunk to (GitGutterUndoHunk)') +nnoremap (GitGutterUndoHunk) :GitGutterUndoHunk +nnoremap GitGutterStageHunk :call gitgutter#utility#warn('Please change your map GitGutterStageHunk to (GitGutterStageHunk)') +nnoremap (GitGutterStageHunk) :GitGutterStageHunk +xnoremap GitGutterStageHunk :call gitgutter#utility#warn('Please change your map GitGutterStageHunk to (GitGutterStageHunk)') +xnoremap (GitGutterStageHunk) :GitGutterStageHunk +nnoremap GitGutterPrevHunk &diff ? '[c' : ":\call gitgutter#utility#warn('Please change your map \GitGutterPrevHunk to \(GitGutterPrevHunk)')\ " +nnoremap (GitGutterPrevHunk) &diff ? '[c' : ":\execute v:count1 . 'GitGutterPrevHunk'\ " +nnoremap GitGutterNextHunk &diff ? ']c' : ":\call gitgutter#utility#warn('Please change your map \GitGutterNextHunk to \(GitGutterNextHunk)')\ " +nnoremap (GitGutterNextHunk) &diff ? ']c' : ":\execute v:count1 . 'GitGutterNextHunk'\ " +xnoremap (GitGutterTextObjectOuterVisual) :call gitgutter#hunk#text_object(0) +xnoremap (GitGutterTextObjectInnerVisual) :call gitgutter#hunk#text_object(1) +onoremap (GitGutterTextObjectOuterPending) :call gitgutter#hunk#text_object(0) +onoremap (GitGutterTextObjectInnerPending) :call gitgutter#hunk#text_object(1) +vmap "-d +vmap "*d +vmap "*y +vmap "-d"*P +nmap "*P +inoremap  complete_info()["selected"] != "-1" ? "\" : "\u\ " +inoremap ,s :w +let &cpo=s:cpo_save +unlet s:cpo_save +set autoindent +set background=dark +set backspace=2 +set clipboard=unnamed +set expandtab +set fileencodings=ucs-bom,utf-8,default,latin1 +set helplang=en +set hlsearch +set ignorecase +set laststatus=2 +set modelines=0 +set path=.,/usr/include,,,** +set runtimepath=~/.vim,~/.vim/plugged/vim-gitgutter/,~/.vim/plugged/ctrlsf.vim/,~/.vim/plugged/lightline.vim/,~/.vim/plugged/auto-pairs/,~/.vim/plugged/fzf/,~/.vim/plugged/fzf.vim/,~/.vim/plugged/goyo.vim/,~/.vim/plugged/gv.vim/,~/.vim/plugged/seoul256.vim/,~/.vim/plugged/vim-swap/,~/.vim/plugged/tagbar/,~/.vim/plugged/coc.nvim/,~/.vim/plugged/vim-fugitive/,~/.vim/plugged/vim-indent-guides/,/usr/share/vim/vimfiles,/usr/share/vim/vim82,/usr/share/vim/vimfiles/after,~/.vim/plugged/ctrlsf.vim/after,~/.vim/after +set shiftwidth=4 +set smartcase +set noswapfile +set tabline=%!lightline#tabline() +set tabstop=4 +set title +set updatetime=100 +set wildignore=*.pyc +set wildmenu +set wildmode=longest:list,full +set window=0 +set nowritebackup +let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0 +let v:this_session=expand(":p") +silent only +silent tabonly +cd ~/src/gita +if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == '' + let s:wipebuf = bufnr('%') +endif +set shortmess=aoO +argglobal +%argdel +$argadd gita/__main__.py +set stal=2 +tabnew +tabrewind +edit gita/utils.py +set splitbelow splitright +wincmd _ | wincmd | +vsplit +1wincmd h +wincmd w +set nosplitbelow +set nosplitright +wincmd t +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe 'vert 1resize ' . ((&columns * 94 + 94) / 188) +exe 'vert 2resize ' . ((&columns * 93 + 94) / 188) +argglobal +let s:cpo_save=&cpo +set cpo&vim +inoremap :call AutoPairsJump() a +inoremap AutoPairsToggle() +inoremap =AutoPairsBackInsert() +inoremap =AutoPairsFastWrap() +inoremap =AutoPairsDelete() +inoremap =AutoPairsDelete() +inoremap =AutoPairsMoveCharacter('''') +inoremap =AutoPairsMoveCharacter('"') +inoremap =AutoPairsMoveCharacter('}') +inoremap =AutoPairsMoveCharacter('{') +inoremap =AutoPairsMoveCharacter(']') +inoremap =AutoPairsMoveCharacter('[') +inoremap =AutoPairsMoveCharacter(')') +inoremap =AutoPairsMoveCharacter('(') +nmap ,hp (GitGutterPreviewHunk) +nmap ,hu (GitGutterUndoHunk) +nmap ,hs (GitGutterStageHunk) +xmap ,hs (GitGutterStageHunk) +inoremap § =AutoPairsMoveCharacter('''') +inoremap ¢ =AutoPairsMoveCharacter('"') +inoremap © =AutoPairsMoveCharacter(')') +inoremap ¨ =AutoPairsMoveCharacter('(') +inoremap î :call AutoPairsJump() a +inoremap ð AutoPairsToggle() +inoremap â =AutoPairsBackInsert() +inoremap ý =AutoPairsMoveCharacter('}') +inoremap û =AutoPairsMoveCharacter('{') +inoremap Ý =AutoPairsMoveCharacter(']') +inoremap Û =AutoPairsMoveCharacter('[') +nmap [c (GitGutterPrevHunk) +nmap ]c (GitGutterNextHunk) +xmap ac (GitGutterTextObjectOuterVisual) +omap ac (GitGutterTextObjectOuterPending) +xmap ic (GitGutterTextObjectInnerVisual) +omap ic (GitGutterTextObjectInnerPending) +noremap :call AutoPairsJump() +noremap :call AutoPairsToggle() +inoremap  =AutoPairsDelete() +inoremap  =AutoPairsFastWrap() +inoremap  =AutoPairsSpace() +inoremap " =AutoPairsInsert('"') +inoremap ' =AutoPairsInsert('''') +inoremap ( =AutoPairsInsert('(') +inoremap ) =AutoPairsInsert(')') +noremap î :call AutoPairsJump() +noremap ð :call AutoPairsToggle() +inoremap [ =AutoPairsInsert('[') +inoremap ] =AutoPairsInsert(']') +inoremap ` =AutoPairsInsert('`') +inoremap { =AutoPairsInsert('{') +inoremap } =AutoPairsInsert('}') +let &cpo=s:cpo_save +unlet s:cpo_save +setlocal autoindent +setlocal backupcopy= +setlocal nobinary +setlocal nobreakindent +setlocal breakindentopt= +setlocal bufhidden= +setlocal buflisted +setlocal buftype= +setlocal nocindent +setlocal cinkeys=0{,0},0),0],:,!^F,o,O,e +setlocal cinoptions= +setlocal cinwords=if,else,while,do,for,switch +set colorcolumn=80 +setlocal colorcolumn=80 +setlocal comments=b:#,fb:- +setlocal commentstring=#\ %s +setlocal complete=.,w,b,u,t,i +setlocal completefunc= +setlocal nocopyindent +setlocal cryptmethod= +setlocal nocursorbind +setlocal nocursorcolumn +setlocal nocursorline +setlocal cursorlineopt=both +setlocal define= +setlocal dictionary= +setlocal nodiff +setlocal equalprg= +setlocal errorformat= +setlocal expandtab +if &filetype != 'python' +setlocal filetype=python +endif +setlocal fixendofline +setlocal foldcolumn=0 +set nofoldenable +setlocal nofoldenable +setlocal foldexpr=0 +setlocal foldignore=# +setlocal foldlevel=0 +setlocal foldmarker={{{,}}} +set foldmethod=indent +setlocal foldmethod=indent +setlocal foldminlines=1 +setlocal foldnestmax=20 +setlocal foldtext=foldtext() +setlocal formatexpr= +setlocal formatoptions=tcq +setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s* +setlocal formatprg= +setlocal grepprg= +setlocal iminsert=0 +setlocal imsearch=-1 +setlocal include=^\\s*\\(from\\|import\\) +setlocal includeexpr=substitute(substitute(substitute(v:fname,b:grandparent_match,b:grandparent_sub,''),b:parent_match,b:parent_sub,''),b:child_match,b:child_sub,'g') +setlocal indentexpr=GetPythonIndent(v:lnum) +setlocal indentkeys=0{,0},0),0],:,!^F,o,O,e,<:>,=elif,=except +setlocal noinfercase +setlocal iskeyword=@,48-57,_,192-255 +setlocal keywordprg=pydoc +setlocal nolinebreak +setlocal nolisp +setlocal lispwords= +setlocal nolist +setlocal makeencoding= +setlocal makeprg= +setlocal matchpairs=(:),{:},[:] +setlocal modeline +setlocal modifiable +setlocal nrformats=bin,octal,hex +set number +setlocal number +setlocal numberwidth=4 +setlocal omnifunc=pythoncomplete#Complete +setlocal path= +setlocal nopreserveindent +setlocal nopreviewwindow +setlocal quoteescape=\\ +setlocal noreadonly +setlocal norelativenumber +setlocal noscrollbind +setlocal scrolloff=-1 +setlocal shiftwidth=4 +setlocal noshortname +setlocal showbreak= +setlocal sidescrolloff=-1 +setlocal signcolumn=auto +setlocal nosmartindent +setlocal softtabstop=4 +set spell +setlocal spell +setlocal spellcapcheck=[.?!]\\_[\\])'\"\ \ ]\\+ +setlocal spellfile= +setlocal spelllang=en +setlocal statusline=%{lightline#link()}%#LightlineLeft_active_0#%(\ %{lightline#mode()}\ %)%{(&paste)?\"|\":\"\"}%(\ %{&paste?\"PASTE\":\"\"}\ %)%#LightlineLeft_active_0_1#%#LightlineLeft_active_1#%(\ %R\ %)%{(&readonly)&&(1||(&modified||!&modifiable))?\"|\":\"\"}%(\ %t\ %)%{(&modified||!&modifiable)?\"|\":\"\"}%(\ %M\ %)%#LightlineLeft_active_1_2#%#LightlineMiddle_active#%=%#LightlineRight_active_2_3#%#LightlineRight_active_2#%(\ %{&ff}\ %)%{1||1?\"|\":\"\"}%(\ %{&fenc!=#\"\"?&fenc:&enc}\ %)%{1?\"|\":\"\"}%(\ %{&ft!=#\"\"?&ft:\"no\ ft\"}\ %)%#LightlineRight_active_1_2#%#LightlineRight_active_1#%(\ %3p%%\ %)%#LightlineRight_active_0_1#%#LightlineRight_active_0#%(\ %3l:%-2c\ %) +setlocal suffixesadd=.py +setlocal noswapfile +setlocal synmaxcol=3000 +if &syntax != 'python' +setlocal syntax=python +endif +setlocal tabstop=8 +setlocal tagcase= +setlocal tagfunc= +setlocal tags= +setlocal termwinkey= +setlocal termwinscroll=10000 +setlocal termwinsize= +setlocal textwidth=0 +setlocal thesaurus= +setlocal noundofile +setlocal undolevels=-123456 +setlocal wincolor= +setlocal nowinfixheight +setlocal nowinfixwidth +set nowrap +setlocal nowrap +setlocal wrapmargin=0 +let s:l = 83 - ((21 * winheight(0) + 21) / 43) +if s:l < 1 | let s:l = 1 | endif +exe s:l +normal! zt +83 +normal! 05| +wincmd w +argglobal +if bufexists("gita/__main__.py") | buffer gita/__main__.py | else | edit gita/__main__.py | endif +let s:cpo_save=&cpo +set cpo&vim +inoremap :call AutoPairsJump() a +inoremap AutoPairsToggle() +inoremap =AutoPairsBackInsert() +inoremap =AutoPairsFastWrap() +inoremap =AutoPairsDelete() +inoremap =AutoPairsDelete() +inoremap =AutoPairsMoveCharacter('''') +inoremap =AutoPairsMoveCharacter('"') +inoremap =AutoPairsMoveCharacter('}') +inoremap =AutoPairsMoveCharacter('{') +inoremap =AutoPairsMoveCharacter(']') +inoremap =AutoPairsMoveCharacter('[') +inoremap =AutoPairsMoveCharacter(')') +inoremap =AutoPairsMoveCharacter('(') +nmap ,hp (GitGutterPreviewHunk) +nmap ,hu (GitGutterUndoHunk) +nmap ,hs (GitGutterStageHunk) +xmap ,hs (GitGutterStageHunk) +inoremap § =AutoPairsMoveCharacter('''') +inoremap ¢ =AutoPairsMoveCharacter('"') +inoremap © =AutoPairsMoveCharacter(')') +inoremap ¨ =AutoPairsMoveCharacter('(') +inoremap î :call AutoPairsJump() a +inoremap ð AutoPairsToggle() +inoremap â =AutoPairsBackInsert() +inoremap ý =AutoPairsMoveCharacter('}') +inoremap û =AutoPairsMoveCharacter('{') +inoremap Ý =AutoPairsMoveCharacter(']') +inoremap Û =AutoPairsMoveCharacter('[') +nmap [c (GitGutterPrevHunk) +nmap ]c (GitGutterNextHunk) +xmap ac (GitGutterTextObjectOuterVisual) +omap ac (GitGutterTextObjectOuterPending) +xmap ic (GitGutterTextObjectInnerVisual) +omap ic (GitGutterTextObjectInnerPending) +noremap :call AutoPairsJump() +noremap :call AutoPairsToggle() +inoremap  =AutoPairsDelete() +inoremap  =AutoPairsFastWrap() +inoremap  =AutoPairsSpace() +inoremap " =AutoPairsInsert('"') +inoremap ' =AutoPairsInsert('''') +inoremap ( =AutoPairsInsert('(') +inoremap ) =AutoPairsInsert(')') +noremap î :call AutoPairsJump() +noremap ð :call AutoPairsToggle() +inoremap [ =AutoPairsInsert('[') +inoremap ] =AutoPairsInsert(']') +inoremap ` =AutoPairsInsert('`') +inoremap { =AutoPairsInsert('{') +inoremap } =AutoPairsInsert('}') +let &cpo=s:cpo_save +unlet s:cpo_save +setlocal autoindent +setlocal backupcopy= +setlocal nobinary +setlocal nobreakindent +setlocal breakindentopt= +setlocal bufhidden= +setlocal buflisted +setlocal buftype= +setlocal nocindent +setlocal cinkeys=0{,0},0),0],:,!^F,o,O,e +setlocal cinoptions= +setlocal cinwords=if,else,while,do,for,switch +set colorcolumn=0 +setlocal colorcolumn=0 +setlocal comments=b:#,fb:- +setlocal commentstring=#\ %s +setlocal complete=.,w,b,u,t,i +setlocal completefunc= +setlocal nocopyindent +setlocal cryptmethod= +setlocal nocursorbind +setlocal nocursorcolumn +setlocal nocursorline +setlocal cursorlineopt=both +setlocal define= +setlocal dictionary= +setlocal nodiff +setlocal equalprg= +setlocal errorformat= +setlocal expandtab +if &filetype != 'python' +setlocal filetype=python +endif +setlocal fixendofline +setlocal foldcolumn=0 +set nofoldenable +setlocal nofoldenable +setlocal foldexpr=0 +setlocal foldignore=# +setlocal foldlevel=0 +setlocal foldmarker={{{,}}} +set foldmethod=indent +setlocal foldmethod=indent +setlocal foldminlines=1 +setlocal foldnestmax=20 +setlocal foldtext=foldtext() +setlocal formatexpr= +setlocal formatoptions=tcq +setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s* +setlocal formatprg= +setlocal grepprg= +setlocal iminsert=0 +setlocal imsearch=-1 +setlocal include=^\\s*\\(from\\|import\\) +setlocal includeexpr=substitute(substitute(substitute(v:fname,b:grandparent_match,b:grandparent_sub,''),b:parent_match,b:parent_sub,''),b:child_match,b:child_sub,'g') +setlocal indentexpr=GetPythonIndent(v:lnum) +setlocal indentkeys=0{,0},0),0],:,!^F,o,O,e,<:>,=elif,=except +setlocal noinfercase +setlocal iskeyword=@,48-57,_,192-255 +setlocal keywordprg=pydoc +setlocal nolinebreak +setlocal nolisp +setlocal lispwords= +setlocal nolist +setlocal makeencoding= +setlocal makeprg= +setlocal matchpairs=(:),{:},[:] +setlocal modeline +setlocal modifiable +setlocal nrformats=bin,octal,hex +set number +setlocal number +setlocal numberwidth=4 +setlocal omnifunc=pythoncomplete#Complete +setlocal path= +setlocal nopreserveindent +setlocal nopreviewwindow +setlocal quoteescape=\\ +setlocal noreadonly +setlocal norelativenumber +setlocal noscrollbind +setlocal scrolloff=-1 +setlocal shiftwidth=4 +setlocal noshortname +setlocal showbreak= +setlocal sidescrolloff=-1 +setlocal signcolumn=auto +setlocal nosmartindent +setlocal softtabstop=4 +set spell +setlocal spell +setlocal spellcapcheck=[.?!]\\_[\\])'\"\ \ ]\\+ +setlocal spellfile= +setlocal spelllang=en +setlocal statusline=%{lightline#link()}%#LightlineLeft_inactive_0#%(\ %t\ %)%#LightlineLeft_inactive_0_1#%#LightlineMiddle_inactive#%=%#LightlineRight_inactive_1_2#%#LightlineRight_inactive_1#%(\ %3p%%\ %)%#LightlineRight_inactive_0_1#%#LightlineRight_inactive_0#%(\ %3l:%-2c\ %) +setlocal suffixesadd=.py +setlocal noswapfile +setlocal synmaxcol=3000 +if &syntax != 'python' +setlocal syntax=python +endif +setlocal tabstop=8 +setlocal tagcase= +setlocal tagfunc= +setlocal tags= +setlocal termwinkey= +setlocal termwinscroll=10000 +setlocal termwinsize= +setlocal textwidth=0 +setlocal thesaurus= +setlocal noundofile +setlocal undolevels=-123456 +setlocal wincolor= +setlocal nowinfixheight +setlocal nowinfixwidth +set nowrap +setlocal nowrap +setlocal wrapmargin=0 +let s:l = 34 - ((32 * winheight(0) + 21) / 43) +if s:l < 1 | let s:l = 1 | endif +exe s:l +normal! zt +34 +normal! 09| +wincmd w +exe 'vert 1resize ' . ((&columns * 94 + 94) / 188) +exe 'vert 2resize ' . ((&columns * 93 + 94) / 188) +tabnext +edit gita/common.py +set splitbelow splitright +set nosplitbelow +set nosplitright +wincmd t +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +argglobal +let s:cpo_save=&cpo +set cpo&vim +inoremap :call AutoPairsJump() a +inoremap AutoPairsToggle() +inoremap =AutoPairsBackInsert() +inoremap =AutoPairsFastWrap() +inoremap =AutoPairsDelete() +inoremap =AutoPairsDelete() +inoremap =AutoPairsMoveCharacter('''') +inoremap =AutoPairsMoveCharacter('"') +inoremap =AutoPairsMoveCharacter('}') +inoremap =AutoPairsMoveCharacter('{') +inoremap =AutoPairsMoveCharacter(']') +inoremap =AutoPairsMoveCharacter('[') +inoremap =AutoPairsMoveCharacter(')') +inoremap =AutoPairsMoveCharacter('(') +nmap ,hp (GitGutterPreviewHunk) +nmap ,hu (GitGutterUndoHunk) +nmap ,hs (GitGutterStageHunk) +xmap ,hs (GitGutterStageHunk) +inoremap § =AutoPairsMoveCharacter('''') +inoremap ¢ =AutoPairsMoveCharacter('"') +inoremap © =AutoPairsMoveCharacter(')') +inoremap ¨ =AutoPairsMoveCharacter('(') +inoremap î :call AutoPairsJump() a +inoremap ð AutoPairsToggle() +inoremap â =AutoPairsBackInsert() +inoremap ý =AutoPairsMoveCharacter('}') +inoremap û =AutoPairsMoveCharacter('{') +inoremap Ý =AutoPairsMoveCharacter(']') +inoremap Û =AutoPairsMoveCharacter('[') +nmap [c (GitGutterPrevHunk) +nmap ]c (GitGutterNextHunk) +xmap ac (GitGutterTextObjectOuterVisual) +omap ac (GitGutterTextObjectOuterPending) +xmap ic (GitGutterTextObjectInnerVisual) +omap ic (GitGutterTextObjectInnerPending) +noremap :call AutoPairsJump() +noremap :call AutoPairsToggle() +inoremap  =AutoPairsDelete() +inoremap  =AutoPairsFastWrap() +inoremap  =AutoPairsSpace() +inoremap " =AutoPairsInsert('"') +inoremap ' =AutoPairsInsert('''') +inoremap ( =AutoPairsInsert('(') +inoremap ) =AutoPairsInsert(')') +noremap î :call AutoPairsJump() +noremap ð :call AutoPairsToggle() +inoremap [ =AutoPairsInsert('[') +inoremap ] =AutoPairsInsert(']') +inoremap ` =AutoPairsInsert('`') +inoremap { =AutoPairsInsert('{') +inoremap } =AutoPairsInsert('}') +let &cpo=s:cpo_save +unlet s:cpo_save +setlocal autoindent +setlocal backupcopy= +setlocal nobinary +setlocal nobreakindent +setlocal breakindentopt= +setlocal bufhidden= +setlocal buflisted +setlocal buftype= +setlocal nocindent +setlocal cinkeys=0{,0},0),0],:,!^F,o,O,e +setlocal cinoptions= +setlocal cinwords=if,else,while,do,for,switch +set colorcolumn=0 +setlocal colorcolumn=0 +setlocal comments=b:#,fb:- +setlocal commentstring=#\ %s +setlocal complete=.,w,b,u,t,i +setlocal completefunc= +setlocal nocopyindent +setlocal cryptmethod= +setlocal nocursorbind +setlocal nocursorcolumn +setlocal nocursorline +setlocal cursorlineopt=both +setlocal define= +setlocal dictionary= +setlocal nodiff +setlocal equalprg= +setlocal errorformat= +setlocal expandtab +if &filetype != 'python' +setlocal filetype=python +endif +setlocal fixendofline +setlocal foldcolumn=0 +set nofoldenable +setlocal nofoldenable +setlocal foldexpr=0 +setlocal foldignore=# +setlocal foldlevel=0 +setlocal foldmarker={{{,}}} +set foldmethod=indent +setlocal foldmethod=indent +setlocal foldminlines=1 +setlocal foldnestmax=20 +setlocal foldtext=foldtext() +setlocal formatexpr= +setlocal formatoptions=tcq +setlocal formatlistpat=^\\s*\\d\\+[\\]:.)}\\t\ ]\\s* +setlocal formatprg= +setlocal grepprg= +setlocal iminsert=0 +setlocal imsearch=-1 +setlocal include=^\\s*\\(from\\|import\\) +setlocal includeexpr=substitute(substitute(substitute(v:fname,b:grandparent_match,b:grandparent_sub,''),b:parent_match,b:parent_sub,''),b:child_match,b:child_sub,'g') +setlocal indentexpr=GetPythonIndent(v:lnum) +setlocal indentkeys=0{,0},0),0],:,!^F,o,O,e,<:>,=elif,=except +setlocal noinfercase +setlocal iskeyword=@,48-57,_,192-255 +setlocal keywordprg=pydoc +setlocal nolinebreak +setlocal nolisp +setlocal lispwords= +setlocal nolist +setlocal makeencoding= +setlocal makeprg= +setlocal matchpairs=(:),{:},[:] +setlocal modeline +setlocal modifiable +setlocal nrformats=bin,octal,hex +set number +setlocal number +setlocal numberwidth=4 +setlocal omnifunc=pythoncomplete#Complete +setlocal path= +setlocal nopreserveindent +setlocal nopreviewwindow +setlocal quoteescape=\\ +setlocal noreadonly +setlocal norelativenumber +setlocal noscrollbind +setlocal scrolloff=-1 +setlocal shiftwidth=4 +setlocal noshortname +setlocal showbreak= +setlocal sidescrolloff=-1 +setlocal signcolumn=auto +setlocal nosmartindent +setlocal softtabstop=4 +set spell +setlocal spell +setlocal spellcapcheck=[.?!]\\_[\\])'\"\ \ ]\\+ +setlocal spellfile= +setlocal spelllang=en +setlocal statusline=%{lightline#link()}%#LightlineLeft_active_0#%(\ %{lightline#mode()}\ %)%{(&paste)?\"|\":\"\"}%(\ %{&paste?\"PASTE\":\"\"}\ %)%#LightlineLeft_active_0_1#%#LightlineLeft_active_1#%(\ %R\ %)%{(&readonly)&&(1||(&modified||!&modifiable))?\"|\":\"\"}%(\ %t\ %)%{(&modified||!&modifiable)?\"|\":\"\"}%(\ %M\ %)%#LightlineLeft_active_1_2#%#LightlineMiddle_active#%=%#LightlineRight_active_2_3#%#LightlineRight_active_2#%(\ %{&ff}\ %)%{1||1?\"|\":\"\"}%(\ %{&fenc!=#\"\"?&fenc:&enc}\ %)%{1?\"|\":\"\"}%(\ %{&ft!=#\"\"?&ft:\"no\ ft\"}\ %)%#LightlineRight_active_1_2#%#LightlineRight_active_1#%(\ %3p%%\ %)%#LightlineRight_active_0_1#%#LightlineRight_active_0#%(\ %3l:%-2c\ %) +setlocal suffixesadd=.py +setlocal noswapfile +setlocal synmaxcol=3000 +if &syntax != 'python' +setlocal syntax=python +endif +setlocal tabstop=8 +setlocal tagcase= +setlocal tagfunc= +setlocal tags= +setlocal termwinkey= +setlocal termwinscroll=10000 +setlocal termwinsize= +setlocal textwidth=0 +setlocal thesaurus= +setlocal noundofile +setlocal undolevels=-123456 +setlocal wincolor= +setlocal nowinfixheight +setlocal nowinfixwidth +set nowrap +setlocal nowrap +setlocal wrapmargin=0 +let s:l = 2 - ((1 * winheight(0) + 21) / 43) +if s:l < 1 | let s:l = 1 | endif +exe s:l +normal! zt +2 +normal! 0 +tabnext 1 +set stal=1 +badd +34 gita/__main__.py +badd +0 gita/utils.py +badd +0 gita/common.py +if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 + silent exe 'bwipe ' . s:wipebuf +endif +unlet! s:wipebuf +set winheight=1 winwidth=20 shortmess=filnxtToOS +set winminheight=1 winminwidth=1 +let s:sx = expand(":p:r")."x.vim" +if filereadable(s:sx) + exe "source " . fnameescape(s:sx) +endif +let &so = s:so_save | let &siso = s:siso_save +nohlsearch +doautoall SessionLoadPost +unlet SessionLoad +" vim: set ft=vim : -- cgit v1.2.3