1
0
Fork 0

Adding upstream version 0.15.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-11 18:41:52 +01:00
parent d01d7be95b
commit f2586667ea
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
22 changed files with 1805 additions and 372 deletions

View file

@ -7,30 +7,36 @@ _gita_completions()
cur=${COMP_WORDS[COMP_CWORD]} cur=${COMP_WORDS[COMP_CWORD]}
cmd=${COMP_WORDS[1]} 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 # this doesn't work for two repos with the same basename
#gita_path=${XDG_CONFIG_HOME:-$HOME/.config}/gita/repo_path #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}` #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 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})) COMPREPLY=($(compgen -W "${commands}" ${cur}))
elif [ $COMP_CWORD -gt 1 ]; then elif [ $COMP_CWORD -gt 1 ]; then
case $cmd in case $cmd in
add) add)
COMPREPLY=($(compgen -d ${cur})) 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 return
;; ;;
*) *)
repos=`gita ls`
COMPREPLY=($(compgen -W "${repos}" ${cur})) COMPREPLY=($(compgen -W "${repos}" ${cur}))
;; ;;
esac esac
fi fi
} }
complete -F _gita_completions gita complete -F _gita_completions gita

7
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

View file

@ -1 +1 @@
include gita/cmds.yml include gita/cmds.json

210
README.md
View file

