1
0
Fork 0

Merging upstream version 0.15.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-11 18:42:23 +01:00
parent 86d5d7fe9f
commit 9cbf6c15e9
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]}
cmd=${COMP_WORDS[1]}
# FIXME: this is somewhat slow
commands=`gita -h | sed '2q;d' |sed 's/[{}.,]/ /g'`
repos=`gita ls`
# this doesn't work for two repos with the same basename
#gita_path=${XDG_CONFIG_HOME:-$HOME/.config}/gita/repo_path
#repos=`awk '{split($0, paths, ":")} END {for (i in paths) {n=split(paths[i],b, /\//); print b[n]}}' ${gita_path}`
if [ $COMP_CWORD -eq 1 ]; then
# FIXME: this is somewhat slow
commands=`gita -h | sed '2q;d' |sed 's/[{}.,]/ /g'`
COMPREPLY=($(compgen -W "${commands}" ${cur}))
elif [ $COMP_CWORD -gt 1 ]; then
case $cmd in
add)
COMPREPLY=($(compgen -d ${cur}))
;;
ll)
clone)
COMPREPLY=($(compgen -f ${cur}))
;;
color | flags)
COMPREPLY=($(compgen -W "ll set" ${cur}))
;;
ll | context)
groups=`gita group ls`
COMPREPLY=($(compgen -W "${groups}" ${cur}))
return
;;
*)
repos=`gita ls`
COMPREPLY=($(compgen -W "${repos}" ${cur}))
;;
esac
fi
}
complete -F _gita_completions gita

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
@ -29,11 +29,13 @@ I also hate to change directories to execute git commands.
![gita screenshot](https://github.com/nosarthur/gita/raw/master/doc/screenshot.png)
In the screenshot, the `gita remote nowhub` command translates to `git remote -v`
for the `nowhub` repo, even though we are at the `blog` repo.
To see the pre-defined sub-commands, run `gita -h` or take a look at
[cmds.yml](https://github.com/nosarthur/gita/blob/master/gita/cmds.yml).
To add your own sub-commands, see the [customization section](#custom).
In this screenshot, the `gita ll` command displays the status of all repos.
The `gita remote dotfiles` command translates to `git remote -v`
for the `dotfiles` repo, even though we are not in the repo.
The `gita fetch` command fetches from all repos and two of them have updates.
To see the pre-defined commands, run `gita -h` or take a look at
[cmds.json](https://github.com/nosarthur/gita/blob/master/gita/cmds.json).
To add your own commands, see the [customization section](#custom).
To run arbitrary `git` command, see the [superman mode section](#superman).
To run arbitrary shell command, see the [shell mode section](#shell).
@ -48,7 +50,7 @@ The branch color distinguishes 5 situations between local and remote branches:
The choice of purple for ahead and yellow for behind is motivated by
[blueshift](https://en.wikipedia.org/wiki/Blueshift) and [redshift](https://en.wikipedia.org/wiki/Redshift),
using green as baseline.
You can change the color scheme using the `gita color` sub-command.
You can change the color scheme using the `gita color` command.
See the [customization section](#custom).
The additional status symbols denote
@ -60,7 +62,13 @@ The additional status symbols denote
The bookkeeping sub-commands are
- `gita add <repo-path(s)>`: add repo(s) to `gita`
- `gita add -a <repo-parent-path(s)>`: add repo(s) in <repo-parent-path(s)> recursively
and automatically generate hierarchical groups. See the [customization section](#custom) for more details.
- `gita add -b <bare-repo-path(s)>`: add bare repo(s) to `gita`. See the [customization section](#custom) for more details on setting custom worktree.
- `gita add -m <main-repo-path(s)>`: add main repo(s) to `gita`. See the [customization section](#custom) for more details.
- `gita add -r <repo-parent-path(s)>`: add repo(s) in <repo-parent-path(s)> recursively
- `gita clone <config-file>`: clone repos in `config-file` (generated by `gita freeze`) to current directory.
- `gita clone -p <config-file>`: clone repos in `config-file` to prescribed paths.
- `gita context`: context sub-command
- `gita context`: show current context
- `gita context none`: remove context
@ -68,9 +76,12 @@ The bookkeeping sub-commands are
- `gita color`: color sub-command
- `gita color [ll]`: Show available colors and the current coloring scheme
- `gita color set <situation> <color>`: Use the specified color for the local-remote situation
- `gita flags`: flags sub-command
- `gita flags set <repo-name> <flags>`: add custom `flags` to repo
- `gita flags [ll]`: display repos with custom flags
- `gita freeze`: print information of all repos such as URL, name, and path.
- `gita group`: group sub-command
- `gita group add <repo-name(s)> -n <group-name>`: add repo(s) to a new group or existing group
- `gita group add <repo-name(s)> -n <group-name>`: add repo(s) to a new or existing group
- `gita group [ll]`: display existing groups with repos
- `gita group ls`: display existing group names
- `gita group rename <group-name> <new-name>`: change group name
@ -85,7 +96,7 @@ The bookkeeping sub-commands are
- `gita ls`: display the names of all repos
- `gita ls <repo-name>`: display the absolute path of one repo
- `gita rename <repo-name> <new-name>`: rename a repo
- `gita rm <repo-name(s)>`: remove repo(s) from `gita` (won't remove files from disk)
- `gita rm <repo-name(s)>`: remove repo(s) from `gita` (won't remove files on disk)
- `gita -v`: display gita version
The `git` delegating sub-commands are of two formats
@ -99,7 +110,7 @@ They translate to `git <sub-command>` for the corresponding repos.
By default, only `fetch` and `pull` take optional input. In other words,
`gita fetch` and `gita pull` apply to all repos.
To see the pre-defined sub-commands, run `gita -h` or take a look at
[cmds.yml](https://github.com/nosarthur/gita/blob/master/gita/cmds.yml).
[cmds.json](https://github.com/nosarthur/gita/blob/master/gita/cmds.json).
To add your own sub-commands or override the default behaviors, see the [customization section](#custom).
To run arbitrary `git` command, see the [superman mode section](#superman).
@ -107,7 +118,8 @@ If more than one repos are specified, the `git` command runs asynchronously,
with the exception of `log`, `difftool` and `mergetool`,
which require non-trivial user input.
Repo paths are saved in `$XDG_CONFIG_HOME/gita/repo_path` (most likely `~/.config/gita/repo_path`).
Repo configuration is saved in `$XDG_CONFIG_HOME/gita/repos.csv`
(most likely `~/.config/gita/repos.csv`).
## Installation
@ -124,7 +136,7 @@ pip3 install -e <gita-source-folder>
```
In either case, calling `gita` in terminal may not work,
then you can put the following line in the `.bashrc` file.
then put the following line in the `.bashrc` file.
```
alias gita="python3 -m gita"
@ -140,7 +152,7 @@ Download
[.gita-completion.bash](https://github.com/nosarthur/gita/blob/master/.gita-completion.bash)
or
[.gita-completion.zsh](https://github.com/nosarthur/gita/blob/master/.gita-completion.zsh)
and source it in the corresponding rc file.
and source it in shell.
## <a name='superman'></a> Superman mode
@ -171,65 +183,178 @@ Here `repo-name(s)` or `group-name(s)` are optional, and their absence means all
For example,
- `gita shell ll` lists contents for all repos
- `gita shell repo1 mkdir docs` create a new directory `docs` in repo1
- `gita shell repo1 repo2 mkdir docs` create a new directory `docs` in `repo1` and `repo2`
- `gita shell "git describe --abbrev=0 --tags | xargs git checkout"`: check out the latest tag for all repos
## <a name='custom'></a> Customization
### user-defined sub-command using yaml file
### define repo group and context
Custom delegating sub-commands can be defined in `$XDG_CONFIG_HOME/gita/cmds.yml`
(most likely `~/.config/gita/cmds.yml`).
When the project contains several independent but related repos,
we can define a group and execute `gita` command on this group.
For example,
```
gita group add repo1 repo2 -n my-group
gita ll my-group
gita pull my-group
```
To save more typing, one can set a group as context, then any `gita` command
is scoped to the group
```
gita context my-group
gita ll
gita pull
```
It is also possible to recursively add repos within a directory and
generate hierarchical groups automatically. For example, running
```
gita add -a src
```
on the following folder structure
```
src
├── project1
│   ├── repo1
│   └── repo2
├── repo3
├── project2
│   ├── repo4
│   └── repo5
└── repo6
```
gives rise to
```
src:repo1,repo2,repo3,repo4,repo5,repo6
src-project1:repo1,repo2
src-project2:repo4,repo5
```
### define main repos and shadow the global configuration setting with local setting
The so-called main repos contain `.gita` folder for local configurations.
It works best for the repos-within-repo project structure, for example,
```
main-repo
├── sub-repo1
│   └── sub-sub-repo
├── sub-repo2
└── sub-repo3
```
When executing `gita` commands within/relative to a main repo, local configurations
are used. And only repos within the current main repos are in the scope.
To add a main repo, run
```
gita add -m main-repo-path
```
Subordinate repos are added recursively to the local configuration.
Only the main repo is saved to the global configuration.
In the `gita ll` display, the main repos are underlined.
### add user-defined sub-command using json file
Custom delegating sub-commands can be defined in `$XDG_CONFIG_HOME/gita/cmds.json`
(most likely `~/.config/gita/cmds.json`)
And they shadow the default ones if name collisions exist.
Default delegating sub-commands are defined in
[cmds.yml](https://github.com/nosarthur/gita/blob/master/gita/cmds.yml).
[cmds.json](https://github.com/nosarthur/gita/blob/master/gita/cmds.json).
For example, `gita stat <repo-name(s)>` is registered as
```yaml
stat:
cmd: diff --stat
help: show edit statistics
```json
"stat":{
"cmd": "git diff --stat",
"help": "show edit statistics"
}
```
which executes `git diff --stat` for the specified repo(s).
If the delegated `git` command is a single word, the `cmd` tag can be omitted.
See `push` for an example.
To disable asynchronous execution, set the `disable_async` tag to be `true`.
See `difftool` for an example.
To disable asynchronous execution, set `disable_async` to be `true`.
See the `difftool` example:
If you want a custom command to behave like `gita fetch`, i.e., to apply the
command to all repos when no repo is specified,
set the `allow_all` option to be `true`.
```json
"difftool":{
"cmd": "git difftool",
"disable_async": true,
"help": "show differences using a tool"
}
```
If you want a custom command to behave like `gita fetch`, i.e., to apply to all
repos when no repo is specified, set `allow_all` to be `true`.
For example, the following snippet creates a new command
`gita comaster [repo-name(s)]` with optional repo name input.
```yaml
comaster:
cmd: checkout master
allow_all: true
help: checkout the master branch
```json
"comaster":{
"cmd": "checkout master",
"allow_all": true,
"help": "checkout the master branch"
}
```
Any command that runs in the [superman mode](#superman) mode or the
[shell mode](#shell) can be defined in this json format.
For example, the following command runs in shell mode and fetches only the
current branch from upstream.
```json
"fetchcrt":{
"cmd": "git rev-parse --abbrev-ref HEAD | xargs git fetch --prune upstream",
"allow_all": true,
"shell": true,
"help": "fetch current branch only"
}
```
### customize the local/remote relationship coloring displayed by the `gita ll` command
You can see the default color scheme and the available colors via `gita color`.
To change the color coding, use `gita color set <situation> <color>`.
The configuration is saved in `$XDG_CONFIG_HOME/gita/color.yml`.
The configuration is saved in `$XDG_CONFIG_HOME/gita/color.csv`.
### customize information displayed by the `gita ll` command
You can customize the information displayed by `gita ll`.
The used and unused information items are shown with `gita info`, and the
configuration is saved in `$XDG_CONFIG_HOME/gita/info.yml`.
configuration is saved in `$XDG_CONFIG_HOME/gita/info.csv`.
For example, the default information items setting corresponds to
For example, the default setting corresponds to
```yaml
- branch
- commit_msg
```csv
branch,commit_msg,commit_time
```
### customize git command flags
One can set custom flags to run `git` commands. For example
```
gita flags set my-repo --git-dir=$HOME/somefolder --work-tree=$HOME
```
Then any `git` command/alias triggered from `gita` on `my-repo` will use these flags.
Note that the flags are applied immediately after `git`. For example,
`gita st my-repo` translates to
```
git --git-dir=$HOME/somefolder --work-tree=$HOME status
```
running from the `my-repo` directory.
## Requirements
Gita requires Python 3.6 or higher, due to the use of
@ -249,9 +374,12 @@ To contribute, you can
- request/implement features
- star/recommend this project
Read [this article](https://www.dataschool.io/how-to-contribute-on-github/) if you have never contribute code to open source project before.
Chat room is available on [![Join the chat at https://gitter.im/nosarthur/gita](https://badges.gitter.im/nosarthur/gita.svg)](https://gitter.im/nosarthur/gita?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
To run tests locally, simply `pytest`.
To run tests locally, simply `pytest` in the source code folder.
Note that context should be set as `none`.
More implementation details are in
[design.md](https://github.com/nosarthur/gita/blob/master/doc/design.md).
A step-by-step guide to reproduce this project is [here](https://nosarthur.github.io/side%20project/2019/05/27/gita-breakdown.html).

View file

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

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 sys
import yaml
import csv
import argparse
import subprocess
import pkg_resources
from itertools import chain
from pathlib import Path
import glob
from . import utils, info, common
def _group_name(name: str) -> str:
"""
"""
repos = utils.get_repos()
if name in repos:
print(f"Cannot use group name {name} since it's a repo name.")
sys.exit(1)
return name
def f_add(args: argparse.Namespace):
repos = utils.get_repos()
paths = args.paths
if args.recursive:
paths = chain.from_iterable(Path(p).glob('**') for p in args.paths)
utils.add_repos(repos, paths)
if args.main:
# add to global and tag as main
main_repos = utils.add_repos(repos, paths, repo_type='m')
# add sub-repo recursively and save to local config
for name, prop in main_repos.items():
main_path = prop['path']
print('Inside main repo:', name)
#sub_paths = Path(main_path).glob('**')
sub_paths = glob.glob(os.path.join(main_path,'**/'), recursive=True)
utils.add_repos({}, sub_paths, root=main_path)
else:
if args.recursive or args.auto_group:
paths = chain.from_iterable(
glob.glob(os.path.join(p, '**/'), recursive=True)
for p in args.paths)
new_repos = utils.add_repos(repos, paths, is_bare=args.bare)
if args.auto_group:
new_groups = utils.auto_group(new_repos, args.paths)
if new_groups:
print(f'Created {len(new_groups)} new group(s).')
utils.write_to_groups_file(new_groups, 'a+')
def f_rename(args: argparse.Namespace):
@ -39,16 +69,32 @@ def f_rename(args: argparse.Namespace):
utils.rename_repo(repos, args.repo[0], args.new_name)
def f_flags(args: argparse.Namespace):
cmd = args.flags_cmd or 'll'
repos = utils.get_repos()
if cmd == 'll':
for r, prop in repos.items():
if prop['flags']:
print(f"{r}: {prop['flags']}")
elif cmd == 'set':
# when in memory, flags are List[str], when on disk, they are space
# delimited str
repos[args.repo]['flags'] = args.flags
utils.write_to_repo_file(repos, 'w')
def f_color(args: argparse.Namespace):
cmd = args.color_cmd or 'll'
if cmd == 'll': # pragma: no cover
info.show_colors()
elif cmd == 'set':
colors = info.get_color_encoding()
colors[args.situation] = info.Color[args.color].value
yml_config = common.get_config_fname('color.yml')
with open(yml_config, 'w') as f:
yaml.dump(colors, f, default_flow_style=None)
colors[args.situation] = args.color
csv_config = common.get_config_fname('color.csv')
with open(csv_config, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=colors)
writer.writeheader()
writer.writerow(colors)
def f_info(args: argparse.Namespace):
@ -56,36 +102,52 @@ def f_info(args: argparse.Namespace):
cmd = args.info_cmd or 'll'
if cmd == 'll':
print('In use:', ','.join(to_display))
unused = set(info.ALL_INFO_ITEMS) - set(to_display)
unused = sorted(list(set(info.ALL_INFO_ITEMS) - set(to_display)))
if unused:
print('Unused:', ' '.join(unused))
print('Unused:', ','.join(unused))
return
if cmd == 'add' and args.info_item not in to_display:
to_display.append(args.info_item)
yml_config = common.get_config_fname('info.yml')
with open(yml_config, 'w') as f:
yaml.dump(to_display, f, default_flow_style=None)
csv_config = common.get_config_fname('info.csv')
with open(csv_config, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(to_display)
elif cmd == 'rm' and args.info_item in to_display:
to_display.remove(args.info_item)
yml_config = common.get_config_fname('info.yml')
with open(yml_config, 'w') as f:
yaml.dump(to_display, f, default_flow_style=None)
csv_config = common.get_config_fname('info.csv')
with open(csv_config, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(to_display)
def f_clone(args: argparse.Namespace):
path = Path.cwd()
errors = utils.exec_async_tasks(
if args.preserve_path:
utils.exec_async_tasks(
utils.run_async(repo_name, path, ['git', 'clone', url, abs_path])
for url, repo_name, abs_path in utils.parse_clone_config(args.fname))
else:
utils.exec_async_tasks(
utils.run_async(repo_name, path, ['git', 'clone', url])
for url, repo_name, _ in utils.parse_clone_config(args.fname))
def f_freeze(_):
repos = utils.get_repos()
for name, path in repos.items():
seen = {''}
for name, prop in repos.items():
path = prop['path']
# TODO: What do we do with main repos? Maybe give an option to print
# their sub-repos too.
url = ''
cp = subprocess.run(['git', 'remote', '-v'], cwd=path, capture_output=True)
if cp.returncode == 0:
url = cp.stdout.decode('utf-8').split('\n')[0].split()[1]
lines = cp.stdout.decode('utf-8').split('\n')
if cp.returncode == 0 and len(lines) > 0:
parts = lines[0].split()
if len(parts)>1:
url = parts[1]
if url not in seen:
seen.add(url)
print(f'{url},{name},{path}')
@ -107,7 +169,7 @@ def f_ll(args: argparse.Namespace):
def f_ls(args: argparse.Namespace):
repos = utils.get_repos()
if args.repo: # one repo, show its path
print(repos[args.repo])
print(repos[args.repo]['path'])
else: # show names of all repos
print(' '.join(repos))
@ -128,6 +190,11 @@ def f_group(args: argparse.Namespace):
groups[new_name] = groups[gname]
del groups[gname]
utils.write_to_groups_file(groups, 'w')
# change context
ctx = utils.get_context()
if ctx and str(ctx.stem) == gname:
# ctx.rename(ctx.with_stem(new_name)) # only works in py3.9
ctx.rename(ctx.with_name(f'{new_name}.context'))
elif cmd == 'rm':
ctx = utils.get_context()
for name in args.to_ungroup:
@ -178,11 +245,21 @@ def f_rm(args: argparse.Namespace):
"""
Unregister repo(s) from gita
"""
path_file = common.get_config_fname('repo_path')
path_file = common.get_config_fname('repos.csv')
if os.path.isfile(path_file):
repos = utils.get_repos()
main_paths = [prop['path'] for prop in repos.values() if prop['type'] == 'm']
# TODO: add test case to delete main repo from main repo
# only local setting should be affected instead of the global one
for repo in args.repo:
del repos[repo]
# 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')
@ -205,21 +282,33 @@ def f_git_cmd(args: argparse.Namespace):
for r in groups[k]:
chosen[r] = repos[r]
repos = chosen
cmds = ['git'] + args.cmd
if len(repos) == 1 or cmds[1] in args.async_blacklist:
for path in repos.values():
per_repo_cmds = []
for prop in repos.values():
cmds = args.cmd.copy()
if cmds[0] == 'git' and prop['flags']:
cmds[1:1] = prop['flags']
per_repo_cmds.append(cmds)
# This async blacklist mechanism is broken if the git command name does
# not match with the gita command name.
if len(repos) == 1 or args.cmd[1] in args.async_blacklist:
for prop, cmds in zip(repos.values(), per_repo_cmds):
path = prop['path']
print(path)
subprocess.run(cmds, cwd=path)
subprocess.run(cmds, cwd=path, shell=args.shell)
else: # run concurrent subprocesses
# Async execution cannot deal with multiple repos' user name/password.
# Here we shut off any user input in the async execution, and re-run
# the failed ones synchronously.
errors = utils.exec_async_tasks(
utils.run_async(repo_name, path, cmds) for repo_name, path in repos.items())
utils.run_async(repo_name, prop['path'], cmds)
for cmds, (repo_name, prop) in zip(per_repo_cmds, repos.items()))
for path in errors:
if path:
print(path)
subprocess.run(cmds, cwd=path)
# FIXME: This is broken, flags are missing. But probably few
# people will use `gita flags`
subprocess.run(args.cmd, cwd=path)
def f_shell(args):
@ -249,10 +338,10 @@ def f_shell(args):
for r in groups[k]:
chosen[r] = repos[r]
repos = chosen
cmds = args.man[i:]
for name, path in repos.items():
cmds = ' '.join(args.man[i:]) # join the shell command into a single string
for name, prop in repos.items():
# TODO: pull this out as a function
got = subprocess.run(cmds, cwd=path, check=True,
got = subprocess.run(cmds, cwd=prop['path'], check=True, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
print(utils.format_output(got.stdout.decode(), name))
@ -271,8 +360,9 @@ def f_super(args):
names.append(word)
else:
break
args.cmd = args.man[i:]
args.cmd = ['git'] + args.man[i:]
args.repo = names
args.shell = False
f_git_cmd(args)
@ -292,9 +382,17 @@ def main(argv=None):
# bookkeeping sub-commands
p_add = subparsers.add_parser('add', description='add repo(s)',
help='add repo(s)')
p_add.add_argument('paths', nargs='+', help="repo(s) to add")
p_add.add_argument('-r', dest='recursive', action='store_true',
help="recursively add repo(s) in the given path.")
p_add.add_argument('paths', nargs='+', type=os.path.abspath, help="repo(s) to add")
xgroup = p_add.add_mutually_exclusive_group()
xgroup.add_argument('-r', '--recursive', action='store_true',
help="recursively add repo(s) in the given path(s).")
xgroup.add_argument('-m', '--main', action='store_true',
help="make main repo(s), sub-repos are recursively added.")
xgroup.add_argument('-a', '--auto-group', action='store_true',
help="recursively add repo(s) in the given path(s) "
"and create hierarchical groups based on folder structure.")
xgroup.add_argument('-b', '--bare', action='store_true',
help="add bare repo(s)")
p_add.set_defaults(func=f_add)
p_rm = subparsers.add_parser('rm', description='remove repo(s)',
@ -305,15 +403,22 @@ def main(argv=None):
help="remove the chosen repo(s)")
p_rm.set_defaults(func=f_rm)
p_freeze = subparsers.add_parser('freeze', description='print all repo information')
p_freeze = subparsers.add_parser('freeze',
description='print all repo information',
help='print all repo information')
p_freeze.set_defaults(func=f_freeze)
p_clone = subparsers.add_parser('clone', description='clone repos from config file')
p_clone = subparsers.add_parser('clone',
description='clone repos from config file',
help='clone repos from config file')
p_clone.add_argument('fname',
help='config file. Its content should be the output of `gita freeze`.')
p_clone.add_argument('-p', '--preserve-path', dest='preserve_path', action='store_true',
help="clone repo(s) in their original paths")
p_clone.set_defaults(func=f_clone)
p_rename = subparsers.add_parser('rename', description='rename a repo')
p_rename = subparsers.add_parser('rename', description='rename a repo',
help='rename a repo')
p_rename.add_argument(
'repo',
nargs=1,
@ -322,8 +427,25 @@ def main(argv=None):
p_rename.add_argument('new_name', help="new name")
p_rename.set_defaults(func=f_rename)
p_flags = subparsers.add_parser('flags',
description='Set custom git flags for repo.',
help='git flags configuration')
p_flags.set_defaults(func=f_flags)
flags_cmds = p_flags.add_subparsers(dest='flags_cmd',
help='additional help with sub-command -h')
flags_cmds.add_parser('ll',
description='display repos with custom flags')
pf_set = flags_cmds.add_parser('set',
description='Set flags for repo.')
pf_set.add_argument('repo', choices=utils.get_repos(),
help="repo name")
pf_set.add_argument('flags',
nargs=argparse.REMAINDER,
help="custom flags, use quotes")
p_color = subparsers.add_parser('color',
description='display and modify branch coloring of the ll sub-command.')
description='display and modify branch coloring of the ll sub-command.',
help='color configuration')
p_color.set_defaults(func=f_color)
color_cmds = p_color.add_subparsers(dest='color_cmd',
help='additional help with sub-command -h')
@ -339,7 +461,8 @@ def main(argv=None):
help="available colors")
p_info = subparsers.add_parser('info',
description='list, add, or remove information items of the ll sub-command.')
description='list, add, or remove information items of the ll sub-command.',
help='information setting')
p_info.set_defaults(func=f_info)
info_cmds = p_info.add_subparsers(dest='info_cmd',
help='additional help with sub-command -h')
@ -347,11 +470,11 @@ def main(argv=None):
description='show used and unused information items of the ll sub-command')
info_cmds.add_parser('add', description='Enable information item.'
).add_argument('info_item',
choices=('branch', 'commit_msg', 'path'),
choices=info.ALL_INFO_ITEMS,
help="information item to add")
info_cmds.add_parser('rm', description='Disable information item.'
).add_argument('info_item',
choices=('branch', 'commit_msg', 'path'),
choices=info.ALL_INFO_ITEMS,
help="information item to delete")
@ -379,6 +502,7 @@ def main(argv=None):
p_ll.set_defaults(func=f_ll)
p_context = subparsers.add_parser('context',
help='set context',
description='Set and remove context. A context is a group.'
' When set, all operations apply only to repos in that group.')
p_context.add_argument('choice',
@ -388,7 +512,8 @@ def main(argv=None):
p_context.set_defaults(func=f_context)
p_ls = subparsers.add_parser(
'ls', description='display names of all repos, or path of a chosen repo')
'ls', help='show repo(s) or repo path',
description='display names of all repos, or path of a chosen repo')
p_ls.add_argument('repo',
nargs='?',
choices=utils.get_repos(),
@ -396,7 +521,8 @@ def main(argv=None):
p_ls.set_defaults(func=f_ls)
p_group = subparsers.add_parser(
'group', description='list, add, or remove repo group(s)')
'group', description='list, add, or remove repo group(s)',
help='group repos')
p_group.set_defaults(func=f_group)
group_cmds = p_group.add_subparsers(dest='group_cmd',
help='additional help with sub-command -h')
@ -410,6 +536,7 @@ def main(argv=None):
help="repo(s) to be grouped")
pg_add.add_argument('-n', '--name',
dest='gname',
type=_group_name,
metavar='group-name',
required=True,
help="group name")
@ -429,6 +556,7 @@ def main(argv=None):
choices=utils.get_groups(),
help="existing group to rename")
pg_rename.add_argument('new_name', metavar='new-name',
type=_group_name,
help="new group name")
group_cmds.add_parser('rm',
description='Remove group(s).').add_argument('to_ungroup',
@ -439,6 +567,7 @@ def main(argv=None):
# superman mode
p_super = subparsers.add_parser(
'super',
help='run any git command/alias',
description='Superman mode: delegate any git command/alias in specified or '
'all repo(s).\n'
'Examples:\n \t gita super myrepo1 commit -am "fix a bug"\n'
@ -446,7 +575,7 @@ def main(argv=None):
p_super.add_argument(
'man',
nargs=argparse.REMAINDER,
help="execute arbitrary git command/alias for specified or all repos "
help="execute arbitrary git command/alias for specified or all repos\n"
"Example: gita super myrepo1 diff --name-only --staged "
"Another: gita super checkout master ")
p_super.set_defaults(func=f_super)
@ -454,6 +583,7 @@ def main(argv=None):
# shell mode
p_shell = subparsers.add_parser(
'shell',
help='run any shell command',
description='shell mode: delegate any shell command in specified or '
'all repo(s).\n'
'Examples:\n \t gita shell pwd\n'
@ -470,7 +600,7 @@ def main(argv=None):
cmds = utils.get_cmds_from_files()
for name, data in cmds.items():
help = data.get('help')
cmd = data.get('cmd') or name
cmd = data['cmd']
if data.get('allow_all'):
choices = utils.get_choices()
nargs = '*'
@ -481,7 +611,14 @@ def main(argv=None):
help += ' for the chosen repo(s) or group(s)'
sp = subparsers.add_parser(name, description=help)
sp.add_argument('repo', nargs=nargs, choices=choices, help=help)
sp.set_defaults(func=f_git_cmd, cmd=cmd.split())
is_shell = bool(data.get('shell'))
sp.add_argument('-s', '--shell', default=is_shell, type=bool,
help='If set, run in shell mode')
if is_shell:
cmd = [cmd]
else:
cmd = cmd.split()
sp.set_defaults(func=f_git_cmd, cmd=cmd)
args = p.parse_args(argv)

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
def get_config_dir() -> str:
parent = os.environ.get('XDG_CONFIG_HOME') or os.path.join(
def get_config_dir(root=None) -> str:
if root is None:
root = os.environ.get('XDG_CONFIG_HOME') or os.path.join(
os.path.expanduser('~'), '.config')
root = os.path.join(parent, "gita")
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.
"""
root = get_config_dir()
return os.path.join(root, fname)
return os.path.join(get_config_dir(root), fname)

View file

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

View file

@ -1,15 +1,53 @@
import os
import yaml
import json
import csv
import asyncio
import platform
import subprocess
from functools import lru_cache, partial
from pathlib import Path
from typing import List, Dict, Coroutine, Union, Iterator
from typing import List, Dict, Coroutine, Union, Iterator, Tuple
from collections import Counter, defaultdict
from . import info
from . import common
# 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()
def get_context() -> Union[Path, None]:
"""
@ -21,42 +59,18 @@ def get_context() -> Union[Path, 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()
def get_groups() -> Dict[str, List[str]]:
"""
Return a `dict` of group name to repo names.
"""
fname = common.get_config_fname('groups.yml')
fname = common.get_config_fname('groups.csv')
groups = {}
# Each line is a repo path and repo name separated by ,
if os.path.isfile(fname) and os.stat(fname).st_size > 0:
with open(fname, 'r') as f:
groups = yaml.load(f, Loader=yaml.FullLoader)
rows = csv.reader(f, delimiter=':')
groups = {r[0]: r[1].split() for r in rows}
return groups
@ -75,10 +89,12 @@ def get_choices() -> List[Union[str, None]]:
return choices
def is_git(path: str) -> bool:
def is_git(path: str, is_bare=False) -> bool:
"""
Return True if the path is a git repo.
"""
if not os.path.exists(path):
return False
# An alternative is to call `git rev-parse --is-inside-work-tree`
# I don't see why that one is better yet.
# For a regular git repo, .git is a folder, for a worktree repo, .git is a file.
@ -88,59 +104,172 @@ def is_git(path: str) -> bool:
# `git rev-parse --git-common-dir`
loc = os.path.join(path, '.git')
# TODO: we can display the worktree repos in a different font.
return os.path.exists(loc)
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, str], repo: str, new_name: str):
def rename_repo(repos: Dict[str, Dict[str, str]], repo: str, new_name: str):
"""
Write new repo name to file
"""
path = repos[repo]
if new_name in repos:
print(f"{new_name} is already in use!")
return
prop = repos[repo]
del repos[repo]
repos[new_name] = path
repos[new_name] = prop
# write to local config if inside a main path
main_paths = (prop['path'] for prop in repos.values() if prop['type'] == 'm')
cwd = os.getcwd()
is_local_config = True
for p in main_paths:
if is_relative_to(cwd, p):
write_to_repo_file(repos, 'w', p)
break
else: # global config
write_to_repo_file(repos, 'w')
is_local_config = False
# update groups only when outside any main repos
if is_local_config:
return
groups = get_groups()
for g, members in groups.items():
if repo in members:
members.remove(repo)
members.append(new_name)
groups[g] = sorted(members)
write_to_groups_file(groups, 'w')
def write_to_repo_file(repos: Dict[str, 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())
fname = common.get_config_fname('repo_path')
data = [(prop['path'], name, prop['type'], ' '.join(prop['flags']))
for name, prop in repos.items()]
fname = common.get_config_fname('repos.csv', root)
os.makedirs(os.path.dirname(fname), exist_ok=True)
with open(fname, mode) as f:
f.write(data)
with open(fname, mode, newline='') as f:
writer = csv.writer(f, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
writer.writerows(data)
def write_to_groups_file(groups: Dict[str, List[str]], mode: str):
"""
"""
fname = common.get_config_fname('groups.yml')
fname = common.get_config_fname('groups.csv')
os.makedirs(os.path.dirname(fname), exist_ok=True)
if not groups: # all groups are deleted
open(fname, 'w').close()
else:
with open(fname, mode) as f:
yaml.dump(groups, f, default_flow_style=None)
with open(fname, mode, newline='') as f:
data = [
(group, ' '.join(repos))
for group, repos in groups.items()
]
writer = csv.writer(f, delimiter=':', quotechar='"', quoting=csv.QUOTE_MINIMAL)
writer.writerows(data)
def 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
"""
existing_paths = set(repos.values())
new_paths = set(os.path.abspath(p) for p in new_paths if is_git(p))
existing_paths = {prop['path'] for prop in repos.values()}
new_paths = {p for p in new_paths if is_git(p, is_bare)}
new_paths = new_paths - existing_paths
new_repos = {}
if new_paths:
print(f"Found {len(new_paths)} new repo(s).")
new_repos = {
os.path.basename(os.path.normpath(path)): path
for path in new_paths}
write_to_repo_file(new_repos, 'a+')
name_counts = Counter(
os.path.basename(os.path.normpath(p)) for p in new_paths
)
new_repos = {_make_name(path, repos, name_counts): {
'path': path,
'type': _get_repo_type(path, repo_type, root),
'flags': '',
} for path in new_paths}
# When root is not None, we could optionally set its type to 'm', i.e.,
# main repo.
write_to_repo_file(new_repos, 'a+', root)
else:
print('No new repos found!')
return new_repos
def _generate_dir_hash(repo_path: str, paths: List[str]) -> Tuple[str, ...]:
"""
Return relative parent strings
For example, if `repo_path` is /a/b/c/d/here, and one of `paths` is /a/b/
then return (b, c, d)
"""
for p in paths:
if is_relative_to(repo_path, p):
break
else:
return ()
return (os.path.basename(p),
*os.path.normpath(os.path.relpath(repo_path, p)).split(os.sep)[:-1])
def auto_group(repos: Dict[str, Dict[str, str]], paths: List[str]
) -> Dict[str, List[str]]:
"""
"""
# FIXME: the upstream code should make sure that paths are all independent
# i.e., each repo should be contained in one and only one path
new_groups = defaultdict(list)
for repo_name, prop in repos.items():
hash = _generate_dir_hash(prop['path'], paths)
if not hash:
continue
for i in range(1, len(hash)+1):
group_name = '-'.join(hash[:i])
new_groups[group_name].append(repo_name)
# FIXME: need to make sure the new group names don't clash with old ones
# or repo names
return new_groups
def parse_clone_config(fname: str) -> Iterator[List[str]]:
@ -157,6 +286,7 @@ async def run_async(repo_name: str, path: str, cmds: List[str]) -> Union[None, s
Run `cmds` asynchronously in `path` directory. Return the `path` if
execution fails.
"""
# TODO: deprecated since 3.8, will be removed in 3.10
process = await asyncio.create_subprocess_exec(
*cmds,
stdin=asyncio.subprocess.DEVNULL,
@ -199,7 +329,7 @@ def exec_async_tasks(tasks: List[Coroutine]) -> List[Union[None, str]]:
return errors
def describe(repos: Dict[str, str], no_colors: bool=False) -> str:
def describe(repos: Dict[str, Dict[str, str]], no_colors: bool = False) -> str:
"""
Return the status of all repos
"""
@ -213,8 +343,13 @@ def describe(repos: Dict[str, str], no_colors: bool=False) -> str:
funcs[idx] = partial(get_repo_status, no_colors=True)
for name in sorted(repos):
path = repos[name]
info_items = ' '.join(f(path) for f in funcs)
info_items = ' '.join(f(repos[name]) for f in funcs)
if repos[name]['type'] == 'm':
# ANSI color code also takes length in Python
name = f'{info.Color.underline}{name}{info.Color.end}'
width = name_width + 8
yield f'{name:<{width}}{info_items}'
else:
yield f'{name:<{name_width}}{info_items}'
@ -231,17 +366,17 @@ def get_cmds_from_files() -> Dict[str, Dict[str, str]]:
}
"""
# default config file
fname = os.path.join(os.path.dirname(__file__), "cmds.yml")
with open(fname, 'r') as stream:
cmds = yaml.load(stream, Loader=yaml.FullLoader)
fname = os.path.join(os.path.dirname(__file__), "cmds.json")
with open(fname, 'r') as f:
cmds = json.load(f)
# custom config file
root = common.get_config_dir()
fname = os.path.join(root, 'cmds.yml')
fname = os.path.join(root, 'cmds.json')
custom_cmds = {}
if os.path.isfile(fname) and os.path.getsize(fname):
with open(fname, 'r') as stream:
custom_cmds = yaml.load(stream, Loader=yaml.FullLoader)
with open(fname, 'r') as f:
custom_cmds = json.load(f)
# custom commands shadow default ones
cmds.update(custom_cmds)

View file

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

View file

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

View file

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

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]
yy: [a, c, d]
xx:a b
yy:a c d

View file

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

View file

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

View file

@ -1,28 +1,102 @@
import os
import pytest
from unittest.mock import patch, mock_open
from unittest.mock import patch
from pathlib import Path
import argparse
import asyncio
import shlex
from gita import __main__
from gita import utils, info
from gita import utils, info, common
from conftest import (
PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME,
PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME, PATH_FNAME_MAIN,
async_mock, TEST_DIR,
)
@patch('gita.utils.get_repos', return_value={'aa'})
def test_group_name(_):
got = __main__._group_name('xx')
assert got == 'xx'
with pytest.raises(SystemExit):
__main__._group_name('aa')
class TestAdd:
@pytest.mark.parametrize('input, expected', [
(['add', '.'], ''),
(['add', '-m', '.'], 'm'),
])
@patch('gita.common.get_config_fname')
def test_add(self, mock_path_fname, tmp_path, input, expected):
def side_effect(input, _=None):
return tmp_path / f'{input}.txt'
mock_path_fname.side_effect = side_effect
utils.get_repos.cache_clear()
__main__.main(input)
utils.get_repos.cache_clear()
got = utils.get_repos()
assert len(got) == 1
assert got['gita']['type'] == expected
@patch('gita.utils.is_git', return_value=True)
def test_add_main(self, _, tmp_path, monkeypatch, tmpdir):
def side_effect(root=None):
if root is None:
return os.path.join(tmp_path, "gita")
else:
return os.path.join(root, ".gita")
def desc(repos, **_):
print(len(repos), repos.keys())
assert len(repos) > 0
for r, prop in repos.items():
if prop['type'] == 'm':
assert 'test_add_main' in r
break
else:
assert 0, 'no main repo found'
return ''
monkeypatch.setattr(common, 'get_config_dir', side_effect)
monkeypatch.setattr(utils, 'describe', desc)
utils.get_repos.cache_clear()
with tmpdir.as_cwd():
__main__.main(['add', '-m', '.'])
utils.get_repos.cache_clear()
__main__.main(['ll'])
@pytest.mark.parametrize('path_fname, expected', [
(PATH_FNAME, ''),
(PATH_FNAME_CLASH, "repo2: ['--haha', '--pp']\n"),
])
@patch('gita.utils.is_git', return_value=True)
@patch('gita.utils.get_groups', return_value={})
@patch('gita.common.get_config_fname')
def test_flags(mock_path_fname, _, __, path_fname, expected, capfd):
mock_path_fname.return_value = path_fname
utils.get_repos.cache_clear()
__main__.main(['flags'])
out, err = capfd.readouterr()
assert err == ''
assert out == expected
class TestLsLl:
@patch('gita.common.get_config_fname')
def testLl(self, mock_path_fname, capfd, tmp_path):
def test_ll(self, mock_path_fname, capfd, tmp_path):
"""
functional test
"""
# avoid modifying the local configuration
def side_effect(input):
def side_effect(input, _=None):
return tmp_path / f'{input}.txt'
#mock_path_fname.return_value = tmp_path / 'path_config.txt'
mock_path_fname.side_effect = side_effect
utils.get_repos.cache_clear()
__main__.main(['add', '.'])
out, err = capfd.readouterr()
assert err == ''
@ -52,11 +126,11 @@ class TestLsLl:
__main__.main(['ls', 'gita'])
out, err = capfd.readouterr()
assert err == ''
assert out.strip() == utils.get_repos()['gita']
assert out.strip() == utils.get_repos()['gita']['path']
def testLs(self, monkeypatch, capfd):
def test_ls(self, monkeypatch, capfd):
monkeypatch.setattr(utils, 'get_repos',
lambda: {'repo1': '/a/', 'repo2': '/b/'})
lambda: {'repo1': {'path': '/a/'}, 'repo2': {'path': '/b/'}})
monkeypatch.setattr(utils, 'describe', lambda x: x)
__main__.main(['ls'])
out, err = capfd.readouterr()
@ -71,19 +145,22 @@ class TestLsLl:
(PATH_FNAME,
"repo1 cmaster dsu\x1b[0m msg \nrepo2 cmaster dsu\x1b[0m msg \nxxx cmaster dsu\x1b[0m msg \n"),
(PATH_FNAME_EMPTY, ""),
(PATH_FNAME_MAIN,
'\x1b[4mmain1\x1b[0m cmaster dsu\x1b[0m msg \nxx cmaster dsu\x1b[0m msg \n'),
(PATH_FNAME_CLASH,
"repo1 cmaster dsu\x1b[0m msg\nrepo2 cmaster dsu\x1b[0m msg\nx/repo1 cmaster dsu\x1b[0m msg\n"
"repo1 cmaster dsu\x1b[0m msg \nrepo2 cmaster dsu\x1b[0m msg \n"
),
])
@patch('gita.utils.is_git', return_value=True)
@patch('gita.info.get_head', return_value="master")
@patch('gita.info._get_repo_status', return_value=("d", "s", "u", "c"))
@patch('gita.info.get_commit_msg', return_value="msg")
@patch('gita.info.get_commit_time', return_value="")
@patch('gita.common.get_config_fname')
def testWithPathFiles(self, mock_path_fname, _0, _1, _2, _3, path_fname,
def test_with_path_files(self, mock_path_fname, _0, _1, _2, _3, _4, path_fname,
expected, capfd):
def side_effect(input):
if input == 'repo_path':
def side_effect(input, _=None):
if input == 'repos.csv':
return path_fname
return f'/{input}'
mock_path_fname.side_effect = side_effect
@ -95,25 +172,63 @@ class TestLsLl:
assert out == expected
@pytest.mark.parametrize('input, expected', [
({'repo1': {'path': '/a/'}, 'repo2': {'path': '/b/'}}, ''),
])
@patch('subprocess.run')
@patch('gita.utils.get_repos', return_value={'repo1': '/a/', 'repo2': '/b/'})
def test_freeze(_, mock_run, capfd):
@patch('gita.utils.get_repos')
def test_freeze(mock_repos, mock_run, input, expected, capfd):
mock_repos.return_value = input
__main__.main(['freeze'])
assert mock_run.call_count == 2
out, err = capfd.readouterr()
assert err == ''
assert out == ',repo1,/a/\n,repo2,/b/\n'
assert out == expected
@patch('gita.utils.parse_clone_config', return_value=[
['git@github.com:user/repo.git', 'repo', '/a/repo']])
@patch('gita.utils.run_async', new=async_mock())
@patch('subprocess.run')
def test_clone(*_):
asyncio.set_event_loop(asyncio.new_event_loop())
args = argparse.Namespace()
args.fname = ['freeze_filename']
args.preserve_path = None
__main__.f_clone(args)
mock_run = utils.run_async.mock
assert mock_run.call_count == 1
cmds = ['git', 'clone', 'git@github.com:user/repo.git']
mock_run.assert_called_once_with('repo', Path.cwd(), cmds)
@patch('gita.utils.parse_clone_config', return_value=[
['git@github.com:user/repo.git', 'repo', '/a/repo']])
@patch('gita.utils.run_async', new=async_mock())
@patch('subprocess.run')
def test_clone_with_preserve_path(*_):
asyncio.set_event_loop(asyncio.new_event_loop())
args = argparse.Namespace()
args.fname = ['freeze_filename']
args.preserve_path = True
__main__.f_clone(args)
mock_run = utils.run_async.mock
assert mock_run.call_count == 1
cmds = ['git', 'clone', 'git@github.com:user/repo.git', '/a/repo']
mock_run.assert_called_once_with('repo', Path.cwd(), cmds)
@patch('os.path.isfile', return_value=True)
@patch('gita.common.get_config_fname', return_value='some path')
@patch('gita.utils.get_repos', return_value={'repo1': '/a/', 'repo2': '/b/'})
@patch('gita.utils.get_repos', return_value={'repo1': {'path': '/a/', 'type': ''},
'repo2': {'path': '/b/', 'type': ''}})
@patch('gita.utils.write_to_repo_file')
def test_rm(mock_write, *_):
args = argparse.Namespace()
args.repo = ['repo1']
__main__.f_rm(args)
mock_write.assert_called_once_with({'repo2': '/b/'}, 'w')
mock_write.assert_called_once_with(
{'repo2': {'path': '/b/', 'type': ''}}, 'w')
def test_not_add():
@ -121,17 +236,19 @@ def test_not_add():
__main__.main(['add', '/home/some/repo/'])
@patch('gita.utils.get_repos', return_value={'repo2': '/d/efg'})
@patch('gita.utils.get_repos', return_value={'repo2': {'path': '/d/efg',
'flags': []}})
@patch('subprocess.run')
def test_fetch(mock_run, *_):
asyncio.set_event_loop(asyncio.new_event_loop())
__main__.main(['fetch'])
mock_run.assert_called_once_with(['git', 'fetch'], cwd='/d/efg')
mock_run.assert_called_once_with(['git', 'fetch'], cwd='/d/efg', shell=False)
@patch(
'gita.utils.get_repos', return_value={
'repo1': '/a/bc',
'repo2': '/d/efg'
'repo1': {'path': '/a/bc', 'flags': []},
'repo2': {'path': '/d/efg', 'flags': []}
})
@patch('gita.utils.run_async', new=async_mock())
@patch('subprocess.run')
@ -149,28 +266,28 @@ def test_async_fetch(*_):
'diff --name-only --staged',
"commit -am 'lala kaka'",
])
@patch('gita.utils.get_repos', return_value={'repo7': 'path7'})
@patch('gita.utils.get_repos', return_value={'repo7': {'path': 'path7', 'flags': []}})
@patch('subprocess.run')
def test_superman(mock_run, _, input):
mock_run.reset_mock()
args = ['super', 'repo7'] + shlex.split(input)
__main__.main(args)
expected_cmds = ['git'] + shlex.split(input)
mock_run.assert_called_once_with(expected_cmds, cwd='path7')
mock_run.assert_called_once_with(expected_cmds, cwd='path7', shell=False)
@pytest.mark.parametrize('input', [
'diff --name-only --staged',
"commit -am 'lala kaka'",
])
@patch('gita.utils.get_repos', return_value={'repo7': 'path7'})
@patch('gita.utils.get_repos', return_value={'repo7': {'path': 'path7', 'flags': []}})
@patch('subprocess.run')
def test_shell(mock_run, _, input):
mock_run.reset_mock()
args = ['shell', 'repo7'] + shlex.split(input)
args = ['shell', 'repo7', input]
__main__.main(args)
expected_cmds = shlex.split(input)
mock_run.assert_called_once_with(expected_cmds, cwd='path7', check=True, stderr=-2, stdout=-1)
expected_cmds = input
mock_run.assert_called_once_with(expected_cmds, cwd='path7', check=True, shell=True, stderr=-2, stdout=-1)
class TestContext:
@ -184,21 +301,21 @@ class TestContext:
@patch('gita.utils.get_context', return_value=Path('gname.context'))
@patch('gita.utils.get_groups', return_value={'gname': ['a', 'b']})
def testDisplayContext(self, _, __, capfd):
def test_display_context(self, _, __, capfd):
__main__.main(['context'])
out, err = capfd.readouterr()
assert err == ''
assert 'gname: a b\n' == out
@patch('gita.utils.get_context')
def testReset(self, mock_ctx):
def test_reset(self, mock_ctx):
__main__.main(['context', 'none'])
mock_ctx.return_value.unlink.assert_called()
@patch('gita.utils.get_context', return_value=None)
@patch('gita.common.get_config_dir', return_value=TEST_DIR)
@patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []})
def testSetFirstTime(self, *_):
def test_set_first_time(self, *_):
ctx = TEST_DIR / 'lala.context'
assert not ctx.is_file()
__main__.main(['context', 'lala'])
@ -208,7 +325,7 @@ class TestContext:
@patch('gita.common.get_config_dir', return_value=TEST_DIR)
@patch('gita.utils.get_groups', return_value={'lala': ['b'], 'kaka': []})
@patch('gita.utils.get_context')
def testSetSecondTime(self, mock_ctx, *_):
def test_set_second_time(self, mock_ctx, *_):
__main__.main(['context', 'kaka'])
mock_ctx.return_value.rename.assert_called()
@ -216,7 +333,7 @@ class TestContext:
class TestGroupCmd:
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
def testLs(self, _, capfd):
def test_ls(self, _, capfd):
args = argparse.Namespace()
args.to_group = None
args.group_cmd = 'ls'
@ -227,7 +344,7 @@ class TestGroupCmd:
assert 'xx yy\n' == out
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
def testLl(self, _, capfd):
def test_ll(self, _, capfd):
args = argparse.Namespace()
args.to_group = None
args.group_cmd = None
@ -239,7 +356,7 @@ class TestGroupCmd:
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file')
def testRename(self, mock_write, _):
def test_rename(self, mock_write, _):
args = argparse.Namespace()
args.gname = 'xx'
args.new_name = 'zz'
@ -250,7 +367,7 @@ class TestGroupCmd:
mock_write.assert_called_once_with(expected, 'w')
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
def testRenameError(self, *_):
def test_rename_error(self, *_):
args = argparse.Namespace()
args.gname = 'xx'
args.new_name = 'yy'
@ -266,7 +383,7 @@ class TestGroupCmd:
@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file')
def testRm(self, mock_write, _, __, input, expected):
def test_rm(self, mock_write, _, __, input, expected):
utils.get_groups.cache_clear()
args = ['group', 'rm'] + shlex.split(input)
__main__.main(args)
@ -275,7 +392,7 @@ class TestGroupCmd:
@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file')
def testAdd(self, mock_write, *_):
def test_add(self, mock_write, *_):
args = argparse.Namespace()
args.to_group = ['a', 'c']
args.group_cmd = 'add'
@ -287,7 +404,7 @@ class TestGroupCmd:
@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file')
def testAddToExisting(self, mock_write, *_):
def test_add_to_existing(self, mock_write, *_):
args = argparse.Namespace()
args.to_group = ['a', 'c']
args.group_cmd = 'add'
@ -300,7 +417,7 @@ class TestGroupCmd:
@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
@patch('gita.utils.write_to_groups_file')
def testRmRepo(self, mock_write, *_):
def test_rm_repo(self, mock_write, *_):
args = argparse.Namespace()
args.from_group = ['a', 'c']
args.group_cmd = 'rmrepo'
@ -310,6 +427,21 @@ class TestGroupCmd:
mock_write.assert_called_once_with(
{'xx': ['b'], 'yy': ['a', 'c', 'd']}, 'w')
@patch('gita.common.get_config_fname')
def test_integration(self, mock_path_fname, tmp_path, capfd):
def side_effect(input, _=None):
return tmp_path / f'{input}.csv'
mock_path_fname.side_effect = side_effect
__main__.main('add .'.split())
utils.get_repos.cache_clear()
__main__.main('group add gita -n test'.split())
utils.get_groups.cache_clear()
__main__.main('ll test'.split())
out, err = capfd.readouterr()
assert err == ''
assert 'gita' in out
@patch('gita.utils.is_git', return_value=True)
@patch('gita.common.get_config_fname', return_value=PATH_FNAME)
@ -319,44 +451,61 @@ def test_rename(mock_rename, _, __):
args = ['rename', 'repo1', 'abc']
__main__.main(args)
mock_rename.assert_called_once_with(
{'repo1': '/a/bcd/repo1', 'repo2': '/e/fgh/repo2',
'xxx': '/a/b/c/repo3'},
{'repo1': {'path': '/a/bcd/repo1', 'type': '', 'flags': []},
'xxx': {'path': '/a/b/c/repo3', 'type': '', 'flags': []},
'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': []}},
'repo1', 'abc')
class TestInfo:
@patch('gita.common.get_config_fname', return_value='')
def testLl(self, _, capfd):
def test_ll(self, _, capfd):
args = argparse.Namespace()
args.info_cmd = None
__main__.f_info(args)
out, err = capfd.readouterr()
assert 'In use: branch,commit_msg\nUnused: path\n' == out
assert 'In use: branch,commit_msg,commit_time\nUnused: path\n' == out
assert err == ''
@patch('gita.common.get_config_fname', return_value='')
@patch('yaml.dump')
def testAdd(self, mock_dump, _):
@patch('gita.common.get_config_fname')
def test_add(self, mock_get_fname, tmpdir):
args = argparse.Namespace()
args.info_cmd = 'add'
args.info_item = 'path'
with patch('builtins.open', mock_open(), create=True):
with tmpdir.as_cwd():
csv_config = Path.cwd() / 'info.csv'
mock_get_fname.return_value = csv_config
__main__.f_info(args)
mock_dump.assert_called_once()
args, kwargs = mock_dump.call_args
assert args[0] == ['branch', 'commit_msg', 'path']
assert kwargs == {'default_flow_style': None}
items = info.get_info_items()
assert items == ['branch', 'commit_msg', 'commit_time', 'path']
@patch('gita.common.get_config_fname', return_value='')
@patch('yaml.dump')
def testRm(self, mock_dump, _):
@patch('gita.common.get_config_fname')
def test_rm(self, mock_get_fname, tmpdir):
args = argparse.Namespace()
args.info_cmd = 'rm'
args.info_item = 'commit_msg'
with patch('builtins.open', mock_open(), create=True):
with tmpdir.as_cwd():
csv_config = Path.cwd() / 'info.csv'
mock_get_fname.return_value = csv_config
__main__.f_info(args)
mock_dump.assert_called_once()
args, kwargs = mock_dump.call_args
assert args[0] == ['branch']
assert kwargs == {'default_flow_style': None}
items = info.get_info_items()
assert items == ['branch', 'commit_time']
@patch('gita.common.get_config_fname')
def test_set_color(mock_get_fname, tmpdir):
args = argparse.Namespace()
args.color_cmd = 'set'
args.color = 'redrum' # this color doesn't exist
args.situation = 'in-sync'
with tmpdir.as_cwd():
csv_config = Path.cwd() / 'colors.csv'
mock_get_fname.return_value = csv_config
__main__.f_color(args)
info.get_color_encoding.cache_clear() # avoid side effect
items = info.get_color_encoding()
info.get_color_encoding.cache_clear() # avoid side effect
assert items == {'no-remote': 'white', 'in-sync': 'redrum',
'diverged': 'red', 'local-ahead': 'purple',
'remote-ahead': 'yellow'}

View file

@ -1,5 +1,7 @@
import pytest
import asyncio
import subprocess
from pathlib import Path
from unittest.mock import patch, mock_open
from gita import utils, info
@ -8,16 +10,41 @@ from conftest import (
)
@pytest.mark.parametrize('repo_path, paths, expected', [
('/a/b/c/repo', ['/a/b'], ('b', 'c')),
])
def test_generate_dir_hash(repo_path, paths, expected):
got = utils._generate_dir_hash(repo_path, paths)
assert got == expected
@pytest.mark.parametrize('repos, paths, expected', [
({'r1': {'path': '/a/b//repo1'}, 'r2': {'path': '/a/b/repo2'}},
['/a/b'], {'b': ['r1', 'r2']}),
({'r1': {'path': '/a/b//repo1'}, 'r2': {'path': '/a/b/c/repo2'}},
['/a/b'], {'b': ['r1', 'r2'], 'b-c': ['r2']}),
({'r1': {'path': '/a/b/c/repo1'}, 'r2': {'path': '/a/b/c/repo2'}},
['/a/b'], {'b-c': ['r1', 'r2'], 'b': ['r1', 'r2']}),
])
def test_auto_group(repos, paths, expected):
got = utils.auto_group(repos, paths)
assert got == expected
@pytest.mark.parametrize('test_input, diff_return, expected', [
([{'abc': '/root/repo/'}, False], True, 'abc \x1b[31mrepo *+_ \x1b[0m msg'),
([{'abc': '/root/repo/'}, True], True, 'abc repo *+_ msg'),
([{'repo': '/root/repo2/'}, False], False, 'repo \x1b[32mrepo _ \x1b[0m msg'),
([{'abc': {'path': '/root/repo/', 'type': '', 'flags': []}}, False],
True, 'abc \x1b[31mrepo *+_ \x1b[0m msg xx'),
([{'abc': {'path': '/root/repo/', 'type': '', 'flags': []}}, True],
True, 'abc repo *+_ msg xx'),
([{'repo': {'path': '/root/repo2/', 'type': '', 'flags': []}}, False],
False, 'repo \x1b[32mrepo _ \x1b[0m msg xx'),
])
def test_describe(test_input, diff_return, expected, monkeypatch):
monkeypatch.setattr(info, 'get_head', lambda x: 'repo')
monkeypatch.setattr(info, 'run_quiet_diff', lambda _: diff_return)
monkeypatch.setattr(info, 'get_commit_msg', lambda _: "msg")
monkeypatch.setattr(info, 'has_untracked', lambda: True)
monkeypatch.setattr(info, 'run_quiet_diff', lambda *_: diff_return)
monkeypatch.setattr(info, 'get_commit_msg', lambda *_: "msg")
monkeypatch.setattr(info, 'get_commit_time', lambda *_: "xx")
monkeypatch.setattr(info, 'has_untracked', lambda *_: True)
monkeypatch.setattr('os.chdir', lambda x: None)
print('expected: ', repr(expected))
print('got: ', repr(next(utils.describe(*test_input))))
@ -26,15 +53,14 @@ def test_describe(test_input, diff_return, expected, monkeypatch):
@pytest.mark.parametrize('path_fname, expected', [
(PATH_FNAME, {
'repo1': '/a/bcd/repo1',
'repo2': '/e/fgh/repo2',
'xxx': '/a/b/c/repo3',
'repo1': {'path': '/a/bcd/repo1', 'type': '', 'flags': []},
'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': []},
'xxx': {'path': '/a/b/c/repo3', 'type': '', 'flags': []},
}),
(PATH_FNAME_EMPTY, {}),
(PATH_FNAME_CLASH, {
'repo1': '/a/bcd/repo1',
'repo2': '/e/fgh/repo2',
'x/repo1': '/root/x/repo1'
'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': ['--haha', '--pp']},
'repo1': {'path': '/root/x/repo1', 'type': '', 'flags': []}
}),
])
@patch('gita.utils.is_git', return_value=True)
@ -70,42 +96,46 @@ def test_get_groups(mock_group_fname, group_fname, expected):
@patch('os.path.getsize', return_value=True)
def test_custom_push_cmd(*_):
with patch('builtins.open',
mock_open(read_data='push:\n cmd: hand\n help: me')):
mock_open(read_data='{"push":{"cmd":"hand","help":"me","allow_all":true}}')):
cmds = utils.get_cmds_from_files()
assert cmds['push'] == {'cmd': 'hand', 'help': 'me'}
assert cmds['push'] == {'cmd': 'hand', 'help': 'me', 'allow_all': True}
@pytest.mark.parametrize(
'path_input, expected',
[
(['/home/some/repo/'], '/home/some/repo,repo\n'), # add one new
(['/home/some/repo'], '/home/some/repo,some/repo,,\r\n'), # add one new
(['/home/some/repo1', '/repo2'],
{'/repo2,repo2\n/home/some/repo1,repo1\n', # add two new
'/home/some/repo1,repo1\n/repo2,repo2\n'}), # add two new
{'/repo2,repo2,,\r\n', # add two new
'/home/some/repo1,repo1,,\r\n'}), # add two new
(['/home/some/repo1', '/nos/repo'],
'/home/some/repo1,repo1\n'), # add one old one new
'/home/some/repo1,repo1,,\r\n'), # add one old one new
])
@patch('os.makedirs')
@patch('gita.utils.is_git', return_value=True)
def test_add_repos(_0, _1, path_input, expected, monkeypatch):
monkeypatch.setenv('XDG_CONFIG_HOME', '/config')
with patch('builtins.open', mock_open()) as mock_file:
utils.add_repos({'repo': '/nos/repo'}, path_input)
mock_file.assert_called_with('/config/gita/repo_path', 'a+')
utils.add_repos({'repo': {'path': '/nos/repo'}}, path_input)
mock_file.assert_called_with('/config/gita/repos.csv', 'a+', newline='')
handle = mock_file()
if type(expected) == str:
handle.write.assert_called_once_with(expected)
else:
handle.write.assert_called_once()
# the write order is random
assert handle.write.call_count == 2
args, kwargs = handle.write.call_args
assert args[0] in expected
assert not kwargs
@patch('gita.utils.write_to_groups_file')
@patch('gita.utils.write_to_repo_file')
def test_rename_repo(mock_write):
utils.rename_repo({'r1': '/a/b', 'r2': '/c/c'}, 'r2', 'xxx')
mock_write.assert_called_once_with({'r1': '/a/b', 'xxx': '/c/c'}, 'w')
def test_rename_repo(mock_write, _):
repos = {'r1': {'path': '/a/b', 'type': None},
'r2': {'path': '/c/c', 'type': None}}
utils.rename_repo(repos, 'r2', 'xxx')
mock_write.assert_called_once_with(repos, 'w')
def test_async_output(capfd):
@ -124,3 +154,10 @@ def test_async_output(capfd):
out, err = capfd.readouterr()
assert err == ''
assert out == 'myrepo: 0\nmyrepo: 0\n\nmyrepo: 1\nmyrepo: 1\n\nmyrepo: 2\nmyrepo: 2\n\nmyrepo: 3\nmyrepo: 3\n\n'
def test_is_git(tmpdir):
with tmpdir.as_cwd():
subprocess.run('git init --bare .'.split())
assert utils.is_git(Path.cwd()) is False
assert utils.is_git(Path.cwd(), is_bare=True) is True

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 :