summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2021-07-17 07:26:34 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2021-07-17 07:26:43 +0000
commit8fd7f9bfed753dbaa5543747569b4c2543aff03d (patch)
tree1b8854ec7e68bb7daf7e8b7db657669d930a99be
parentReleasing debian version 0.12.9-1. (diff)
downloadgita-8fd7f9bfed753dbaa5543747569b4c2543aff03d.tar.xz
gita-8fd7f9bfed753dbaa5543747569b4c2543aff03d.zip
Merging upstream version 0.15.1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.gita-completion.bash18
-rw-r--r--.github/dependabot.yml7
-rw-r--r--MANIFEST.in2
-rw-r--r--README.md210
-rw-r--r--doc/README_CN.md30
-rw-r--r--doc/screenshot.pngbin225737 -> 262997 bytes
-rw-r--r--gita/__main__.py235
-rw-r--r--gita/cmds.json89
-rw-r--r--gita/cmds.yml65
-rw-r--r--gita/common.py17
-rw-r--r--gita/info.py103
-rw-r--r--gita/utils.py261
-rw-r--r--setup.py3
-rw-r--r--tests/clash_path_file4
-rw-r--r--tests/conftest.py1
-rw-r--r--tests/main_path_file2
-rw-r--r--tests/mock_group_file4
-rw-r--r--tests/mock_path_file2
-rw-r--r--tests/test_info.py4
-rw-r--r--tests/test_main.py271
-rw-r--r--tests/test_utils.py85
-rw-r--r--work.vim766
22 files changed, 1806 insertions, 373 deletions
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 <repo-path(s)>`: add repo(s) to `gita`
+- `gita add -a <repo-parent-path(s)>`: add repo(s) in <repo-parent-path(s)> recursively
+ and automatically generate hierarchical groups. See the [customization section](#custom) for more details.
+- `gita add -b <bare-repo-path(s)>`: add bare repo(s) to `gita`. See the [customization section](#custom) for more details on setting custom worktree.
+- `gita add -m <main-repo-path(s)>`: add main repo(s) to `gita`. See the [customization section](#custom) for more details.
+- `gita add -r <repo-parent-path(s)>`: add repo(s) in <repo-parent-path(s)> recursively
- `gita clone <config-file>`: clone repos in `config-file` (generated by `gita freeze`) to current directory.
+- `gita clone -p <config-file>`: 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 <situation> <color>`: Use the specified color for the local-remote situation
+- `gita flags`: flags sub-command
+ - `gita flags set <repo-name> <flags>`: 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 <repo-name(s)> -n <group-name>`: add repo(s) to a new group or existing group
+ - `gita group add <repo-name(s)> -n <group-name>`: 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 <group-name> <new-name>`: change group name
@@ -85,7 +96,7 @@ The bookkeeping sub-commands are
- `gita ls`: display the names of all repos
- `gita ls <repo-name>`: display the absolute path of one repo
- `gita rename <repo-name> <new-name>`: rename a repo
-- `gita rm <repo-name(s)>`: remove repo(s) from `gita` (won't remove files from disk)
+- `gita rm <repo-name(s)>`: 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 <sub-command>` 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 <gita-source-folder>
```
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.
## <a name='superman'></a> 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
## <a name='custom'></a> 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 <repo-name(s)>` 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 <situation> <color>`.
-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 <repo-path(s)>`: 添加库
+- `gita add -a <repo-parent-path(s)>`:
+- `gita add -b <bare-repo-path(s)>`:
+- `gita add -m <main-repo-path(s)>`:
+- `gita add -r <repo-parent-path(s)>`:
+- `gita clone <config-file>`:
+- `gita clone -p <config-file>`:
- `gita context`: 情境命令
- `gita context`: 显示当前的情境
- `gita context none`: 去除情境
- `gita context <group-name>`: 把情境设置成`group-name`, 之后所有的操作只作用到这个组里的库
+- `gita color`:
+ - `gita color [ll]`:
+ - `gita color set <situation> <color>`:
+- `gita flags`:
+ - `gita flags set <repo-name> <flags>`:
+ - `gita flags [ll]`:
+- `gita freeze`:
- `gita group`: 组群命令
- `gita group add <repo-name(s)>`: 把库加入新的或者已经存在的组
- `gita group [ll]`: 显示已有的组和它们的库
- `gita group ls`: 显示已有的组名
- `gita group rename <group-name> <new-name>`: 改组名
- `gita group rm group(s): 删除组
+ - `gita group rmrepo -n <group-name>:
- `gita info`: 显示已用的和未用的信息项
+ - `gita info [ll]`
+ - `gita info add <info-item>`
+ - `gita info rm <info-item>`
- `gita ll`: 显示所有库的状态信息
- `gita ll <group-name>`: 显示一个组群中库的状态信息
- `gita ls`: 显示所有库的名字
@@ -65,7 +82,7 @@
- `gita rm <repo-name(s)>`: 移除库(不会删除文件)
- `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
--- a/doc/screenshot.png
+++ b/doc/screenshot.png
Binary files 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 <silent> <Plug>(-fzf-complete-finish) l
+inoremap <silent> <Plug>CocRefresh =coc#_complete()
+inoremap <silent> <Plug>(fzf-maps-i) :call fzf#vim#maps('i', 0)
+inoremap <expr> <Plug>(fzf-complete-buffer-line) fzf#vim#complete#buffer_line()
+inoremap <expr> <Plug>(fzf-complete-line) fzf#vim#complete#line()
+inoremap <expr> <Plug>(fzf-complete-file-ag) fzf#vim#complete#path('ag -l -g ""')
+inoremap <expr> <Plug>(fzf-complete-file) fzf#vim#complete#path("find . -path '*/\.*' -prune -o -type f -print -o -type l -print | sed 's:^..::'")
+inoremap <expr> <Plug>(fzf-complete-path) fzf#vim#complete#path("find . -path '*/\.*' -prune -o -print | sed '1d;s:^..::'")
+inoremap <expr> <Plug>(fzf-complete-word) fzf#vim#complete#word()
+inoremap <silent> <SNR>20_AutoPairsReturn =AutoPairsReturn()
+inoremap <silent> <expr> <C-Space> coc#refresh()
+inoremap <expr> <S-Tab> pumvisible() ? "\" : "\"
+map! <D-v> *
+nnoremap * *``
+nmap <silent> ,ig <Plug>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 ""<Left>
+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
+vnoremap > >gv
+nnoremap N Nzzzv
+vmap gx <Plug>NetrwBrowseXVis
+nmap gx <Plug>NetrwBrowseX
+nmap g> <Plug>(swap-next)
+nmap g< <Plug>(swap-prev)
+xmap gs <Plug>(swap-interactive)
+nmap gs <Plug>(swap-interactive)
+nmap <silent> gr <Plug>(coc-references)
+nmap <silent> gi <Plug>(coc-implementation)
+nmap <silent> gy <Plug>(coc-type-definition)
+nmap <silent> gd <Plug>(coc-definition)
+nnoremap n nzzzv
+nnoremap <silent> <Plug>(-fzf-complete-finish) a
+nnoremap <Plug>(-fzf-:) :
+nnoremap <Plug>(-fzf-/) /
+nnoremap <Plug>(-fzf-vim-do) :execute g:__fzf_command
+vnoremap <silent> <Plug>NetrwBrowseXVis :call netrw#BrowseXVis()
+nnoremap <silent> <Plug>NetrwBrowseX :call netrw#BrowseX(netrw#GX(),netrw#CheckIfRemote(netrw#GX()))
+onoremap <silent> <Plug>(coc-classobj-a) :call coc#rpc#request('selectSymbolRange', [v:false, '', ['Interface', 'Struct', 'Class']])
+onoremap <silent> <Plug>(coc-classobj-i) :call coc#rpc#request('selectSymbolRange', [v:true, '', ['Interface', 'Struct', 'Class']])
+vnoremap <silent> <Plug>(coc-classobj-a) :call coc#rpc#request('selectSymbolRange', [v:false, visualmode(), ['Interface', 'Struct', 'Class']])
+vnoremap <silent> <Plug>(coc-classobj-i) :call coc#rpc#request('selectSymbolRange', [v:true, visualmode(), ['Interface', 'Struct', 'Class']])
+onoremap <silent> <Plug>(coc-funcobj-a) :call coc#rpc#request('selectSymbolRange', [v:false, '', ['Method', 'Function']])
+onoremap <silent> <Plug>(coc-funcobj-i) :call coc#rpc#request('selectSymbolRange', [v:true, '', ['Method', 'Function']])
+vnoremap <silent> <Plug>(coc-funcobj-a) :call coc#rpc#request('selectSymbolRange', [v:false, visualmode(), ['Method', 'Function']])
+vnoremap <silent> <Plug>(coc-funcobj-i) :call coc#rpc#request('selectSymbolRange', [v:true, visualmode(), ['Method', 'Function']])
+nnoremap <silent> <Plug>(coc-cursors-position) :call coc#rpc#request('cursorsSelect', [bufnr('%'), 'position', 'n'])
+nnoremap <silent> <Plug>(coc-cursors-word) :call coc#rpc#request('cursorsSelect', [bufnr('%'), 'word', 'n'])
+vnoremap <silent> <Plug>(coc-cursors-range) :call coc#rpc#request('cursorsSelect', [bufnr('%'), 'range', visualmode()])
+nnoremap <silent> <Plug>(coc-refactor) :call CocActionAsync('refactor')
+nnoremap <silent> <Plug>(coc-command-repeat) :call CocAction('repeatCommand')
+nnoremap <silent> <Plug>(coc-float-jump) :call coc#float#jump()
+nnoremap <silent> <Plug>(coc-float-hide) :call coc#float#close_all()
+nnoremap <silent> <Plug>(coc-fix-current) :call CocActionAsync('doQuickfix')
+nnoremap <silent> <Plug>(coc-openlink) :call CocActionAsync('openLink')
+nnoremap <silent> <Plug>(coc-references-used) :call CocActionAsync('jumpUsed')
+nnoremap <silent> <Plug>(coc-references) :call CocActionAsync('jumpReferences')
+nnoremap <silent> <Plug>(coc-type-definition) :call CocActionAsync('jumpTypeDefinition')
+nnoremap <silent> <Plug>(coc-implementation) :call CocActionAsync('jumpImplementation')
+nnoremap <silent> <Plug>(coc-declaration) :call CocActionAsync('jumpDeclaration')
+nnoremap <silent> <Plug>(coc-definition) :call CocActionAsync('jumpDefinition')
+nnoremap <silent> <Plug>(coc-diagnostic-prev-error) :call CocActionAsync('diagnosticPrevious', 'error')
+nnoremap <silent> <Plug>(coc-diagnostic-next-error) :call CocActionAsync('diagnosticNext', 'error')
+nnoremap <silent> <Plug>(coc-diagnostic-prev) :call CocActionAsync('diagnosticPrevious')
+nnoremap <silent> <Plug>(coc-diagnostic-next) :call CocActionAsync('diagnosticNext')
+nnoremap <silent> <Plug>(coc-diagnostic-info) :call CocActionAsync('diagnosticInfo')
+nnoremap <silent> <Plug>(coc-format) :call CocActionAsync('format')
+nnoremap <silent> <Plug>(coc-rename) :call CocActionAsync('rename')
+nnoremap <Plug>(coc-codeaction-cursor) :call CocActionAsync('codeAction', 'cursor')
+nnoremap <Plug>(coc-codeaction-line) :call CocActionAsync('codeAction', 'line')
+nnoremap <Plug>(coc-codeaction) :call CocActionAsync('codeAction', '')
+vnoremap <silent> <Plug>(coc-codeaction-selected) :call CocActionAsync('codeAction', visualmode())
+vnoremap <silent> <Plug>(coc-format-selected) :call CocActionAsync('formatSelected', visualmode())
+nnoremap <Plug>(coc-codelens-action) :call CocActionAsync('codeLensAction')
+nnoremap <Plug>(coc-range-select) :call CocActionAsync('rangeSelect', '', v:true)
+vnoremap <silent> <Plug>(coc-range-select-backward) :call CocActionAsync('rangeSelect', visualmode(), v:false)
+vnoremap <silent> <Plug>(coc-range-select) :call CocActionAsync('rangeSelect', visualmode(), v:true)
+noremap <silent> <Plug>(swap-textobject-a) :call swap#textobj#select('a')
+noremap <silent> <Plug>(swap-textobject-i) :call swap#textobj#select('i')
+nnoremap <silent> <Plug>(swap-next) :call swap#prerequisite('n', repeat([['#', '#+1']], v:count1)) g@l
+nnoremap <silent> <Plug>(swap-prev) :call swap#prerequisite('n', repeat([['#', '#-1']], v:count1)) g@l
+xnoremap <silent> <Plug>(swap-interactive) :call swap#prerequisite('x') gvg@
+nnoremap <silent> <Plug>(swap-interactive) :call swap#prerequisite('n') g@l
+onoremap <silent> <Plug>(fzf-maps-o) :call fzf#vim#maps('o', 0)
+xnoremap <silent> <Plug>(fzf-maps-x) :call fzf#vim#maps('x', 0)
+nnoremap <silent> <Plug>(fzf-maps-n) :call fzf#vim#maps('n', 0)
+tnoremap <silent> <Plug>(fzf-normal) 
+tnoremap <silent> <Plug>(fzf-insert) i
+nnoremap <silent> <Plug>(fzf-normal) <Nop>
+nnoremap <silent> <Plug>(fzf-insert) i
+nnoremap <Plug>CtrlSFQuickfixPrompt :CtrlSFQuickfix
+nnoremap <Plug>CtrlSFPrompt :CtrlSF
+nnoremap <silent> <Plug>GitGutterPreviewHunk :call gitgutter#utility#warn('Please change your map <Plug>GitGutterPreviewHunk to <Plug>(GitGutterPreviewHunk)')
+nnoremap <silent> <Plug>(GitGutterPreviewHunk) :GitGutterPreviewHunk
+nnoremap <silent> <Plug>GitGutterUndoHunk :call gitgutter#utility#warn('Please change your map <Plug>GitGutterUndoHunk to <Plug>(GitGutterUndoHunk)')
+nnoremap <silent> <Plug>(GitGutterUndoHunk) :GitGutterUndoHunk
+nnoremap <silent> <Plug>GitGutterStageHunk :call gitgutter#utility#warn('Please change your map <Plug>GitGutterStageHunk to <Plug>(GitGutterStageHunk)')
+nnoremap <silent> <Plug>(GitGutterStageHunk) :GitGutterStageHunk
+xnoremap <silent> <Plug>GitGutterStageHunk :call gitgutter#utility#warn('Please change your map <Plug>GitGutterStageHunk to <Plug>(GitGutterStageHunk)')
+xnoremap <silent> <Plug>(GitGutterStageHunk) :GitGutterStageHunk
+nnoremap <silent> <expr> <Plug>GitGutterPrevHunk &diff ? '[c' : ":\call gitgutter#utility#warn('Please change your map \<Plug>GitGutterPrevHunk to \<Plug>(GitGutterPrevHunk)')\ "
+nnoremap <silent> <expr> <Plug>(GitGutterPrevHunk) &diff ? '[c' : ":\execute v:count1 . 'GitGutterPrevHunk'\ "
+nnoremap <silent> <expr> <Plug>GitGutterNextHunk &diff ? ']c' : ":\call gitgutter#utility#warn('Please change your map \<Plug>GitGutterNextHunk to \<Plug>(GitGutterNextHunk)')\ "
+nnoremap <silent> <expr> <Plug>(GitGutterNextHunk) &diff ? ']c' : ":\execute v:count1 . 'GitGutterNextHunk'\ "
+xnoremap <silent> <Plug>(GitGutterTextObjectOuterVisual) :call gitgutter#hunk#text_object(0)
+xnoremap <silent> <Plug>(GitGutterTextObjectInnerVisual) :call gitgutter#hunk#text_object(1)
+onoremap <silent> <Plug>(GitGutterTextObjectOuterPending) :call gitgutter#hunk#text_object(0)
+onoremap <silent> <Plug>(GitGutterTextObjectInnerPending) :call gitgutter#hunk#text_object(1)
+vmap <BS> "-d
+vmap <D-x> "*d
+vmap <D-c> "*y
+vmap <D-v> "-d"*P
+nmap <D-v> "*P
+inoremap <expr>  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("<sfile>: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 <buffer> <silent> <M-n> :call AutoPairsJump() a
+inoremap <buffer> <silent> <expr> <M-p> AutoPairsToggle()
+inoremap <buffer> <silent> <M-b> =AutoPairsBackInsert()
+inoremap <buffer> <silent> <C-W> =AutoPairsFastWrap()
+inoremap <buffer> <silent> <C-H> =AutoPairsDelete()
+inoremap <buffer> <silent> <BS> =AutoPairsDelete()
+inoremap <buffer> <silent> <M-'> =AutoPairsMoveCharacter('''')
+inoremap <buffer> <silent> <M-"> =AutoPairsMoveCharacter('"')
+inoremap <buffer> <silent> <M-}> =AutoPairsMoveCharacter('}')
+inoremap <buffer> <silent> <M-{> =AutoPairsMoveCharacter('{')
+inoremap <buffer> <silent> <M-]> =AutoPairsMoveCharacter(']')
+inoremap <buffer> <silent> <M-[> =AutoPairsMoveCharacter('[')
+inoremap <buffer> <silent> <M-)> =AutoPairsMoveCharacter(')')
+inoremap <buffer> <silent> <M-(> =AutoPairsMoveCharacter('(')
+nmap <buffer> ,hp <Plug>(GitGutterPreviewHunk)
+nmap <buffer> ,hu <Plug>(GitGutterUndoHunk)
+nmap <buffer> ,hs <Plug>(GitGutterStageHunk)
+xmap <buffer> ,hs <Plug>(GitGutterStageHunk)
+inoremap <buffer> <silent> § =AutoPairsMoveCharacter('''')
+inoremap <buffer> <silent> ¢ =AutoPairsMoveCharacter('"')
+inoremap <buffer> <silent> © =AutoPairsMoveCharacter(')')
+inoremap <buffer> <silent> ¨ =AutoPairsMoveCharacter('(')
+inoremap <buffer> <silent> î :call AutoPairsJump() a
+inoremap <buffer> <silent> <expr> ð AutoPairsToggle()
+inoremap <buffer> <silent> â =AutoPairsBackInsert()
+inoremap <buffer> <silent> ý =AutoPairsMoveCharacter('}')
+inoremap <buffer> <silent> û =AutoPairsMoveCharacter('{')
+inoremap <buffer> <silent> Ý =AutoPairsMoveCharacter(']')
+inoremap <buffer> <silent> Û =AutoPairsMoveCharacter('[')
+nmap <buffer> [c <Plug>(GitGutterPrevHunk)
+nmap <buffer> ]c <Plug>(GitGutterNextHunk)
+xmap <buffer> ac <Plug>(GitGutterTextObjectOuterVisual)
+omap <buffer> ac <Plug>(GitGutterTextObjectOuterPending)
+xmap <buffer> ic <Plug>(GitGutterTextObjectInnerVisual)
+omap <buffer> ic <Plug>(GitGutterTextObjectInnerPending)
+noremap <buffer> <silent> <M-n> :call AutoPairsJump()
+noremap <buffer> <silent> <M-p> :call AutoPairsToggle()
+inoremap <buffer> <silent>  =AutoPairsDelete()
+inoremap <buffer> <silent>  =AutoPairsFastWrap()
+inoremap <buffer> <silent>  =AutoPairsSpace()
+inoremap <buffer> <silent> " =AutoPairsInsert('"')
+inoremap <buffer> <silent> ' =AutoPairsInsert('''')
+inoremap <buffer> <silent> ( =AutoPairsInsert('(')
+inoremap <buffer> <silent> ) =AutoPairsInsert(')')
+noremap <buffer> <silent> î :call AutoPairsJump()
+noremap <buffer> <silent> ð :call AutoPairsToggle()
+inoremap <buffer> <silent> [ =AutoPairsInsert('[')
+inoremap <buffer> <silent> ] =AutoPairsInsert(']')
+inoremap <buffer> <silent> ` =AutoPairsInsert('`')
+inoremap <buffer> <silent> { =AutoPairsInsert('{')
+inoremap <buffer> <silent> } =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 <buffer> <silent> <M-n> :call AutoPairsJump() a
+inoremap <buffer> <silent> <expr> <M-p> AutoPairsToggle()
+inoremap <buffer> <silent> <M-b> =AutoPairsBackInsert()
+inoremap <buffer> <silent> <C-W> =AutoPairsFastWrap()
+inoremap <buffer> <silent> <C-H> =AutoPairsDelete()
+inoremap <buffer> <silent> <BS> =AutoPairsDelete()
+inoremap <buffer> <silent> <M-'> =AutoPairsMoveCharacter('''')
+inoremap <buffer> <silent> <M-"> =AutoPairsMoveCharacter('"')
+inoremap <buffer> <silent> <M-}> =AutoPairsMoveCharacter('}')
+inoremap <buffer> <silent> <M-{> =AutoPairsMoveCharacter('{')
+inoremap <buffer> <silent> <M-]> =AutoPairsMoveCharacter(']')
+inoremap <buffer> <silent> <M-[> =AutoPairsMoveCharacter('[')
+inoremap <buffer> <silent> <M-)> =AutoPairsMoveCharacter(')')
+inoremap <buffer> <silent> <M-(> =AutoPairsMoveCharacter('(')
+nmap <buffer> ,hp <Plug>(GitGutterPreviewHunk)
+nmap <buffer> ,hu <Plug>(GitGutterUndoHunk)
+nmap <buffer> ,hs <Plug>(GitGutterStageHunk)
+xmap <buffer> ,hs <Plug>(GitGutterStageHunk)
+inoremap <buffer> <silent> § =AutoPairsMoveCharacter('''')
+inoremap <buffer> <silent> ¢ =AutoPairsMoveCharacter('"')
+inoremap <buffer> <silent> © =AutoPairsMoveCharacter(')')
+inoremap <buffer> <silent> ¨ =AutoPairsMoveCharacter('(')
+inoremap <buffer> <silent> î :call AutoPairsJump() a
+inoremap <buffer> <silent> <expr> ð AutoPairsToggle()
+inoremap <buffer> <silent> â =AutoPairsBackInsert()
+inoremap <buffer> <silent> ý =AutoPairsMoveCharacter('}')
+inoremap <buffer> <silent> û =AutoPairsMoveCharacter('{')
+inoremap <buffer> <silent> Ý =AutoPairsMoveCharacter(']')
+inoremap <buffer> <silent> Û =AutoPairsMoveCharacter('[')
+nmap <buffer> [c <Plug>(GitGutterPrevHunk)
+nmap <buffer> ]c <Plug>(GitGutterNextHunk)
+xmap <buffer> ac <Plug>(GitGutterTextObjectOuterVisual)
+omap <buffer> ac <Plug>(GitGutterTextObjectOuterPending)
+xmap <buffer> ic <Plug>(GitGutterTextObjectInnerVisual)
+omap <buffer> ic <Plug>(GitGutterTextObjectInnerPending)
+noremap <buffer> <silent> <M-n> :call AutoPairsJump()
+noremap <buffer> <silent> <M-p> :call AutoPairsToggle()
+inoremap <buffer> <silent>  =AutoPairsDelete()
+inoremap <buffer> <silent>  =AutoPairsFastWrap()
+inoremap <buffer> <silent>  =AutoPairsSpace()
+inoremap <buffer> <silent> " =AutoPairsInsert('"')
+inoremap <buffer> <silent> ' =AutoPairsInsert('''')
+inoremap <buffer> <silent> ( =AutoPairsInsert('(')
+inoremap <buffer> <silent> ) =AutoPairsInsert(')')
+noremap <buffer> <silent> î :call AutoPairsJump()
+noremap <buffer> <silent> ð :call AutoPairsToggle()
+inoremap <buffer> <silent> [ =AutoPairsInsert('[')
+inoremap <buffer> <silent> ] =AutoPairsInsert(']')
+inoremap <buffer> <silent> ` =AutoPairsInsert('`')
+inoremap <buffer> <silent> { =AutoPairsInsert('{')
+inoremap <buffer> <silent> } =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 <buffer> <silent> <M-n> :call AutoPairsJump() a
+inoremap <buffer> <silent> <expr> <M-p> AutoPairsToggle()
+inoremap <buffer> <silent> <M-b> =AutoPairsBackInsert()
+inoremap <buffer> <silent> <C-W> =AutoPairsFastWrap()
+inoremap <buffer> <silent> <C-H> =AutoPairsDelete()
+inoremap <buffer> <silent> <BS> =AutoPairsDelete()
+inoremap <buffer> <silent> <M-'> =AutoPairsMoveCharacter('''')
+inoremap <buffer> <silent> <M-"> =AutoPairsMoveCharacter('"')
+inoremap <buffer> <silent> <M-}> =AutoPairsMoveCharacter('}')
+inoremap <buffer> <silent> <M-{> =AutoPairsMoveCharacter('{')
+inoremap <buffer> <silent> <M-]> =AutoPairsMoveCharacter(']')
+inoremap <buffer> <silent> <M-[> =AutoPairsMoveCharacter('[')
+inoremap <buffer> <silent> <M-)> =AutoPairsMoveCharacter(')')
+inoremap <buffer> <silent> <M-(> =AutoPairsMoveCharacter('(')
+nmap <buffer> ,hp <Plug>(GitGutterPreviewHunk)
+nmap <buffer> ,hu <Plug>(GitGutterUndoHunk)
+nmap <buffer> ,hs <Plug>(GitGutterStageHunk)
+xmap <buffer> ,hs <Plug>(GitGutterStageHunk)
+inoremap <buffer> <silent> § =AutoPairsMoveCharacter('''')
+inoremap <buffer> <silent> ¢ =AutoPairsMoveCharacter('"')
+inoremap <buffer> <silent> © =AutoPairsMoveCharacter(')')
+inoremap <buffer> <silent> ¨ =AutoPairsMoveCharacter('(')
+inoremap <buffer> <silent> î :call AutoPairsJump() a
+inoremap <buffer> <silent> <expr> ð AutoPairsToggle()
+inoremap <buffer> <silent> â =AutoPairsBackInsert()
+inoremap <buffer> <silent> ý =AutoPairsMoveCharacter('}')
+inoremap <buffer> <silent> û =AutoPairsMoveCharacter('{')
+inoremap <buffer> <silent> Ý =AutoPairsMoveCharacter(']')
+inoremap <buffer> <silent> Û =AutoPairsMoveCharacter('[')
+nmap <buffer> [c <Plug>(GitGutterPrevHunk)
+nmap <buffer> ]c <Plug>(GitGutterNextHunk)
+xmap <buffer> ac <Plug>(GitGutterTextObjectOuterVisual)
+omap <buffer> ac <Plug>(GitGutterTextObjectOuterPending)
+xmap <buffer> ic <Plug>(GitGutterTextObjectInnerVisual)
+omap <buffer> ic <Plug>(GitGutterTextObjectInnerPending)
+noremap <buffer> <silent> <M-n> :call AutoPairsJump()
+noremap <buffer> <silent> <M-p> :call AutoPairsToggle()
+inoremap <buffer> <silent>  =AutoPairsDelete()
+inoremap <buffer> <silent>  =AutoPairsFastWrap()
+inoremap <buffer> <silent>  =AutoPairsSpace()
+inoremap <buffer> <silent> " =AutoPairsInsert('"')
+inoremap <buffer> <silent> ' =AutoPairsInsert('''')
+inoremap <buffer> <silent> ( =AutoPairsInsert('(')
+inoremap <buffer> <silent> ) =AutoPairsInsert(')')
+noremap <buffer> <silent> î :call AutoPairsJump()
+noremap <buffer> <silent> ð :call AutoPairsToggle()
+inoremap <buffer> <silent> [ =AutoPairsInsert('[')
+inoremap <buffer> <silent> ] =AutoPairsInsert(']')
+inoremap <buffer> <silent> ` =AutoPairsInsert('`')
+inoremap <buffer> <silent> { =AutoPairsInsert('{')
+inoremap <buffer> <silent> } =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("<sfile>: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 :