@ -14,7 +14,7 @@
| | ____ | | | | | ___ | | | ____ | | | | | ___ |
| | \_ ) | | | | | ( ) | | | \_ ) | | | | | ( ) |
| (___) |__) (___ | | | ) ( | | (___) |__) (___ | | | ) ( |
(_______)_______/ )_( |/ \| v0.12 (_______)_______/ )_( |/ \| v0.15
``` ```
# Gita: a command-line tool to manage multiple git repos # 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) ![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` In this screenshot, the `gita ll` command displays the status of all repos.
for the `nowhub` repo, even though we are at the `blog` repo. The `gita remote dotfiles` command translates to `git remote -v`
To see the pre-defined sub-commands, run `gita -h` or take a look at for the `dotfiles` repo, even though we are not in the repo.
[cmds.yml](https://github.com/nosarthur/gita/blob/master/gita/cmds.yml). The `gita fetch` command fetches from all repos and two of them have updates.
To add your own sub-commands, see the [customization section](#custom). 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 `git` command, see the [superman mode section](#superman).
To run arbitrary shell command, see the [shell mode section](#shell). 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 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), [blueshift](https://en.wikipedia.org/wiki/Blueshift) and [redshift](https://en.wikipedia.org/wiki/Redshift),
using green as baseline. 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). See the [customization section](#custom).
The additional status symbols denote The additional status symbols denote
@ -60,7 +62,13 @@ The additional status symbols denote
The bookkeeping sub-commands are The bookkeeping sub-commands are
- `gita add <repo-path(s)>`: add repo(s) to `gita` - `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 <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`: context sub-command
- `gita context`: show current context - `gita context`: show current context
- `gita context none`: remove context - `gita context none`: remove context
@ -68,9 +76,12 @@ The bookkeeping sub-commands are
- `gita color`: color sub-command - `gita color`: color sub-command
- `gita color [ll]`: Show available colors and the current coloring scheme - `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 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 freeze`: print information of all repos such as URL, name, and path.
- `gita group`: group sub-command - `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 [ll]`: display existing groups with repos
- `gita group ls`: display existing group names - `gita group ls`: display existing group names
- `gita group rename <group-name> <new-name>`: change group name - `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`: display the names of all repos
- `gita ls <repo-name>`: display the absolute path of one repo - `gita ls <repo-name>`: display the absolute path of one repo
- `gita rename <repo-name> <new-name>`: rename a 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 - `gita -v`: display gita version
The `git` delegating sub-commands are of two formats 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, By default, only `fetch` and `pull` take optional input. In other words,
`gita fetch` and `gita pull` apply to all repos. `gita fetch` and `gita pull` apply to all repos.
To see the pre-defined sub-commands, run `gita -h` or take a look at 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 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). 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`, with the exception of `log`, `difftool` and `mergetool`,
which require non-trivial user input. 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 ## Installation
@ -124,7 +136,7 @@ pip3 install -e <gita-source-folder>
``` ```
In either case, calling `gita` in terminal may not work, 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" alias gita="python3 -m gita"
@ -140,7 +152,7 @@ Download
[.gita-completion.bash](https://github.com/nosarthur/gita/blob/master/.gita-completion.bash) [.gita-completion.bash](https://github.com/nosarthur/gita/blob/master/.gita-completion.bash)
or or
[.gita-completion.zsh](https://github.com/nosarthur/gita/blob/master/.gita-completion.zsh) [.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 ## <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, For example,
- `gita shell ll` lists contents for all repos - `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 ## <a name='custom'></a> Customization
### user-defined sub-command using yaml file ### define repo group and context
Custom delegating sub-commands can be defined in `$XDG_CONFIG_HOME/gita/cmds.yml` When the project contains several independent but related repos,
(most likely `~/.config/gita/cmds.yml`). 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
```
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. And they shadow the default ones if name collisions exist.
Default delegating sub-commands are defined in 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 For example, `gita stat <repo-name(s)>` is registered as
```yaml ```json
stat: "stat":{
cmd: diff --stat "cmd": "git diff --stat",
help: show edit statistics "help": "show edit statistics"
}
``` ```
which executes `git diff --stat` for the specified repo(s). 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. To disable asynchronous execution, set `disable_async` to be `true`.
See `push` for an example. See the `difftool` example:
To disable asynchronous execution, set the `disable_async` tag to be `true`.
See `difftool` for an example.
If you want a custom command to behave like `gita fetch`, i.e., to apply the ```json
command to all repos when no repo is specified, "difftool":{
set the `allow_all` option to be `true`. "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 to all
repos when no repo is specified, set `allow_all` to be `true`.
For example, the following snippet creates a new command For example, the following snippet creates a new command
`gita comaster [repo-name(s)]` with optional repo name input. `gita comaster [repo-name(s)]` with optional repo name input.
```yaml ```json
comaster: "comaster":{
cmd: checkout master "cmd": "checkout master",
allow_all: true "allow_all": true,
help: checkout the master branch "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 ### 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`. 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>`. 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 ### customize information displayed by the `gita ll` command
You can customize the information displayed by `gita ll`. You can customize the information displayed by `gita ll`.
The used and unused information items are shown with `gita info`, and the 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 ```csv
- branch branch,commit_msg,commit_time
- commit_msg
``` ```
### 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 ## Requirements
Gita requires Python 3.6 or higher, due to the use of Gita requires Python 3.6 or higher, due to the use of
@ -249,9 +374,12 @@ To contribute, you can
- request/implement features - request/implement features
- star/recommend this project - 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) 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 More implementation details are in
[design.md](https://github.com/nosarthur/gita/blob/master/doc/design.md). [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). A step-by-step guide to reproduce this project is [here](https://nosarthur.github.io/side%20project/2019/05/27/gita-breakdown.html).

View file

@ -14,12 +14,12 @@
| | ____ | | | | | ___ | | | ____ | | | | | ___ |
| | \_ ) | | | | | ( ) | | | \_ ) | | | | | ( ) |
| (___) |__) (___ | | | ) ( | | (___) |__) (___ | | | ) ( |
(_______)_______/ )_( |/ \| v0.12 (_______)_______/ )_( |/ \| v0.15
``` ```
# Gita一个管理多个 git 库的命令行工具 # Gita一个管理多个 git 库的命令行工具
这个工具有两个作用: 这个工具有两个功能:
- 并排显示多个库的状态信息,比如分支名,编辑状态,提交信息等 - 并排显示多个库的状态信息,比如分支名,编辑状态,提交信息等
- 在任何目录下(批处理)代理执行 git 指令 - 在任何目录下(批处理)代理执行 git 指令
@ -46,17 +46,34 @@
基础指令: 基础指令:
- `gita add <repo-path(s)>`: 添加库 - `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`: 显示当前的情境 - `gita context`: 显示当前的情境
- `gita context none`: 去除情境 - `gita context none`: 去除情境
- `gita context <group-name>`: 把情境设置成`group-name`, 之后所有的操作只作用到这个组里的库 - `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`: 组群命令
- `gita group add <repo-name(s)>`: 把库加入新的或者已经存在的组 - `gita group add <repo-name(s)>`: 把库加入新的或者已经存在的组
- `gita group [ll]`: 显示已有的组和它们的库 - `gita group [ll]`: 显示已有的组和它们的库
- `gita group ls`: 显示已有的组名 - `gita group ls`: 显示已有的组名
- `gita group rename <group-name> <new-name>`: 改组名 - `gita group rename <group-name> <new-name>`: 改组名
- `gita group rm group(s): 删除组 - `gita group rm group(s): 删除组
- `gita group rmrepo -n <group-name>:
- `gita info`: 显示已用的和未用的信息项 - `gita info`: 显示已用的和未用的信息项
- `gita info [ll]`
- `gita info add <info-item>`
- `gita info rm <info-item>`
- `gita ll`: 显示所有库的状态信息 - `gita ll`: 显示所有库的状态信息
- `gita ll <group-name>`: 显示一个组群中库的状态信息 - `gita ll <group-name>`: 显示一个组群中库的状态信息
- `gita ls`: 显示所有库的名字 - `gita ls`: 显示所有库的名字
@ -65,7 +82,7 @@
- `gita rm <repo-name(s)>`: 移除库(不会删除文件) - `gita rm <repo-name(s)>`: 移除库(不会删除文件)
- `gita -v`: 显示版本号 - `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 help: checkout the master branch
``` ```
另一个自定义功能是针对`gita ll`展示的信息项。 另一个自定义功能是针对`gita ll`展示的信息项。
`gita info`可以展示所有用到的和没用到的信息项,并且可以通过修改`$XDG_CONFIG_HOME/gita/info.yml`支持自定义。举个栗子,默认的信息项显示配置相当于是: `gita info`可以展示所有用到的和没用到的信息项,并且可以通过修改`$XDG_CONFIG_HOME/gita/info.csv`支持自定义。举个栗子,默认的信息项显示配置相当于是:
```yaml ```csv
- branch branch,commit_msg,commit_time
- commit_msg
``` ```
为了创建自己的信息项,命名一个目录为`extra_info_items` 为了创建自己的信息项,命名一个目录为`extra_info_items`
`$XDG_CONFIG_HOME/gita/extra_repo_info.py`中,要把信息项的名字作为字符串映射到方法中,该方法将库的路径作为输入参数。举个栗子: `$XDG_CONFIG_HOME/gita/extra_repo_info.py`中,要把信息项的名字作为字符串映射到方法中,该方法将库的路径作为输入参数。举个栗子:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Before After
Before After

View file

@ -16,22 +16,52 @@ https://github.com/nosarthur/gita/blob/master/.gita-completion.bash
import os import os
import sys import sys
import yaml import csv
import argparse import argparse
import subprocess import subprocess
import pkg_resources import pkg_resources
from itertools import chain from itertools import chain
from pathlib import Path from pathlib import Path
import glob
from . import utils, info, common 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): def f_add(args: argparse.Namespace):
repos = utils.get_repos() repos = utils.get_repos()
paths = args.paths paths = args.paths
if args.recursive: if args.main:
paths = chain.from_iterable(Path(p).glob('**') for p in args.paths) # add to global and tag as main
utils.add_repos(repos, paths) 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): 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) 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): def f_color(args: argparse.Namespace):
cmd = args.color_cmd or 'll' cmd = args.color_cmd or 'll'
if cmd == 'll': # pragma: no cover if cmd == 'll': # pragma: no cover
info.show_colors() info.show_colors()
elif cmd == 'set': elif cmd == 'set':
colors = info.get_color_encoding() colors = info.get_color_encoding()
colors[args.situation] = info.Color[args.color].value colors[args.situation] = args.color
yml_config = common.get_config_fname('color.yml') csv_config = common.get_config_fname('color.csv')
with open(yml_config, 'w') as f: with open(csv_config, 'w', newline='') as f:
yaml.dump(colors, f, default_flow_style=None) writer = csv.DictWriter(f, fieldnames=colors)
writer.writeheader()
writer.writerow(colors)
def f_info(args: argparse.Namespace): def f_info(args: argparse.Namespace):
@ -56,37 +102,53 @@ def f_info(args: argparse.Namespace):
cmd = args.info_cmd or 'll' cmd = args.info_cmd or 'll'
if cmd == 'll': if cmd == 'll':
print('In use:', ','.join(to_display)) 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: if unused:
print('Unused:', ' '.join(unused)) print('Unused:', ','.join(unused))
return return
if cmd == 'add' and args.info_item not in to_display: if cmd == 'add' and args.info_item not in to_display:
to_display.append(args.info_item) to_display.append(args.info_item)
yml_config = common.get_config_fname('info.yml') csv_config = common.get_config_fname('info.csv')
with open(yml_config, 'w') as f: with open(csv_config, 'w', newline='') as f:
yaml.dump(to_display, f, default_flow_style=None) writer = csv.writer(f)
writer.writerow(to_display)
elif cmd == 'rm' and args.info_item in to_display: elif cmd == 'rm' and args.info_item in to_display:
to_display.remove(args.info_item) to_display.remove(args.info_item)
yml_config = common.get_config_fname('info.yml') csv_config = common.get_config_fname('info.csv')
with open(yml_config, 'w') as f: with open(csv_config, 'w', newline='') as f:
yaml.dump(to_display, f, default_flow_style=None) writer = csv.writer(f)
writer.writerow(to_display)
def f_clone(args: argparse.Namespace): def f_clone(args: argparse.Namespace):
path = Path.cwd() 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]) utils.run_async(repo_name, path, ['git', 'clone', url])
for url, repo_name, _ in utils.parse_clone_config(args.fname)) for url, repo_name, _ in utils.parse_clone_config(args.fname))
def f_freeze(_): def f_freeze(_):
repos = utils.get_repos() 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 = '' url = ''
cp = subprocess.run(['git', 'remote', '-v'], cwd=path, capture_output=True) cp = subprocess.run(['git', 'remote', '-v'], cwd=path, capture_output=True)
if cp.returncode == 0: lines = cp.stdout.decode('utf-8').split('\n')
url = cp.stdout.decode('utf-8').split('\n')[0].split()[1] if cp.returncode == 0 and len(lines) > 0:
print(f'{url},{name},{path}') 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): def f_ll(args: argparse.Namespace):
@ -107,7 +169,7 @@ def f_ll(args: argparse.Namespace):
def f_ls(args: argparse.Namespace): def f_ls(args: argparse.Namespace):
repos = utils.get_repos() repos = utils.get_repos()
if args.repo: # one repo, show its path if args.repo: # one repo, show its path
print(repos[args.repo]) print(repos[args.repo]['path'])
else: # show names of all repos else: # show names of all repos
print(' '.join(repos)) print(' '.join(repos))
@ -128,6 +190,11 @@ def f_group(args: argparse.Namespace):
groups[new_name] = groups[gname] groups[new_name] = groups[gname]
del groups[gname] del groups[gname]
utils.write_to_groups_file(groups, 'w') 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': elif cmd == 'rm':
ctx = utils.get_context() ctx = utils.get_context()
for name in args.to_ungroup: for name in args.to_ungroup:
@ -178,12 +245,22 @@ def f_rm(args: argparse.Namespace):
""" """
Unregister repo(s) from gita 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): if os.path.isfile(path_file):
repos = utils.get_repos() 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: for repo in args.repo:
del repos[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): def f_git_cmd(args: argparse.Namespace):
@ -205,21 +282,33 @@ def f_git_cmd(args: argparse.Namespace):
for r in groups[k]: for r in groups[k]:
chosen[r] = repos[r] chosen[r] = repos[r]
repos = chosen repos = chosen
cmds = ['git'] + args.cmd per_repo_cmds = []
if len(repos) == 1 or cmds[1] in args.async_blacklist: for prop in repos.values():
for path 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) print(path)
subprocess.run(cmds, cwd=path) subprocess.run(cmds, cwd=path, shell=args.shell)
else: # run concurrent subprocesses else: # run concurrent subprocesses
# Async execution cannot deal with multiple repos' user name/password. # Async execution cannot deal with multiple repos' user name/password.
# Here we shut off any user input in the async execution, and re-run # Here we shut off any user input in the async execution, and re-run
# the failed ones synchronously. # the failed ones synchronously.
errors = utils.exec_async_tasks( 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: for path in errors:
if path: if path:
print(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): def f_shell(args):
@ -249,10 +338,10 @@ def f_shell(args):
for r in groups[k]: for r in groups[k]:
chosen[r] = repos[r] chosen[r] = repos[r]
repos = chosen repos = chosen
cmds = args.man[i:] cmds = ' '.join(args.man[i:]) # join the shell command into a single string
for name, path in repos.items(): for name, prop in repos.items():
# TODO: pull this out as a function # 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, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
print(utils.format_output(got.stdout.decode(), name)) print(utils.format_output(got.stdout.decode(), name))
@ -271,8 +360,9 @@ def f_super(args):
names.append(word) names.append(word)
else: else:
break break
args.cmd = args.man[i:] args.cmd = ['git'] + args.man[i:]
args.repo = names args.repo = names
args.shell = False
f_git_cmd(args) f_git_cmd(args)
@ -292,9 +382,17 @@ def main(argv=None):
# bookkeeping sub-commands # bookkeeping sub-commands
p_add = subparsers.add_parser('add', description='add repo(s)', p_add = subparsers.add_parser('add', description='add repo(s)',
help='add repo(s)') help='add repo(s)')
p_add.add_argument('paths', nargs='+', help="repo(s) to add") p_add.add_argument('paths', nargs='+', type=os.path.abspath, help="repo(s) to add")
p_add.add_argument('-r', dest='recursive', action='store_true', xgroup = p_add.add_mutually_exclusive_group()
help="recursively add repo(s) in the given path.") 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_add.set_defaults(func=f_add)
p_rm = subparsers.add_parser('rm', description='remove repo(s)', p_rm = subparsers.add_parser('rm', description='remove repo(s)',
@ -305,15 +403,22 @@ def main(argv=None):
help="remove the chosen repo(s)") help="remove the chosen repo(s)")
p_rm.set_defaults(func=f_rm) 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_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', p_clone.add_argument('fname',
help='config file. Its content should be the output of `gita freeze`.') 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_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( p_rename.add_argument(
'repo', 'repo',
nargs=1, nargs=1,
@ -322,8 +427,25 @@ def main(argv=None):
p_rename.add_argument('new_name', help="new name") p_rename.add_argument('new_name', help="new name")
p_rename.set_defaults(func=f_rename) 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', 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) p_color.set_defaults(func=f_color)
color_cmds = p_color.add_subparsers(dest='color_cmd', color_cmds = p_color.add_subparsers(dest='color_cmd',
help='additional help with sub-command -h') help='additional help with sub-command -h')
@ -339,7 +461,8 @@ def main(argv=None):
help="available colors") help="available colors")
p_info = subparsers.add_parser('info', 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) p_info.set_defaults(func=f_info)
info_cmds = p_info.add_subparsers(dest='info_cmd', info_cmds = p_info.add_subparsers(dest='info_cmd',
help='additional help with sub-command -h') 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') description='show used and unused information items of the ll sub-command')
info_cmds.add_parser('add', description='Enable information item.' info_cmds.add_parser('add', description='Enable information item.'
).add_argument('info_item', ).add_argument('info_item',
choices=('branch', 'commit_msg', 'path'), choices=info.ALL_INFO_ITEMS,
help="information item to add") help="information item to add")
info_cmds.add_parser('rm', description='Disable information item.' info_cmds.add_parser('rm', description='Disable information item.'
).add_argument('info_item', ).add_argument('info_item',
choices=('branch', 'commit_msg', 'path'), choices=info.ALL_INFO_ITEMS,
help="information item to delete") help="information item to delete")
@ -379,6 +502,7 @@ def main(argv=None):
p_ll.set_defaults(func=f_ll) p_ll.set_defaults(func=f_ll)
p_context = subparsers.add_parser('context', p_context = subparsers.add_parser('context',
help='set context',
description='Set and remove context. A context is a group.' description='Set and remove context. A context is a group.'
' When set, all operations apply only to repos in that group.') ' When set, all operations apply only to repos in that group.')
p_context.add_argument('choice', p_context.add_argument('choice',
@ -388,7 +512,8 @@ def main(argv=None):
p_context.set_defaults(func=f_context) p_context.set_defaults(func=f_context)
p_ls = subparsers.add_parser( 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', p_ls.add_argument('repo',
nargs='?', nargs='?',
choices=utils.get_repos(), choices=utils.get_repos(),
@ -396,7 +521,8 @@ def main(argv=None):
p_ls.set_defaults(func=f_ls) p_ls.set_defaults(func=f_ls)
p_group = subparsers.add_parser( 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) p_group.set_defaults(func=f_group)
group_cmds = p_group.add_subparsers(dest='group_cmd', group_cmds = p_group.add_subparsers(dest='group_cmd',
help='additional help with sub-command -h') help='additional help with sub-command -h')
@ -410,6 +536,7 @@ def main(argv=None):
help="repo(s) to be grouped") help="repo(s) to be grouped")
pg_add.add_argument('-n', '--name', pg_add.add_argument('-n', '--name',
dest='gname', dest='gname',
type=_group_name,
metavar='group-name', metavar='group-name',
required=True, required=True,
help="group name") help="group name")
@ -429,6 +556,7 @@ def main(argv=None):
choices=utils.get_groups(), choices=utils.get_groups(),
help="existing group to rename") help="existing group to rename")
pg_rename.add_argument('new_name', metavar='new-name', pg_rename.add_argument('new_name', metavar='new-name',
type=_group_name,
help="new group name") help="new group name")
group_cmds.add_parser('rm', group_cmds.add_parser('rm',
description='Remove group(s).').add_argument('to_ungroup', description='Remove group(s).').add_argument('to_ungroup',
@ -439,6 +567,7 @@ def main(argv=None):
# superman mode # superman mode
p_super = subparsers.add_parser( p_super = subparsers.add_parser(
'super', 'super',
help='run any git command/alias',
description='Superman mode: delegate any git command/alias in specified or ' description='Superman mode: delegate any git command/alias in specified or '
'all repo(s).\n' 'all repo(s).\n'
'Examples:\n \t gita super myrepo1 commit -am "fix a bug"\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( p_super.add_argument(
'man', 'man',
nargs=argparse.REMAINDER, 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 " "Example: gita super myrepo1 diff --name-only --staged "
"Another: gita super checkout master ") "Another: gita super checkout master ")
p_super.set_defaults(func=f_super) p_super.set_defaults(func=f_super)
@ -454,6 +583,7 @@ def main(argv=None):
# shell mode # shell mode
p_shell = subparsers.add_parser( p_shell = subparsers.add_parser(
'shell', 'shell',
help='run any shell command',
description='shell mode: delegate any shell command in specified or ' description='shell mode: delegate any shell command in specified or '
'all repo(s).\n' 'all repo(s).\n'
'Examples:\n \t gita shell pwd\n' 'Examples:\n \t gita shell pwd\n'
@ -470,7 +600,7 @@ def main(argv=None):
cmds = utils.get_cmds_from_files() cmds = utils.get_cmds_from_files()
for name, data in cmds.items(): for name, data in cmds.items():
help = data.get('help') help = data.get('help')
cmd = data.get('cmd') or name cmd = data['cmd']
if data.get('allow_all'): if data.get('allow_all'):
choices = utils.get_choices() choices = utils.get_choices()
nargs = '*' nargs = '*'
@ -481,7 +611,14 @@ def main(argv=None):
help += ' for the chosen repo(s) or group(s)' help += ' for the chosen repo(s) or group(s)'
sp = subparsers.add_parser(name, description=help) sp = subparsers.add_parser(name, description=help)
sp.add_argument('repo', nargs=nargs, choices=choices, help=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) args = p.parse_args(argv)

89
gita/cmds.json Normal file
View file

@ -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"
}
}

View file

@ -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

View file

@ -1,16 +1,17 @@
import os import os
def get_config_dir() -> str: def get_config_dir(root=None) -> str:
parent = os.environ.get('XDG_CONFIG_HOME') or os.path.join( if root is None:
os.path.expanduser('~'), '.config') root = os.environ.get('XDG_CONFIG_HOME') or os.path.join(
root = os.path.join(parent, "gita") os.path.expanduser('~'), '.config')
return root 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. Return the file name that stores the repo locations.
""" """
root = get_config_dir() return os.path.join(get_config_dir(root), fname)
return os.path.join(root, fname)

View file

@ -1,5 +1,5 @@
import os import os
import sys import csv
import yaml import yaml
import subprocess import subprocess
from enum import Enum from enum import Enum
@ -31,40 +31,42 @@ class Color(str, Enum):
b_purple = '\x1b[35;1m' b_purple = '\x1b[35;1m'
b_cyan = '\x1b[36;1m' b_cyan = '\x1b[36;1m'
b_white = '\x1b[37;1m' b_white = '\x1b[37;1m'
underline = '\x1B[4m'
def show_colors(): # pragma: no cover def show_colors(): # pragma: no cover
""" """
""" """
names = {c.value: c.name for c in Color}
for i, c in enumerate(Color, start=1): 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='') print(f'{c.value}{c.name:<8} ', end='')
if i % 9 == 0: if i % 9 == 0:
print() print()
print(f'{Color.end}') print(f'{Color.end}')
for situation, c in sorted(get_color_encoding().items()): 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() @lru_cache()
def get_color_encoding() -> Dict[str, str]: def get_color_encoding() -> Dict[str, str]:
""" """
Return color scheme for different local/remote situations. Return color scheme for different local/remote situations.
In the format of {situation: color name}
""" """
# custom settings # custom settings
yml_config = Path(common.get_config_fname('color.yml')) csv_config = Path(common.get_config_fname('color.csv'))
if yml_config.is_file(): if csv_config.is_file():
with open(yml_config, 'r') as stream: with open(csv_config, 'r') as f:
colors = yaml.load(stream, Loader=yaml.FullLoader) reader = csv.DictReader(f)
colors = next(reader)
else: else:
colors = { colors = {
'no-remote': Color.white.value, 'no-remote': Color.white.name,
'in-sync': Color.green.value, 'in-sync': Color.green.name,
'diverged': Color.red.value, 'diverged': Color.red.name,
'local-ahead': Color.purple.value, 'local-ahead': Color.purple.name,
'remote-ahead': Color.yellow.value, 'remote-ahead': Color.yellow.name,
} }
return colors return colors
@ -80,6 +82,7 @@ def get_info_funcs() -> List[Callable[[str], str]]:
all_info_items = { all_info_items = {
'branch': get_repo_status, 'branch': get_repo_status,
'commit_msg': get_commit_msg, 'commit_msg': get_commit_msg,
'commit_time': get_commit_time,
'path': get_path, 'path': get_path,
} }
return [all_info_items[k] for k in to_display] 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. Return the information items to be displayed in the `gita ll` command.
""" """
# custom settings # custom settings
yml_config = Path(common.get_config_fname('info.yml')) csv_config = Path(common.get_config_fname('info.csv'))
if yml_config.is_file(): if csv_config.is_file():
with open(yml_config, 'r') as stream: with open(csv_config, 'r') as f:
display_items = yaml.load(stream, Loader=yaml.FullLoader) reader = csv.reader(f)
display_items = next(reader)
display_items = [x for x in display_items if x in ALL_INFO_ITEMS] display_items = [x for x in display_items if x in ALL_INFO_ITEMS]
else: else:
# default settings # default settings
display_items = ['branch', 'commit_msg'] display_items = ['branch', 'commit_msg', 'commit_time']
return display_items return display_items
def get_path(path): def get_path(prop: Dict[str, str]) -> str:
return f'{Color.cyan}{path}{Color.end}' return f'{Color.cyan}{prop["path"]}{Color.end}'
# TODO: do we need to add the flags here too?
def get_head(path: str) -> str: 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, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
universal_newlines=True, universal_newlines=True,
@ -114,12 +120,12 @@ def get_head(path: str) -> str:
return result.stdout.strip() 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 Return the return code of git diff `args` in quiet mode
""" """
result = subprocess.run( result = subprocess.run(
['git', 'diff', '--quiet'] + args, ['git'] + flags + ['diff', '--quiet'] + args,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
return result.returncode return result.returncode
@ -135,50 +141,68 @@ def get_common_commit() -> str:
return result.stdout.strip() return result.stdout.strip()
def has_untracked() -> bool: def has_untracked(flags: List[str]) -> bool:
""" """
Return True if untracked file/folder exists 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) stdout=subprocess.PIPE)
return bool(result.stdout) 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. Return the last commit message.
""" """
# `git show-branch --no-name HEAD` is faster than `git show -s --format=%s` # `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, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
universal_newlines=True, universal_newlines=True,
cwd=path) cwd=prop['path'])
return result.stdout.strip() return result.stdout.strip()
def get_repo_status(path: str, no_colors=False) -> str: def get_commit_time(prop: Dict[str, str]) -> str:
head = get_head(path) """
dirty, staged, untracked, color = _get_repo_status(path, no_colors) 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: if color:
return f'{color}{head+" "+dirty+staged+untracked:<10}{Color.end}' return f'{color}{head+" "+dirty+staged+untracked:<10}{Color.end}'
return f'{head+" "+dirty+staged+untracked:<10}' 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 Return the status of one repo
""" """
path = prop['path']
flags = prop['flags']
os.chdir(path) os.chdir(path)
dirty = '*' if run_quiet_diff([]) else '' dirty = '*' if run_quiet_diff(flags, []) else ''
staged = '+' if run_quiet_diff(['--cached']) else '' staged = '+' if run_quiet_diff(flags, ['--cached']) else ''
untracked = '_' if has_untracked() else '' untracked = '_' if has_untracked(flags) else ''
if no_colors: if no_colors:
return dirty, staged, untracked, '' return dirty, staged, untracked, ''
colors = get_color_encoding() colors = {situ: Color[name].value
diff_returncode = run_quiet_diff(['@{u}', '@{0}']) for situ, name in get_color_encoding().items()}
diff_returncode = run_quiet_diff(flags, ['@{u}', '@{0}'])
has_no_remote = diff_returncode == 128 has_no_remote = diff_returncode == 128
has_no_diff = diff_returncode == 0 has_no_diff = diff_returncode == 0
if has_no_remote: if has_no_remote:
@ -187,9 +211,9 @@ def _get_repo_status(path: str, no_colors: bool) -> Tuple[str]:
color = colors['in-sync'] color = colors['in-sync']
else: else:
common_commit = get_common_commit() common_commit = get_common_commit()
outdated = run_quiet_diff(['@{u}', common_commit]) outdated = run_quiet_diff(flags, ['@{u}', common_commit])
if outdated: 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'] color = colors['diverged'] if diverged else colors['remote-ahead']
else: # local is ahead of remote else: # local is ahead of remote
color = colors['local-ahead'] color = colors['local-ahead']
@ -199,5 +223,6 @@ def _get_repo_status(path: str, no_colors: bool) -> Tuple[str]:
ALL_INFO_ITEMS = { ALL_INFO_ITEMS = {
'branch': get_repo_status, 'branch': get_repo_status,
'commit_msg': get_commit_msg, 'commit_msg': get_commit_msg,
'commit_time': get_commit_time,
'path': get_path, 'path': get_path,
} }

View file

@ -1,15 +1,53 @@
import os import os
import yaml import json
import csv
import asyncio import asyncio
import platform import platform
import subprocess
from functools import lru_cache, partial from functools import lru_cache, partial
from pathlib import Path 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 info
from . import common from . import common
# TODO: python3.9 pathlib has is_relative_to() function
def is_relative_to(kid: str, parent: str) -> bool:
"""
Both the `kid` and `parent` should be absolute path
"""
return parent == os.path.commonpath((kid, parent))
@lru_cache()
def get_repos(root=None) -> Dict[str, Dict[str, str]]:
"""
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('repos.csv', root)
repos = {}
if os.path.isfile(path_file) and os.stat(path_file).st_size > 0:
with open(path_file) as f:
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() @lru_cache()
def get_context() -> Union[Path, None]: def get_context() -> Union[Path, None]:
""" """
@ -21,42 +59,18 @@ def get_context() -> Union[Path, None]:
return matches[0] if matches else None return matches[0] if matches else None
@lru_cache()
def get_repos() -> Dict[str, str]:
"""
Return a `dict` of repo name to repo absolute path
"""
path_file = common.get_config_fname('repo_path')
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
return repos
@lru_cache() @lru_cache()
def get_groups() -> Dict[str, List[str]]: def get_groups() -> Dict[str, List[str]]:
""" """
Return a `dict` of group name to repo names. Return a `dict` of group name to repo names.
""" """
fname = common.get_config_fname('groups.yml') fname = common.get_config_fname('groups.csv')
groups = {} groups = {}
# Each line is a repo path and repo name separated by , # Each line is a repo path and repo name separated by ,
if os.path.isfile(fname) and os.stat(fname).st_size > 0: if os.path.isfile(fname) and os.stat(fname).st_size > 0:
with open(fname, 'r') as f: 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 return groups
@ -75,10 +89,12 @@ def get_choices() -> List[Union[str, None]]:
return choices 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. 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` # An alternative is to call `git rev-parse --is-inside-work-tree`
# I don't see why that one is better yet. # 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. # 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` # `git rev-parse --git-common-dir`
loc = os.path.join(path, '.git') loc = os.path.join(path, '.git')
# TODO: we can display the worktree repos in a different font. # TODO: we can display the worktree repos in a different font.
return os.path.exists(loc) 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):
def rename_repo(repos: Dict[str, str], repo: str, new_name: str):
""" """
Write new repo name to file 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] del repos[repo]
repos[new_name] = path repos[new_name] = prop
write_to_repo_file(repos, 'w') # 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, str], mode: str): 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()) data = [(prop['path'], name, prop['type'], ' '.join(prop['flags']))
fname = common.get_config_fname('repo_path') for name, prop in repos.items()]
fname = common.get_config_fname('repos.csv', root)
os.makedirs(os.path.dirname(fname), exist_ok=True) os.makedirs(os.path.dirname(fname), exist_ok=True)
with open(fname, mode) as f: with open(fname, mode, newline='') as f:
f.write(data) 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): 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) os.makedirs(os.path.dirname(fname), exist_ok=True)
if not groups: # all groups are deleted if not groups: # all groups are deleted
open(fname, 'w').close() open(fname, 'w').close()
else: else:
with open(fname, mode) as f: with open(fname, mode, newline='') as f:
yaml.dump(groups, f, default_flow_style=None) data = [
(group, ' '.join(repos))
for group, repos in groups.items()
]
writer = csv.writer(f, delimiter=':', quotechar='"', quoting=csv.QUOTE_MINIMAL)
writer.writerows(data)
def add_repos(repos: Dict[str, str], new_paths: List[str]): def _make_name(path: str, repos: Dict[str, Dict[str, str]],
name_counts: Counter) -> str:
""" """
Write new repo paths to file 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 _get_repo_type(path, repo_type, root) -> str:
"""
"""
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 @param repos: name -> path
""" """
existing_paths = set(repos.values()) existing_paths = {prop['path'] for prop in repos.values()}
new_paths = set(os.path.abspath(p) for p in new_paths if is_git(p)) new_paths = {p for p in new_paths if is_git(p, is_bare)}
new_paths = new_paths - existing_paths new_paths = new_paths - existing_paths
new_repos = {}
if new_paths: if new_paths:
print(f"Found {len(new_paths)} new repo(s).") print(f"Found {len(new_paths)} new repo(s).")
new_repos = { name_counts = Counter(
os.path.basename(os.path.normpath(path)): path os.path.basename(os.path.normpath(p)) for p in new_paths
for path in new_paths} )
write_to_repo_file(new_repos, 'a+') 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: else:
print('No new repos found!') 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]]: 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 Run `cmds` asynchronously in `path` directory. Return the `path` if
execution fails. execution fails.
""" """
# TODO: deprecated since 3.8, will be removed in 3.10
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
*cmds, *cmds,
stdin=asyncio.subprocess.DEVNULL, stdin=asyncio.subprocess.DEVNULL,
@ -199,7 +329,7 @@ def exec_async_tasks(tasks: List[Coroutine]) -> List[Union[None, str]]:
return errors 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 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) funcs[idx] = partial(get_repo_status, no_colors=True)
for name in sorted(repos): for name in sorted(repos):
path = repos[name] info_items = ' '.join(f(repos[name]) for f in funcs)
info_items = ' '.join(f(path) for f in funcs) if repos[name]['type'] == 'm':
yield f'{name:<{name_width}}{info_items}' # 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]]: 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 # default config file
fname = os.path.join(os.path.dirname(__file__), "cmds.yml") fname = os.path.join(os.path.dirname(__file__), "cmds.json")
with open(fname, 'r') as stream: with open(fname, 'r') as f:
cmds = yaml.load(stream, Loader=yaml.FullLoader) cmds = json.load(f)
# custom config file # custom config file
root = common.get_config_dir() root = common.get_config_dir()
fname = os.path.join(root, 'cmds.yml') fname = os.path.join(root, 'cmds.json')
custom_cmds = {} custom_cmds = {}
if os.path.isfile(fname) and os.path.getsize(fname): if os.path.isfile(fname) and os.path.getsize(fname):
with open(fname, 'r') as stream: with open(fname, 'r') as f:
custom_cmds = yaml.load(stream, Loader=yaml.FullLoader) custom_cmds = json.load(f)
# custom commands shadow default ones # custom commands shadow default ones
cmds.update(custom_cmds) cmds.update(custom_cmds)

View file

@ -7,7 +7,7 @@ with open('README.md', encoding='utf-8') as f:
setup( setup(
name='gita', name='gita',
packages=['gita'], packages=['gita'],
version='0.12.7', version='0.15.1',
license='MIT', license='MIT',
description='Manage multiple git repos with sanity', description='Manage multiple git repos with sanity',
long_description=long_description, long_description=long_description,
@ -18,7 +18,6 @@ setup(
author='Dong Zhou', author='Dong Zhou',
author_email='zhou.dong@gmail.com', author_email='zhou.dong@gmail.com',
entry_points={'console_scripts': ['gita = gita.__main__:main']}, entry_points={'console_scripts': ['gita = gita.__main__:main']},
install_requires=['pyyaml>=5.1'],
python_requires='~=3.6', python_requires='~=3.6',
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",

View file

@ -1,3 +1,3 @@
/a/bcd/repo1,repo1 /a/bcd/repo1,repo1,
/e/fgh/repo2,repo2 /e/fgh/repo2,repo2,,--haha --pp
/root/x/repo1,repo1 /root/x/repo1,repo1

View file

@ -11,6 +11,7 @@ def fullpath(fname: str):
PATH_FNAME = fullpath('mock_path_file') PATH_FNAME = fullpath('mock_path_file')
PATH_FNAME_EMPTY = fullpath('empty_path_file') PATH_FNAME_EMPTY = fullpath('empty_path_file')
PATH_FNAME_CLASH = fullpath('clash_path_file') PATH_FNAME_CLASH = fullpath('clash_path_file')
PATH_FNAME_MAIN = fullpath('main_path_file')
GROUP_FNAME = fullpath('mock_group_file') GROUP_FNAME = fullpath('mock_group_file')
def async_mock(): def async_mock():

2
tests/main_path_file Normal file
View file

@ -0,0 +1,2 @@
/path/to/main/,main1,m
/xxx/xx,xx,

View file

@ -1,2 +1,2 @@
xx: [a, b] xx:a b
yy: [a, c, d] yy:a c d

View file

@ -1,4 +1,4 @@
/a/bcd/repo1,repo1 /a/bcd/repo1,repo1
/a/b/c/repo3,xxx /a/b/c/repo3,xxx,,
/e/fgh/repo2,repo2 /e/fgh/repo2,repo2

View file

@ -8,9 +8,9 @@ from gita import info
def test_run_quiet_diff(mock_run): def test_run_quiet_diff(mock_run):
mock_return = MagicMock() mock_return = MagicMock()
mock_run.return_value = mock_return 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( mock_run.assert_called_once_with(
['git', 'diff', '--quiet', 'my', 'args'], ['git', '--flags', 'diff', '--quiet', 'my', 'args'],
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
assert got == mock_return.returncode assert got == mock_return.returncode

View file

@ -1,28 +1,102 @@
import os
import pytest import pytest
from unittest.mock import patch, mock_open from unittest.mock import patch
from pathlib import Path from pathlib import Path
import argparse import argparse
import asyncio
import shlex import shlex
from gita import __main__ from gita import __main__
from gita import utils, info from gita import utils, info, common
from conftest import ( 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, 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: class TestLsLl:
@patch('gita.common.get_config_fname') @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 functional test
""" """
# avoid modifying the local configuration # avoid modifying the local configuration
def side_effect(input): def side_effect(input, _=None):
return tmp_path / f'{input}.txt' return tmp_path / f'{input}.txt'
#mock_path_fname.return_value = tmp_path / 'path_config.txt'
mock_path_fname.side_effect = side_effect mock_path_fname.side_effect = side_effect
utils.get_repos.cache_clear()
__main__.main(['add', '.']) __main__.main(['add', '.'])
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert err == '' assert err == ''
@ -52,11 +126,11 @@ class TestLsLl:
__main__.main(['ls', 'gita']) __main__.main(['ls', 'gita'])
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert err == '' 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', monkeypatch.setattr(utils, 'get_repos',
lambda: {'repo1': '/a/', 'repo2': '/b/'}) lambda: {'repo1': {'path': '/a/'}, 'repo2': {'path': '/b/'}})
monkeypatch.setattr(utils, 'describe', lambda x: x) monkeypatch.setattr(utils, 'describe', lambda x: x)
__main__.main(['ls']) __main__.main(['ls'])
out, err = capfd.readouterr() out, err = capfd.readouterr()
@ -69,21 +143,24 @@ class TestLsLl:
@pytest.mark.parametrize('path_fname, expected', [ @pytest.mark.parametrize('path_fname, expected', [
(PATH_FNAME, (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_EMPTY, ""),
(PATH_FNAME_MAIN,
'\x1b[4mmain1\x1b[0m cmaster dsu\x1b[0m msg \nxx cmaster dsu\x1b[0m msg \n'),
(PATH_FNAME_CLASH, (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.utils.is_git', return_value=True)
@patch('gita.info.get_head', return_value="master") @patch('gita.info.get_head', return_value="master")
@patch('gita.info._get_repo_status', return_value=("d", "s", "u", "c")) @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_msg', return_value="msg")
@patch('gita.info.get_commit_time', return_value="")
@patch('gita.common.get_config_fname') @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): expected, capfd):
def side_effect(input): def side_effect(input, _=None):
if input == 'repo_path': if input == 'repos.csv':
return path_fname return path_fname
return f'/{input}' return f'/{input}'
mock_path_fname.side_effect = side_effect mock_path_fname.side_effect = side_effect
@ -95,25 +172,63 @@ class TestLsLl:
assert out == expected assert out == expected
@pytest.mark.parametrize('input, expected', [
({'repo1': {'path': '/a/'}, 'repo2': {'path': '/b/'}}, ''),
])
@patch('subprocess.run') @patch('subprocess.run')
@patch('gita.utils.get_repos', return_value={'repo1': '/a/', 'repo2': '/b/'}) @patch('gita.utils.get_repos')
def test_freeze(_, mock_run, capfd): def test_freeze(mock_repos, mock_run, input, expected, capfd):
mock_repos.return_value = input
__main__.main(['freeze']) __main__.main(['freeze'])
assert mock_run.call_count == 2 assert mock_run.call_count == 2
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert err == '' 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('os.path.isfile', return_value=True)
@patch('gita.common.get_config_fname', return_value='some path') @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') @patch('gita.utils.write_to_repo_file')
def test_rm(mock_write, *_): def test_rm(mock_write, *_):
args = argparse.Namespace() args = argparse.Namespace()
args.repo = ['repo1'] args.repo = ['repo1']
__main__.f_rm(args) __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(): def test_not_add():
@ -121,17 +236,19 @@ def test_not_add():
__main__.main(['add', '/home/some/repo/']) __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') @patch('subprocess.run')
def test_fetch(mock_run, *_): def test_fetch(mock_run, *_):
asyncio.set_event_loop(asyncio.new_event_loop())
__main__.main(['fetch']) __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( @patch(
'gita.utils.get_repos', return_value={ 'gita.utils.get_repos', return_value={
'repo1': '/a/bc', 'repo1': {'path': '/a/bc', 'flags': []},
'repo2': '/d/efg' 'repo2': {'path': '/d/efg', 'flags': []}
}) })
@patch('gita.utils.run_async', new=async_mock()) @patch('gita.utils.run_async', new=async_mock())
@patch('subprocess.run') @patch('subprocess.run')
@ -149,28 +266,28 @@ def test_async_fetch(*_):
'diff --name-only --staged', 'diff --name-only --staged',
"commit -am 'lala kaka'", "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') @patch('subprocess.run')
def test_superman(mock_run, _, input): def test_superman(mock_run, _, input):
mock_run.reset_mock() mock_run.reset_mock()
args = ['super', 'repo7'] + shlex.split(input) args = ['super', 'repo7'] + shlex.split(input)
__main__.main(args) __main__.main(args)
expected_cmds = ['git'] + shlex.split(input) 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', [ @pytest.mark.parametrize('input', [
'diff --name-only --staged', 'diff --name-only --staged',
"commit -am 'lala kaka'", "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') @patch('subprocess.run')
def test_shell(mock_run, _, input): def test_shell(mock_run, _, input):
mock_run.reset_mock() mock_run.reset_mock()
args = ['shell', 'repo7'] + shlex.split(input) args = ['shell', 'repo7', input]
__main__.main(args) __main__.main(args)
expected_cmds = shlex.split(input) expected_cmds = input
mock_run.assert_called_once_with(expected_cmds, cwd='path7', check=True, stderr=-2, stdout=-1) mock_run.assert_called_once_with(expected_cmds, cwd='path7', check=True, shell=True, stderr=-2, stdout=-1)
class TestContext: class TestContext:
@ -184,21 +301,21 @@ class TestContext:
@patch('gita.utils.get_context', return_value=Path('gname.context')) @patch('gita.utils.get_context', return_value=Path('gname.context'))
@patch('gita.utils.get_groups', return_value={'gname': ['a', 'b']}) @patch('gita.utils.get_groups', return_value={'gname': ['a', 'b']})
def testDisplayContext(self, _, __, capfd): def test_display_context(self, _, __, capfd):
__main__.main(['context']) __main__.main(['context'])
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert err == '' assert err == ''
assert 'gname: a b\n' == out assert 'gname: a b\n' == out
@patch('gita.utils.get_context') @patch('gita.utils.get_context')
def testReset(self, mock_ctx): def test_reset(self, mock_ctx):
__main__.main(['context', 'none']) __main__.main(['context', 'none'])
mock_ctx.return_value.unlink.assert_called() mock_ctx.return_value.unlink.assert_called()
@patch('gita.utils.get_context', return_value=None) @patch('gita.utils.get_context', return_value=None)
@patch('gita.common.get_config_dir', return_value=TEST_DIR) @patch('gita.common.get_config_dir', return_value=TEST_DIR)
@patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []}) @patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []})
def testSetFirstTime(self, *_): def test_set_first_time(self, *_):
ctx = TEST_DIR / 'lala.context' ctx = TEST_DIR / 'lala.context'
assert not ctx.is_file() assert not ctx.is_file()
__main__.main(['context', 'lala']) __main__.main(['context', 'lala'])
@ -208,7 +325,7 @@ class TestContext:
@patch('gita.common.get_config_dir', return_value=TEST_DIR) @patch('gita.common.get_config_dir', return_value=TEST_DIR)
@patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []}) @patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []})
@patch('gita.utils.get_context') @patch('gita.utils.get_context')
def testSetSecondTime(self, mock_ctx, *_): def test_set_second_time(self, mock_ctx, *_):
__main__.main(['context', 'kaka']) __main__.main(['context', 'kaka'])
mock_ctx.return_value.rename.assert_called() mock_ctx.return_value.rename.assert_called()
@ -216,7 +333,7 @@ class TestContext:
class TestGroupCmd: class TestGroupCmd:
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
def testLs(self, _, capfd): def test_ls(self, _, capfd):
args = argparse.Namespace() args = argparse.Namespace()
args.to_group = None args.to_group = None
args.group_cmd = 'ls' args.group_cmd = 'ls'
@ -227,7 +344,7 @@ class TestGroupCmd:
assert 'xx yy\n' == out assert 'xx yy\n' == out
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
def testLl(self, _, capfd): def test_ll(self, _, capfd):
args = argparse.Namespace() args = argparse.Namespace()
args.to_group = None args.to_group = None
args.group_cmd = None args.group_cmd = None
@ -239,7 +356,7 @@ class TestGroupCmd:
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file') @patch('gita.utils.write_to_groups_file')
def testRename(self, mock_write, _): def test_rename(self, mock_write, _):
args = argparse.Namespace() args = argparse.Namespace()
args.gname = 'xx' args.gname = 'xx'
args.new_name = 'zz' args.new_name = 'zz'
@ -250,7 +367,7 @@ class TestGroupCmd:
mock_write.assert_called_once_with(expected, 'w') mock_write.assert_called_once_with(expected, 'w')
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
def testRenameError(self, *_): def test_rename_error(self, *_):
args = argparse.Namespace() args = argparse.Namespace()
args.gname = 'xx' args.gname = 'xx'
args.new_name = 'yy' args.new_name = 'yy'
@ -266,7 +383,7 @@ class TestGroupCmd:
@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''}) @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file') @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() utils.get_groups.cache_clear()
args = ['group', 'rm'] + shlex.split(input) args = ['group', 'rm'] + shlex.split(input)
__main__.main(args) __main__.main(args)
@ -275,7 +392,7 @@ class TestGroupCmd:
@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''}) @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file') @patch('gita.utils.write_to_groups_file')
def testAdd(self, mock_write, *_): def test_add(self, mock_write, *_):
args = argparse.Namespace() args = argparse.Namespace()
args.to_group = ['a', 'c'] args.to_group = ['a', 'c']
args.group_cmd = 'add' args.group_cmd = 'add'
@ -287,7 +404,7 @@ class TestGroupCmd:
@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''}) @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file') @patch('gita.utils.write_to_groups_file')
def testAddToExisting(self, mock_write, *_): def test_add_to_existing(self, mock_write, *_):
args = argparse.Namespace() args = argparse.Namespace()
args.to_group = ['a', 'c'] args.to_group = ['a', 'c']
args.group_cmd = 'add' args.group_cmd = 'add'
@ -300,7 +417,7 @@ class TestGroupCmd:
@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''}) @patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME) @patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file') @patch('gita.utils.write_to_groups_file')
def testRmRepo(self, mock_write, *_): def test_rm_repo(self, mock_write, *_):
args = argparse.Namespace() args = argparse.Namespace()
args.from_group = ['a', 'c'] args.from_group = ['a', 'c']
args.group_cmd = 'rmrepo' args.group_cmd = 'rmrepo'
@ -310,6 +427,21 @@ class TestGroupCmd:
mock_write.assert_called_once_with( mock_write.assert_called_once_with(
{'xx': ['b'], 'yy': ['a', 'c', 'd']}, 'w') {'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.utils.is_git', return_value=True)
@patch('gita.common.get_config_fname', return_value=PATH_FNAME) @patch('gita.common.get_config_fname', return_value=PATH_FNAME)
@ -319,44 +451,61 @@ def test_rename(mock_rename, _, __):
args = ['rename', 'repo1', 'abc'] args = ['rename', 'repo1', 'abc']
__main__.main(args) __main__.main(args)
mock_rename.assert_called_once_with( mock_rename.assert_called_once_with(
{'repo1': '/a/bcd/repo1', 'repo2': '/e/fgh/repo2', {'repo1': {'path': '/a/bcd/repo1', 'type': '', 'flags': []},
'xxx': '/a/b/c/repo3'}, 'xxx': {'path': '/a/b/c/repo3', 'type': '', 'flags': []},
'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': []}},
'repo1', 'abc') 'repo1', 'abc')
class TestInfo: class TestInfo:
@patch('gita.common.get_config_fname', return_value='') @patch('gita.common.get_config_fname', return_value='')
def testLl(self, _, capfd): def test_ll(self, _, capfd):
args = argparse.Namespace() args = argparse.Namespace()
args.info_cmd = None args.info_cmd = None
__main__.f_info(args) __main__.f_info(args)
out, err = capfd.readouterr() 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 == '' assert err == ''
@patch('gita.common.get_config_fname', return_value='') @patch('gita.common.get_config_fname')
@patch('yaml.dump') def test_add(self, mock_get_fname, tmpdir):
def testAdd(self, mock_dump, _):
args = argparse.Namespace() args = argparse.Namespace()
args.info_cmd = 'add' args.info_cmd = 'add'
args.info_item = 'path' 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) __main__.f_info(args)
mock_dump.assert_called_once() items = info.get_info_items()
args, kwargs = mock_dump.call_args assert items == ['branch', 'commit_msg', 'commit_time', 'path']
assert args[0] == ['branch', 'commit_msg', 'path']
assert kwargs == {'default_flow_style': None}
@patch('gita.common.get_config_fname', return_value='') @patch('gita.common.get_config_fname')
@patch('yaml.dump') def test_rm(self, mock_get_fname, tmpdir):
def testRm(self, mock_dump, _):
args = argparse.Namespace() args = argparse.Namespace()
args.info_cmd = 'rm' args.info_cmd = 'rm'
args.info_item = 'commit_msg' 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) __main__.f_info(args)
mock_dump.assert_called_once() items = info.get_info_items()
args, kwargs = mock_dump.call_args assert items == ['branch', 'commit_time']
assert args[0] == ['branch']
assert kwargs == {'default_flow_style': None}
@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'}

View file

@ -1,5 +1,7 @@
import pytest import pytest
import asyncio import asyncio
import subprocess
from pathlib import Path
from unittest.mock import patch, mock_open from unittest.mock import patch, mock_open
from gita import utils, info 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', [ @pytest.mark.parametrize('test_input, diff_return, expected', [
([{'abc': '/root/repo/'}, False], True, 'abc \x1b[31mrepo *+_ \x1b[0m msg'), ([{'abc': {'path': '/root/repo/', 'type': '', 'flags': []}}, False],
([{'abc': '/root/repo/'}, True], True, 'abc repo *+_ msg'), True, 'abc \x1b[31mrepo *+_ \x1b[0m msg xx'),
([{'repo': '/root/repo2/'}, False], False, 'repo \x1b[32mrepo _ \x1b[0m msg'), ([{'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): def test_describe(test_input, diff_return, expected, monkeypatch):
monkeypatch.setattr(info, 'get_head', lambda x: 'repo') monkeypatch.setattr(info, 'get_head', lambda x: 'repo')
monkeypatch.setattr(info, 'run_quiet_diff', lambda _: diff_return) monkeypatch.setattr(info, 'run_quiet_diff', lambda *_: diff_return)
monkeypatch.setattr(info, 'get_commit_msg', lambda _: "msg") monkeypatch.setattr(info, 'get_commit_msg', lambda *_: "msg")
monkeypatch.setattr(info, 'has_untracked', lambda: True) monkeypatch.setattr(info, 'get_commit_time', lambda *_: "xx")
monkeypatch.setattr(info, 'has_untracked', lambda *_: True)
monkeypatch.setattr('os.chdir', lambda x: None) monkeypatch.setattr('os.chdir', lambda x: None)
print('expected: ', repr(expected)) print('expected: ', repr(expected))
print('got: ', repr(next(utils.describe(*test_input)))) 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', [ @pytest.mark.parametrize('path_fname, expected', [
(PATH_FNAME, { (PATH_FNAME, {
'repo1': '/a/bcd/repo1', 'repo1': {'path': '/a/bcd/repo1', 'type': '', 'flags': []},
'repo2': '/e/fgh/repo2', 'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': []},
'xxx': '/a/b/c/repo3', 'xxx': {'path': '/a/b/c/repo3', 'type': '', 'flags': []},
}), }),
(PATH_FNAME_EMPTY, {}), (PATH_FNAME_EMPTY, {}),
(PATH_FNAME_CLASH, { (PATH_FNAME_CLASH, {
'repo1': '/a/bcd/repo1', 'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': ['--haha', '--pp']},
'repo2': '/e/fgh/repo2', 'repo1': {'path': '/root/x/repo1', 'type': '', 'flags': []}
'x/repo1': '/root/x/repo1'
}), }),
]) ])
@patch('gita.utils.is_git', return_value=True) @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) @patch('os.path.getsize', return_value=True)
def test_custom_push_cmd(*_): def test_custom_push_cmd(*_):
with patch('builtins.open', 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() 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( @pytest.mark.parametrize(
'path_input, expected', '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'], (['/home/some/repo1', '/repo2'],
{'/repo2,repo2\n/home/some/repo1,repo1\n', # add two new {'/repo2,repo2,,\r\n', # add two new
'/home/some/repo1,repo1\n/repo2,repo2\n'}), # add two new '/home/some/repo1,repo1,,\r\n'}), # add two new
(['/home/some/repo1', '/nos/repo'], (['/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('os.makedirs')
@patch('gita.utils.is_git', return_value=True) @patch('gita.utils.is_git', return_value=True)
def test_add_repos(_0, _1, path_input, expected, monkeypatch): def test_add_repos(_0, _1, path_input, expected, monkeypatch):
monkeypatch.setenv('XDG_CONFIG_HOME', '/config') monkeypatch.setenv('XDG_CONFIG_HOME', '/config')
with patch('builtins.open', mock_open()) as mock_file: with patch('builtins.open', mock_open()) as mock_file:
utils.add_repos({'repo': '/nos/repo'}, path_input) utils.add_repos({'repo': {'path': '/nos/repo'}}, path_input)
mock_file.assert_called_with('/config/gita/repo_path', 'a+') mock_file.assert_called_with('/config/gita/repos.csv', 'a+', newline='')
handle = mock_file() handle = mock_file()
if type(expected) == str: if type(expected) == str:
handle.write.assert_called_once_with(expected) handle.write.assert_called_once_with(expected)
else: else:
handle.write.assert_called_once() # the write order is random
assert handle.write.call_count == 2
args, kwargs = handle.write.call_args args, kwargs = handle.write.call_args
assert args[0] in expected assert args[0] in expected
assert not kwargs assert not kwargs
@patch('gita.utils.write_to_groups_file')
@patch('gita.utils.write_to_repo_file') @patch('gita.utils.write_to_repo_file')
def test_rename_repo(mock_write): def test_rename_repo(mock_write, _):
utils.rename_repo({'r1': '/a/b', 'r2': '/c/c'}, 'r2', 'xxx') repos = {'r1': {'path': '/a/b', 'type': None},
mock_write.assert_called_once_with({'r1': '/a/b', 'xxx': '/c/c'}, 'w') '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): def test_async_output(capfd):
@ -124,3 +154,10 @@ def test_async_output(capfd):
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert err == '' 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' 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

766
work.vim Normal file
View file

@ -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 :