Adding upstream version 0.11.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
1b2b356ce0
commit
3ac0de4543
11 changed files with 528 additions and 157 deletions
56
README.md
56
README.md
|
@ -14,7 +14,7 @@
|
||||||
| | ____ | | | | | ___ |
|
| | ____ | | | | | ___ |
|
||||||
| | \_ ) | | | | | ( ) |
|
| | \_ ) | | | | | ( ) |
|
||||||
| (___) |__) (___ | | | ) ( |
|
| (___) |__) (___ | | | ) ( |
|
||||||
(_______)_______/ )_( |/ \| v0.10
|
(_______)_______/ )_( |/ \| v0.11
|
||||||
```
|
```
|
||||||
|
|
||||||
# Gita: a command-line tool to manage multiple git repos
|
# Gita: a command-line tool to manage multiple git repos
|
||||||
|
@ -29,7 +29,14 @@ I also hate to change directories to execute git commands.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Here the branch color distinguishes 5 situations between local and remote branches:
|
In the screenshot, the `gita remote nowhub` command translates to `git remote -v`
|
||||||
|
for the `nowhub` 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).
|
||||||
|
To run arbitrary `git` command, see the [superman mode section](#superman).
|
||||||
|
|
||||||
|
The branch color distinguishes 5 situations between local and remote branches:
|
||||||
|
|
||||||
- white: local has no remote
|
- white: local has no remote
|
||||||
- green: local is the same as remote
|
- green: local is the same as remote
|
||||||
|
@ -50,32 +57,50 @@ The additional status symbols denote
|
||||||
The bookkeeping sub-commands are
|
The bookkeeping sub-commands are
|
||||||
|
|
||||||
- `gita add <repo-path(s)>`: add repo(s) to `gita`
|
- `gita add <repo-path(s)>`: add repo(s) to `gita`
|
||||||
- `gita rm <repo-name(s)>`: remove repo(s) from `gita` (won't remove files from disk)
|
- `gita context`: context sub-command
|
||||||
- `gita group`: show grouping of the repos
|
- `gita context`: show current context
|
||||||
- `gita group <repo-name(s)>`: group repos
|
- `gita context none`: remove context
|
||||||
- `gita ungroup <repo-name(s)>`: remove grouping for repos
|
- `gita context <group-name>`: set context to `group-name`, all operations then only apply to repos in this group
|
||||||
|
- `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 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 [ll]`: display existing groups with repos
|
||||||
|
- `gita group ls`: display existing group names
|
||||||
|
- `gita group rename <group-name> <new-name>`: change group name
|
||||||
|
- `gita group rm <group-name(s)>`: delete group(s)
|
||||||
|
- `gita info`: info sub-command
|
||||||
|
- `gita info [ll]`: display the used and unused information items
|
||||||
|
- `gita info add <info-item>`: enable information item
|
||||||
|
- `gita info rm <info-item>`: disable information item
|
||||||
- `gita ll`: display the status of all repos
|
- `gita ll`: display the status of all repos
|
||||||
- `gita ll <group-name>`: display the status of repos in a group
|
- `gita ll <group-name>`: display the status of repos in a group
|
||||||
- `gita ls`: display the names of all repos
|
- `gita ls`: display the names of all repos
|
||||||
- `gita ls <repo-name>`: display the absolute path of one repo
|
- `gita ls <repo-name>`: display the absolute path of one repo
|
||||||
- `gita rename <repo-name> <new-name>`: rename a repo
|
- `gita rename <repo-name> <new-name>`: rename a repo
|
||||||
- `gita info`: display the used and unused information items
|
- `gita rm <repo-name(s)>`: remove repo(s) from `gita` (won't remove files from disk)
|
||||||
- `gita -v`: display gita version
|
- `gita -v`: display gita version
|
||||||
|
|
||||||
Repo paths are saved in `$XDG_CONFIG_HOME/gita/repo_path` (most likely `~/.config/gita/repo_path`).
|
|
||||||
|
|
||||||
The delegating sub-commands are of two formats
|
The delegating sub-commands are of two formats
|
||||||
|
|
||||||
- `gita <sub-command> [repo-name(s) or group-name(s)]`:
|
- `gita <sub-command> [repo-name(s) or group-name(s)]`:
|
||||||
optional repo or group input, and no input means all repos.
|
optional repo or group input, and **no input means all repos**.
|
||||||
- `gita <sub-command> <repo-name(s) or groups-name(s)>`:
|
- `gita <sub-command> <repo-name(s) or groups-name(s)>`:
|
||||||
required repo name(s) or group name(s) input
|
required repo name(s) or group name(s) input
|
||||||
|
|
||||||
|
In either case, the `gita` command translates to running `git <sub-command>` for the corresponding repos.
|
||||||
By default, only `fetch` and `pull` take optional input.
|
By default, only `fetch` and `pull` take optional input.
|
||||||
|
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).
|
||||||
|
To run arbitrary `git` command, see the [superman mode section](#superman).
|
||||||
|
|
||||||
If more than one repos are specified, the git command will run asynchronously,
|
If more than one repos are specified, the git command will run asynchronously,
|
||||||
with the exception of `log`, `difftool` and `mergetool`, which require non-trivial user input.
|
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`).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
To install the latest version, run
|
To install the latest version, run
|
||||||
|
@ -110,7 +135,7 @@ or
|
||||||
[.gita-completion.zsh](https://github.com/nosarthur/gita/blob/master/.gita-completion.zsh)
|
[.gita-completion.zsh](https://github.com/nosarthur/gita/blob/master/.gita-completion.zsh)
|
||||||
and source it in the .rc file.
|
and source it in the .rc file.
|
||||||
|
|
||||||
## Superman mode
|
## <a name='superman'></a> Superman mode
|
||||||
|
|
||||||
The superman mode delegates any git command/alias.
|
The superman mode delegates any git command/alias.
|
||||||
Usage:
|
Usage:
|
||||||
|
@ -126,7 +151,7 @@ For example,
|
||||||
- `gita super frontend-repo backend-repo commit -am 'implement a new feature'`
|
- `gita super frontend-repo backend-repo commit -am 'implement a new feature'`
|
||||||
executes `git commit -am 'implement a new feature'` for `frontend-repo` and `backend-repo`
|
executes `git commit -am 'implement a new feature'` for `frontend-repo` and `backend-repo`
|
||||||
|
|
||||||
## Customization
|
## <a name='custom'></a> Customization
|
||||||
|
|
||||||
Custom delegating sub-commands can be defined in `$XDG_CONFIG_HOME/gita/cmds.yml`
|
Custom delegating sub-commands can be defined in `$XDG_CONFIG_HOME/gita/cmds.yml`
|
||||||
(most likely `~/.config/gita/cmds.yml`).
|
(most likely `~/.config/gita/cmds.yml`).
|
||||||
|
@ -164,8 +189,11 @@ comaster:
|
||||||
|
|
||||||
Another customization is the information items displayed by `gita ll`.
|
Another customization is the information items displayed by `gita ll`.
|
||||||
The used and unused information items are shown with `gita info` and one can
|
The used and unused information items are shown with `gita info` and one can
|
||||||
create `$XDG_CONFIG_HOME/gita/info.yml` to customize it. For example, the
|
create `$XDG_CONFIG_HOME/gita/info.yml` to customize it.
|
||||||
default information items setting corresponds to
|
(I am thinking of hiding all these details from user at the moment, which means
|
||||||
|
you probably don't need to read the rest of this section.)
|
||||||
|
|
||||||
|
For example, the default information items setting corresponds to
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- branch
|
- branch
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
| | ____ | | | | | ___ |
|
| | ____ | | | | | ___ |
|
||||||
| | \_ ) | | | | | ( ) |
|
| | \_ ) | | | | | ( ) |
|
||||||
| (___) |__) (___ | | | ) ( |
|
| (___) |__) (___ | | | ) ( |
|
||||||
(_______)_______/ )_( |/ \| v0.10
|
(_______)_______/ )_( |/ \| v0.11
|
||||||
```
|
```
|
||||||
|
|
||||||
# Gita:一个管理多个 git 库的命令行工具
|
# Gita:一个管理多个 git 库的命令行工具
|
||||||
|
@ -46,15 +46,23 @@
|
||||||
基础指令:
|
基础指令:
|
||||||
|
|
||||||
- `gita add <repo-path(s)>`: 添加库
|
- `gita add <repo-path(s)>`: 添加库
|
||||||
- `gita rm <repo-name(s)>`: 移除库(不会删除文件)
|
- `gita context`: 情境命令
|
||||||
- `gita group`: 显示库的组群
|
- `gita context`: 显示当前的情境
|
||||||
- `gita group` <repo-name(s)>: 将库分组
|
- `gita context none`: 去除情境
|
||||||
|
- `gita context <group-name>`: 把情境设置成`group-name`, 之后所有的操作只作用到这个组里的库
|
||||||
|
- `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 info`: 显示已用的和未用的信息项
|
||||||
- `gita ll`: 显示所有库的状态信息
|
- `gita ll`: 显示所有库的状态信息
|
||||||
- `gita ll <group-name>`: 显示一个组群中库的状态信息
|
- `gita ll <group-name>`: 显示一个组群中库的状态信息
|
||||||
- `gita ls`: 显示所有库的名字
|
- `gita ls`: 显示所有库的名字
|
||||||
- `gita ls <repo-name>`: 显示一个库的绝对路径
|
- `gita ls <repo-name>`: 显示一个库的绝对路径
|
||||||
- `gita rename <repo-name> <new-name>`: 重命名一个库
|
- `gita rename <repo-name> <new-name>`: 重命名一个库
|
||||||
- `gita info`: 显示已用的和未用的信息项
|
- `gita rm <repo-name(s)>`: 移除库(不会删除文件)
|
||||||
- `gita -v`: 显示版本号
|
- `gita -v`: 显示版本号
|
||||||
|
|
||||||
库的路径存在`$XDG_CONFIG_HOME/gita/repo_path` (多半是`~/.config/gita/repo_path`)。
|
库的路径存在`$XDG_CONFIG_HOME/gita/repo_path` (多半是`~/.config/gita/repo_path`)。
|
||||||
|
|
193
gita/__main__.py
193
gita/__main__.py
|
@ -15,16 +15,23 @@ https://github.com/nosarthur/gita/blob/master/.gita-completion.bash
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
import argparse
|
import argparse
|
||||||
import subprocess
|
import subprocess
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
from itertools import chain
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from . import utils, info
|
from . import utils, info, common
|
||||||
|
|
||||||
|
|
||||||
def f_add(args: argparse.Namespace):
|
def f_add(args: argparse.Namespace):
|
||||||
repos = utils.get_repos()
|
repos = utils.get_repos()
|
||||||
utils.add_repos(repos, args.paths)
|
paths = args.paths
|
||||||
|
if args.recursive:
|
||||||
|
paths = chain.from_iterable(Path(p).glob('**') for p in args.paths)
|
||||||
|
utils.add_repos(repos, paths)
|
||||||
|
|
||||||
|
|
||||||
def f_rename(args: argparse.Namespace):
|
def f_rename(args: argparse.Namespace):
|
||||||
|
@ -32,12 +39,33 @@ def f_rename(args: argparse.Namespace):
|
||||||
utils.rename_repo(repos, args.repo[0], args.new_name)
|
utils.rename_repo(repos, args.repo[0], args.new_name)
|
||||||
|
|
||||||
|
|
||||||
def f_info(_):
|
def f_color(args: argparse.Namespace):
|
||||||
all_items, to_display = info.get_info_items()
|
cmd = args.color_cmd or 'll'
|
||||||
print('In use:', ','.join(to_display))
|
if cmd == 'll': # pragma: no cover
|
||||||
unused = set(all_items) - set(to_display)
|
info.show_colors()
|
||||||
if unused:
|
elif cmd == 'set':
|
||||||
print('Unused:', ' '.join(unused))
|
print('not implemented')
|
||||||
|
|
||||||
|
|
||||||
|
def f_info(args: argparse.Namespace):
|
||||||
|
to_display = info.get_info_items()
|
||||||
|
cmd = args.info_cmd or 'll'
|
||||||
|
if cmd == 'll':
|
||||||
|
print('In use:', ','.join(to_display))
|
||||||
|
unused = set(info.ALL_INFO_ITEMS) - set(to_display)
|
||||||
|
if 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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
def f_ll(args: argparse.Namespace):
|
def f_ll(args: argparse.Namespace):
|
||||||
|
@ -45,10 +73,13 @@ def f_ll(args: argparse.Namespace):
|
||||||
Display details of all repos
|
Display details of all repos
|
||||||
"""
|
"""
|
||||||
repos = utils.get_repos()
|
repos = utils.get_repos()
|
||||||
|
ctx = utils.get_context()
|
||||||
|
if args.group is None and ctx:
|
||||||
|
args.group = ctx.stem
|
||||||
if args.group: # only display repos in this group
|
if args.group: # only display repos in this group
|
||||||
group_repos = utils.get_groups()[args.group]
|
group_repos = utils.get_groups()[args.group]
|
||||||
repos = {k: repos[k] for k in group_repos if k in repos}
|
repos = {k: repos[k] for k in group_repos if k in repos}
|
||||||
for line in utils.describe(repos):
|
for line in utils.describe(repos, no_colors=args.no_colors):
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,8 +93,26 @@ def f_ls(args: argparse.Namespace):
|
||||||
|
|
||||||
def f_group(args: argparse.Namespace):
|
def f_group(args: argparse.Namespace):
|
||||||
groups = utils.get_groups()
|
groups = utils.get_groups()
|
||||||
if args.to_group:
|
cmd = args.group_cmd or 'll'
|
||||||
gname = input('group name? ')
|
if cmd == 'll':
|
||||||
|
for group, repos in groups.items():
|
||||||
|
print(f"{group}: {' '.join(repos)}")
|
||||||
|
elif cmd == 'ls':
|
||||||
|
print(' '.join(groups))
|
||||||
|
elif cmd == 'rename':
|
||||||
|
new_name = args.new_name
|
||||||
|
if new_name in groups:
|
||||||
|
sys.exit(f'{new_name} already exists.')
|
||||||
|
gname = args.gname
|
||||||
|
groups[new_name] = groups[gname]
|
||||||
|
del groups[gname]
|
||||||
|
utils.write_to_groups_file(groups, 'w')
|
||||||
|
elif cmd == 'rm':
|
||||||
|
for name in args.to_ungroup:
|
||||||
|
del groups[name]
|
||||||
|
utils.write_to_groups_file(groups, 'w')
|
||||||
|
elif cmd == 'add':
|
||||||
|
gname = args.gname
|
||||||
if gname in groups:
|
if gname in groups:
|
||||||
gname_repos = set(groups[gname])
|
gname_repos = set(groups[gname])
|
||||||
gname_repos.update(args.to_group)
|
gname_repos.update(args.to_group)
|
||||||
|
@ -71,31 +120,32 @@ def f_group(args: argparse.Namespace):
|
||||||
utils.write_to_groups_file(groups, 'w')
|
utils.write_to_groups_file(groups, 'w')
|
||||||
else:
|
else:
|
||||||
utils.write_to_groups_file({gname: sorted(args.to_group)}, 'a+')
|
utils.write_to_groups_file({gname: sorted(args.to_group)}, 'a+')
|
||||||
else:
|
|
||||||
for group, repos in groups.items():
|
|
||||||
print(f"{group}: {' '.join(repos)}")
|
|
||||||
|
|
||||||
|
|
||||||
def f_ungroup(args: argparse.Namespace):
|
def f_context(args: argparse.Namespace):
|
||||||
groups = utils.get_groups()
|
choice = args.choice
|
||||||
to_ungroup = set(args.to_ungroup)
|
ctx = utils.get_context()
|
||||||
to_del = []
|
if choice is None:
|
||||||
for name, repos in groups.items():
|
if ctx:
|
||||||
remaining = set(repos) - to_ungroup
|
group = ctx.stem
|
||||||
if remaining:
|
print(f"{group}: {' '.join(utils.get_groups()[group])}")
|
||||||
groups[name] = list(sorted(remaining))
|
|
||||||
else:
|
else:
|
||||||
to_del.append(name)
|
print('Context is not set')
|
||||||
for name in to_del:
|
elif choice == 'none': # remove context
|
||||||
del groups[name]
|
ctx and ctx.unlink()
|
||||||
utils.write_to_groups_file(groups, 'w')
|
else: # set context
|
||||||
|
fname = Path(common.get_config_dir()) / (choice + '.context')
|
||||||
|
if ctx:
|
||||||
|
ctx.rename(fname)
|
||||||
|
else:
|
||||||
|
open(fname, 'w').close()
|
||||||
|
|
||||||
|
|
||||||
def f_rm(args: argparse.Namespace):
|
def f_rm(args: argparse.Namespace):
|
||||||
"""
|
"""
|
||||||
Unregister repo(s) from gita
|
Unregister repo(s) from gita
|
||||||
"""
|
"""
|
||||||
path_file = utils.get_config_fname('repo_path')
|
path_file = common.get_config_fname('repo_path')
|
||||||
if os.path.isfile(path_file):
|
if os.path.isfile(path_file):
|
||||||
repos = utils.get_repos()
|
repos = utils.get_repos()
|
||||||
for repo in args.repo:
|
for repo in args.repo:
|
||||||
|
@ -110,6 +160,9 @@ def f_git_cmd(args: argparse.Namespace):
|
||||||
"""
|
"""
|
||||||
repos = utils.get_repos()
|
repos = utils.get_repos()
|
||||||
groups = utils.get_groups()
|
groups = utils.get_groups()
|
||||||
|
ctx = utils.get_context()
|
||||||
|
if not args.repo and ctx:
|
||||||
|
args.repo = [ctx.stem]
|
||||||
if args.repo: # with user specified repo(s) or group(s)
|
if args.repo: # with user specified repo(s) or group(s)
|
||||||
chosen = {}
|
chosen = {}
|
||||||
for k in args.repo:
|
for k in args.repo:
|
||||||
|
@ -170,6 +223,8 @@ def main(argv=None):
|
||||||
# bookkeeping sub-commands
|
# bookkeeping sub-commands
|
||||||
p_add = subparsers.add_parser('add', help='add repo(s)')
|
p_add = subparsers.add_parser('add', help='add repo(s)')
|
||||||
p_add.add_argument('paths', nargs='+', help="add repo(s)")
|
p_add.add_argument('paths', nargs='+', help="add repo(s)")
|
||||||
|
p_add.add_argument('-r', dest='recursive', action='store_true',
|
||||||
|
help="recursively add repo(s) in the given path.")
|
||||||
p_add.set_defaults(func=f_add)
|
p_add.set_defaults(func=f_add)
|
||||||
|
|
||||||
p_rm = subparsers.add_parser('rm', help='remove repo(s)')
|
p_rm = subparsers.add_parser('rm', help='remove repo(s)')
|
||||||
|
@ -190,8 +245,38 @@ def main(argv=None):
|
||||||
help="new name")
|
help="new name")
|
||||||
p_rename.set_defaults(func=f_rename)
|
p_rename.set_defaults(func=f_rename)
|
||||||
|
|
||||||
p_info = subparsers.add_parser('info', help='show information items of the ll sub-command')
|
p_color = subparsers.add_parser('color',
|
||||||
|
help='display and modify branch coloring of the ll sub-command.')
|
||||||
|
p_color.set_defaults(func=f_color)
|
||||||
|
color_cmds = p_color.add_subparsers(dest='color_cmd',
|
||||||
|
help='additional help with sub-command -h')
|
||||||
|
color_cmds.add_parser('ll',
|
||||||
|
description='display available colors and the current branch coloring in the ll sub-command')
|
||||||
|
pc_set = color_cmds.add_parser('set',
|
||||||
|
description='Set color for local/remote situation.')
|
||||||
|
pc_set.add_argument('situation',
|
||||||
|
choices=info.get_color_encoding(),
|
||||||
|
help="5 possible local/remote situations")
|
||||||
|
pc_set.add_argument('color',
|
||||||
|
choices=[c.name for c in info.Color],
|
||||||
|
help="available colors")
|
||||||
|
|
||||||
|
p_info = subparsers.add_parser('info',
|
||||||
|
help='list, add, or remove information items of the ll sub-command.')
|
||||||
p_info.set_defaults(func=f_info)
|
p_info.set_defaults(func=f_info)
|
||||||
|
info_cmds = p_info.add_subparsers(dest='info_cmd',
|
||||||
|
help='additional help with sub-command -h')
|
||||||
|
info_cmds.add_parser('ll',
|
||||||
|
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'),
|
||||||
|
help="information item to add")
|
||||||
|
info_cmds.add_parser('rm', description='Disable information item.'
|
||||||
|
).add_argument('info_item',
|
||||||
|
choices=('branch', 'commit_msg', 'path'),
|
||||||
|
help="information item to delete")
|
||||||
|
|
||||||
|
|
||||||
ll_doc = f''' status symbols:
|
ll_doc = f''' status symbols:
|
||||||
+: staged changes
|
+: staged changes
|
||||||
|
@ -212,8 +297,19 @@ def main(argv=None):
|
||||||
nargs='?',
|
nargs='?',
|
||||||
choices=utils.get_groups(),
|
choices=utils.get_groups(),
|
||||||
help="show repos in the chosen group")
|
help="show repos in the chosen group")
|
||||||
|
p_ll.add_argument('-n', '--no-colors', action='store_true',
|
||||||
|
help='Disable coloring on the branch names.')
|
||||||
p_ll.set_defaults(func=f_ll)
|
p_ll.set_defaults(func=f_ll)
|
||||||
|
|
||||||
|
p_context = subparsers.add_parser('context',
|
||||||
|
help='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',
|
||||||
|
nargs='?',
|
||||||
|
choices=set().union(utils.get_groups(), {'none'}),
|
||||||
|
help="Without argument, show current context. Otherwise choose a group as context. To remove context, use 'none'. ")
|
||||||
|
p_context.set_defaults(func=f_context)
|
||||||
|
|
||||||
p_ls = subparsers.add_parser(
|
p_ls = subparsers.add_parser(
|
||||||
'ls', help='display names of all repos, or path of a chosen repo')
|
'ls', help='display names of all repos, or path of a chosen repo')
|
||||||
p_ls.add_argument('repo',
|
p_ls.add_argument('repo',
|
||||||
|
@ -223,21 +319,34 @@ def main(argv=None):
|
||||||
p_ls.set_defaults(func=f_ls)
|
p_ls.set_defaults(func=f_ls)
|
||||||
|
|
||||||
p_group = subparsers.add_parser(
|
p_group = subparsers.add_parser(
|
||||||
'group', help='group repos or display names of all groups if no repo is provided')
|
'group', help='list, add, or remove repo group(s)')
|
||||||
p_group.add_argument('to_group',
|
|
||||||
nargs='*',
|
|
||||||
choices=utils.get_choices(),
|
|
||||||
help="repo(s) to be grouped")
|
|
||||||
p_group.set_defaults(func=f_group)
|
p_group.set_defaults(func=f_group)
|
||||||
|
group_cmds = p_group.add_subparsers(dest='group_cmd',
|
||||||
p_ungroup = subparsers.add_parser(
|
help='additional help with sub-command -h')
|
||||||
'ungroup', help='remove group information for repos',
|
group_cmds.add_parser('ll', description='List all groups with repos.')
|
||||||
description="Remove group information on repos")
|
group_cmds.add_parser('ls', description='List all group names.')
|
||||||
p_ungroup.add_argument('to_ungroup',
|
pg_add = group_cmds.add_parser('add', description='Add repo(s) to a group.')
|
||||||
nargs='+',
|
pg_add.add_argument('to_group',
|
||||||
choices=utils.get_repos(),
|
nargs='+',
|
||||||
help="repo(s) to be ungrouped")
|
metavar='repo',
|
||||||
p_ungroup.set_defaults(func=f_ungroup)
|
choices=utils.get_repos(),
|
||||||
|
help="repo(s) to be grouped")
|
||||||
|
pg_add.add_argument('-n', '--name',
|
||||||
|
dest='gname',
|
||||||
|
metavar='group-name',
|
||||||
|
required=True,
|
||||||
|
help="group name")
|
||||||
|
pg_rename = group_cmds.add_parser('rename', description='Change group name.')
|
||||||
|
pg_rename.add_argument('gname', metavar='group-name',
|
||||||
|
choices=utils.get_groups(),
|
||||||
|
help="existing group to rename")
|
||||||
|
pg_rename.add_argument('new_name', metavar='new-name',
|
||||||
|
help="new group name")
|
||||||
|
group_cmds.add_parser('rm',
|
||||||
|
description='Remove group(s).').add_argument('to_ungroup',
|
||||||
|
nargs='+',
|
||||||
|
choices=utils.get_groups(),
|
||||||
|
help="group(s) to delete")
|
||||||
|
|
||||||
# superman mode
|
# superman mode
|
||||||
p_super = subparsers.add_parser(
|
p_super = subparsers.add_parser(
|
||||||
|
|
|
@ -6,3 +6,11 @@ def get_config_dir() -> str:
|
||||||
os.path.expanduser('~'), '.config')
|
os.path.expanduser('~'), '.config')
|
||||||
root = os.path.join(parent, "gita")
|
root = os.path.join(parent, "gita")
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_fname(fname: str) -> str:
|
||||||
|
"""
|
||||||
|
Return the file name that stores the repo locations.
|
||||||
|
"""
|
||||||
|
root = get_config_dir()
|
||||||
|
return os.path.join(root, fname)
|
||||||
|
|
108
gita/info.py
108
gita/info.py
|
@ -2,14 +2,19 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from functools import lru_cache
|
||||||
from typing import Tuple, List, Callable, Dict
|
from typing import Tuple, List, Callable, Dict
|
||||||
|
|
||||||
from . import common
|
from . import common
|
||||||
|
|
||||||
|
|
||||||
class Color:
|
class Color(str, Enum):
|
||||||
"""
|
"""
|
||||||
Terminal color
|
Terminal color
|
||||||
"""
|
"""
|
||||||
|
black = '\x1b[30m'
|
||||||
red = '\x1b[31m' # local diverges from remote
|
red = '\x1b[31m' # local diverges from remote
|
||||||
green = '\x1b[32m' # local == remote
|
green = '\x1b[32m' # local == remote
|
||||||
yellow = '\x1b[33m' # local is behind
|
yellow = '\x1b[33m' # local is behind
|
||||||
|
@ -18,6 +23,43 @@ class Color:
|
||||||
cyan = '\x1b[36m'
|
cyan = '\x1b[36m'
|
||||||
white = '\x1b[37m' # no remote branch
|
white = '\x1b[37m' # no remote branch
|
||||||
end = '\x1b[0m'
|
end = '\x1b[0m'
|
||||||
|
b_black = '\x1b[30;1m'
|
||||||
|
b_red = '\x1b[31;1m'
|
||||||
|
b_green = '\x1b[32;1m'
|
||||||
|
b_yellow = '\x1b[33;1m'
|
||||||
|
b_blue = '\x1b[34;1m'
|
||||||
|
b_purple = '\x1b[35;1m'
|
||||||
|
b_cyan = '\x1b[36;1m'
|
||||||
|
b_white = '\x1b[37;1m'
|
||||||
|
|
||||||
|
|
||||||
|
def show_colors(): # pragma: no cover
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
for i, c in enumerate(Color, start=1):
|
||||||
|
if c != Color.end:
|
||||||
|
print(f'{c.value}{c.name:<8} ', end='')
|
||||||
|
if i % 9 == 0:
|
||||||
|
print()
|
||||||
|
print(f'{Color.end}')
|
||||||
|
for situation, c in get_color_encoding().items():
|
||||||
|
print(f'{situation:<12}: {c.value}{c.name:<8}{Color.end} ')
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_color_encoding():
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: add config file
|
||||||
|
return {
|
||||||
|
'no-remote': Color.white,
|
||||||
|
'in-sync': Color.green,
|
||||||
|
'diverged': Color.red,
|
||||||
|
'local-ahead': Color.purple,
|
||||||
|
'remote-ahead': Color.yellow,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_info_funcs() -> List[Callable[[str], str]]:
|
def get_info_funcs() -> List[Callable[[str], str]]:
|
||||||
|
@ -26,35 +68,30 @@ def get_info_funcs() -> List[Callable[[str], str]]:
|
||||||
take the repo path as input and return the corresponding information as str.
|
take the repo path as input and return the corresponding information as str.
|
||||||
See `get_path`, `get_repo_status`, `get_common_commit` for examples.
|
See `get_path`, `get_repo_status`, `get_common_commit` for examples.
|
||||||
"""
|
"""
|
||||||
info_items, to_display = get_info_items()
|
to_display = get_info_items()
|
||||||
return [info_items[k] for k in to_display]
|
# This re-definition is to make unit test mocking to work
|
||||||
|
all_info_items = {
|
||||||
|
'branch': get_repo_status,
|
||||||
|
'commit_msg': get_commit_msg,
|
||||||
|
'path': get_path,
|
||||||
|
}
|
||||||
|
return [all_info_items[k] for k in to_display]
|
||||||
|
|
||||||
|
|
||||||
def get_info_items() -> Tuple[Dict[str, Callable[[str], str]], List[str]]:
|
def get_info_items() -> List[str]:
|
||||||
"""
|
"""
|
||||||
Return the available information items for display in the `gita ll`
|
Return the information items to be displayed in the `gita ll` command.
|
||||||
sub-command, and the ones to be displayed.
|
|
||||||
It loads custom information functions and configuration if they exist.
|
|
||||||
"""
|
"""
|
||||||
# default settings
|
# default settings
|
||||||
info_items = {'branch': get_repo_status,
|
|
||||||
'commit_msg': get_commit_msg,
|
|
||||||
'path': get_path, }
|
|
||||||
display_items = ['branch', 'commit_msg']
|
display_items = ['branch', 'commit_msg']
|
||||||
|
|
||||||
# custom settings
|
# custom settings
|
||||||
root = common.get_config_dir()
|
yml_config = Path(common.get_config_fname('info.yml'))
|
||||||
src_fname = os.path.join(root, 'extra_repo_info.py')
|
if yml_config.is_file():
|
||||||
yml_fname = os.path.join(root, 'info.yml')
|
with open(yml_config, 'r') as stream:
|
||||||
if os.path.isfile(src_fname):
|
|
||||||
sys.path.append(root)
|
|
||||||
from extra_repo_info import extra_info_items
|
|
||||||
info_items.update(extra_info_items)
|
|
||||||
if os.path.isfile(yml_fname):
|
|
||||||
with open(yml_fname, 'r') as stream:
|
|
||||||
display_items = yaml.load(stream, Loader=yaml.FullLoader)
|
display_items = yaml.load(stream, Loader=yaml.FullLoader)
|
||||||
display_items = [x for x in display_items if x in info_items]
|
display_items = [x for x in display_items if x in ALL_INFO_ITEMS]
|
||||||
return info_items, display_items
|
return display_items
|
||||||
|
|
||||||
|
|
||||||
def get_path(path):
|
def get_path(path):
|
||||||
|
@ -113,13 +150,15 @@ def get_commit_msg(path: str) -> str:
|
||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
def get_repo_status(path: str) -> str:
|
def get_repo_status(path: str, no_colors=False) -> str:
|
||||||
head = get_head(path)
|
head = get_head(path)
|
||||||
dirty, staged, untracked, color = _get_repo_status(path)
|
dirty, staged, untracked, color = _get_repo_status(path, no_colors)
|
||||||
return f'{color}{head+" "+dirty+staged+untracked:<10}{Color.end}'
|
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) -> Tuple[str]:
|
def _get_repo_status(path: str, no_colors: bool) -> Tuple[str]:
|
||||||
"""
|
"""
|
||||||
Return the status of one repo
|
Return the status of one repo
|
||||||
"""
|
"""
|
||||||
|
@ -128,19 +167,30 @@ def _get_repo_status(path: str) -> Tuple[str]:
|
||||||
staged = '+' if run_quiet_diff(['--cached']) else ''
|
staged = '+' if run_quiet_diff(['--cached']) else ''
|
||||||
untracked = '_' if has_untracked() else ''
|
untracked = '_' if has_untracked() else ''
|
||||||
|
|
||||||
|
if no_colors:
|
||||||
|
return dirty, staged, untracked, ''
|
||||||
|
|
||||||
|
colors = get_color_encoding()
|
||||||
diff_returncode = run_quiet_diff(['@{u}', '@{0}'])
|
diff_returncode = run_quiet_diff(['@{u}', '@{0}'])
|
||||||
has_no_remote = diff_returncode == 128
|
has_no_remote = diff_returncode == 128
|
||||||
has_no_diff = diff_returncode == 0
|
has_no_diff = diff_returncode == 0
|
||||||
if has_no_remote:
|
if has_no_remote:
|
||||||
color = Color.white
|
color = colors['no-remote']
|
||||||
elif has_no_diff:
|
elif has_no_diff:
|
||||||
color = Color.green
|
color = colors['in-sync']
|
||||||
else:
|
else:
|
||||||
common_commit = get_common_commit()
|
common_commit = get_common_commit()
|
||||||
outdated = run_quiet_diff(['@{u}', common_commit])
|
outdated = run_quiet_diff(['@{u}', common_commit])
|
||||||
if outdated:
|
if outdated:
|
||||||
diverged = run_quiet_diff(['@{0}', common_commit])
|
diverged = run_quiet_diff(['@{0}', common_commit])
|
||||||
color = Color.red if diverged else Color.yellow
|
color = colors['diverged'] if diverged else colors['remote-ahead']
|
||||||
else: # local is ahead of remote
|
else: # local is ahead of remote
|
||||||
color = Color.purple
|
color = colors['local-ahead']
|
||||||
return dirty, staged, untracked, color
|
return dirty, staged, untracked, color
|
||||||
|
|
||||||
|
|
||||||
|
ALL_INFO_ITEMS = {
|
||||||
|
'branch': get_repo_status,
|
||||||
|
'commit_msg': get_commit_msg,
|
||||||
|
'path': get_path,
|
||||||
|
}
|
||||||
|
|
|
@ -2,19 +2,23 @@ import os
|
||||||
import yaml
|
import yaml
|
||||||
import asyncio
|
import asyncio
|
||||||
import platform
|
import platform
|
||||||
from functools import lru_cache
|
from functools import lru_cache, partial
|
||||||
|
from pathlib import Path
|
||||||
from typing import List, Dict, Coroutine, Union
|
from typing import List, Dict, Coroutine, Union
|
||||||
|
|
||||||
from . import info
|
from . import info
|
||||||
from . import common
|
from . import common
|
||||||
|
|
||||||
|
|
||||||
def get_config_fname(fname: str) -> str:
|
@lru_cache()
|
||||||
|
def get_context() -> Union[Path, None]:
|
||||||
"""
|
"""
|
||||||
Return the file name that stores the repo locations.
|
Return the context: either a group name or 'none'
|
||||||
"""
|
"""
|
||||||
root = common.get_config_dir()
|
config_dir = Path(common.get_config_dir())
|
||||||
return os.path.join(root, fname)
|
matches = list(config_dir.glob('*.context'))
|
||||||
|
assert len(matches) < 2, "Cannot have multiple .context file"
|
||||||
|
return matches[0] if matches else None
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
|
@ -22,7 +26,7 @@ def get_repos() -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Return a `dict` of repo name to repo absolute path
|
Return a `dict` of repo name to repo absolute path
|
||||||
"""
|
"""
|
||||||
path_file = get_config_fname('repo_path')
|
path_file = common.get_config_fname('repo_path')
|
||||||
repos = {}
|
repos = {}
|
||||||
# Each line is a repo path and repo name separated by ,
|
# Each line is a repo path and repo name separated by ,
|
||||||
if os.path.isfile(path_file) and os.stat(path_file).st_size > 0:
|
if os.path.isfile(path_file) and os.stat(path_file).st_size > 0:
|
||||||
|
@ -47,7 +51,7 @@ def get_groups() -> Dict[str, List[str]]:
|
||||||
"""
|
"""
|
||||||
Return a `dict` of group name to repo names.
|
Return a `dict` of group name to repo names.
|
||||||
"""
|
"""
|
||||||
fname = get_config_fname('groups.yml')
|
fname = common.get_config_fname('groups.yml')
|
||||||
groups = {}
|
groups = {}
|
||||||
# Each line is a repo path and repo name separated by ,
|
# Each line is a repo path and repo name separated by ,
|
||||||
if os.path.isfile(fname) and os.stat(fname).st_size > 0:
|
if os.path.isfile(fname) and os.stat(fname).st_size > 0:
|
||||||
|
@ -102,7 +106,7 @@ def write_to_repo_file(repos: Dict[str, str], mode: str):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
data = ''.join(f'{path},{name}\n' for name, path in repos.items())
|
data = ''.join(f'{path},{name}\n' for name, path in repos.items())
|
||||||
fname = get_config_fname('repo_path')
|
fname = common.get_config_fname('repo_path')
|
||||||
os.makedirs(os.path.dirname(fname), exist_ok=True)
|
os.makedirs(os.path.dirname(fname), exist_ok=True)
|
||||||
with open(fname, mode) as f:
|
with open(fname, mode) as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
@ -112,10 +116,13 @@ def write_to_groups_file(groups: Dict[str, List[str]], mode: str):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
"""
|
"""
|
||||||
fname = get_config_fname('groups.yml')
|
fname = common.get_config_fname('groups.yml')
|
||||||
os.makedirs(os.path.dirname(fname), exist_ok=True)
|
os.makedirs(os.path.dirname(fname), exist_ok=True)
|
||||||
with open(fname, mode) as f:
|
if not groups: # all groups are deleted
|
||||||
yaml.dump(groups, f, default_flow_style=None)
|
open(fname, 'w').close()
|
||||||
|
else:
|
||||||
|
with open(fname, mode) as f:
|
||||||
|
yaml.dump(groups, f, default_flow_style=None)
|
||||||
|
|
||||||
|
|
||||||
def add_repos(repos: Dict[str, str], new_paths: List[str]):
|
def add_repos(repos: Dict[str, str], new_paths: List[str]):
|
||||||
|
@ -182,17 +189,23 @@ def exec_async_tasks(tasks: List[Coroutine]) -> List[Union[None, str]]:
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def describe(repos: Dict[str, str]) -> str:
|
def describe(repos: Dict[str, str], no_colors: bool=False) -> str:
|
||||||
"""
|
"""
|
||||||
Return the status of all repos
|
Return the status of all repos
|
||||||
"""
|
"""
|
||||||
if repos:
|
if repos:
|
||||||
name_width = max(len(n) for n in repos) + 1
|
name_width = max(len(n) for n in repos) + 1
|
||||||
funcs = info.get_info_funcs()
|
funcs = info.get_info_funcs()
|
||||||
|
|
||||||
|
get_repo_status = info.get_repo_status
|
||||||
|
if get_repo_status in funcs and no_colors:
|
||||||
|
idx = funcs.index(get_repo_status)
|
||||||
|
funcs[idx] = partial(get_repo_status, no_colors=True)
|
||||||
|
|
||||||
for name in sorted(repos):
|
for name in sorted(repos):
|
||||||
path = repos[name]
|
path = repos[name]
|
||||||
display_items = ' '.join(f(path) for f in funcs)
|
info_items = ' '.join(f(path) for f in funcs)
|
||||||
yield f'{name:<{name_width}}{display_items}'
|
yield f'{name:<{name_width}}{info_items}'
|
||||||
|
|
||||||
|
|
||||||
def get_cmds_from_files() -> Dict[str, Dict[str, str]]:
|
def get_cmds_from_files() -> Dict[str, Dict[str, str]]:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
pytest>=4.4.0
|
pytest>=6.1.2
|
||||||
pytest-cov>=2.6.1
|
pytest-cov>=2.6.1
|
||||||
pytest-xdist>=1.26.0
|
pytest-xdist>=2.1.0
|
||||||
setuptools>=40.6.3
|
setuptools>=40.6.3
|
||||||
twine>=1.12.1
|
twine>=1.12.1
|
||||||
pyyaml>=5.1
|
pyyaml>=5.1
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -7,9 +7,9 @@ with open('README.md', encoding='utf-8') as f:
|
||||||
setup(
|
setup(
|
||||||
name='gita',
|
name='gita',
|
||||||
packages=['gita'],
|
packages=['gita'],
|
||||||
version='0.10.10',
|
version='0.11.9',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
description='Manage multiple git repos',
|
description='Manage multiple git repos with sanity',
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
url='https://github.com/nosarthur/gita',
|
url='https://github.com/nosarthur/gita',
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch, mock_open
|
||||||
|
from pathlib import Path
|
||||||
import argparse
|
import argparse
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
from gita import __main__
|
from gita import __main__
|
||||||
from gita import utils
|
from gita import utils, info
|
||||||
from conftest import (
|
from conftest import (
|
||||||
PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME,
|
PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME,
|
||||||
async_mock
|
async_mock, TEST_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestLsLl:
|
class TestLsLl:
|
||||||
@patch('gita.utils.get_config_fname')
|
@patch('gita.common.get_config_fname')
|
||||||
def testLl(self, mock_path_fname, capfd, tmp_path):
|
def testLl(self, mock_path_fname, capfd, tmp_path):
|
||||||
""" functional test """
|
"""
|
||||||
|
functional test
|
||||||
|
"""
|
||||||
# avoid modifying the local configuration
|
# avoid modifying the local configuration
|
||||||
mock_path_fname.return_value = tmp_path / 'path_config.txt'
|
def side_effect(input):
|
||||||
|
return tmp_path / f'{input}.txt'
|
||||||
|
#mock_path_fname.return_value = tmp_path / 'path_config.txt'
|
||||||
|
mock_path_fname.side_effect = side_effect
|
||||||
__main__.main(['add', '.'])
|
__main__.main(['add', '.'])
|
||||||
out, err = capfd.readouterr()
|
out, err = capfd.readouterr()
|
||||||
assert err == ''
|
assert err == ''
|
||||||
|
@ -34,6 +40,14 @@ class TestLsLl:
|
||||||
out, err = capfd.readouterr()
|
out, err = capfd.readouterr()
|
||||||
assert err == ''
|
assert err == ''
|
||||||
assert 'gita' in out
|
assert 'gita' in out
|
||||||
|
assert info.Color.end in out
|
||||||
|
|
||||||
|
# no color on branch name
|
||||||
|
__main__.main(['ll', '-n'])
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert err == ''
|
||||||
|
assert 'gita' in out
|
||||||
|
assert info.Color.end not in out
|
||||||
|
|
||||||
__main__.main(['ls', 'gita'])
|
__main__.main(['ls', 'gita'])
|
||||||
out, err = capfd.readouterr()
|
out, err = capfd.readouterr()
|
||||||
|
@ -65,10 +79,14 @@ class TestLsLl:
|
||||||
@patch('gita.info.get_head', return_value="master")
|
@patch('gita.info.get_head', return_value="master")
|
||||||
@patch('gita.info._get_repo_status', return_value=("d", "s", "u", "c"))
|
@patch('gita.info._get_repo_status', return_value=("d", "s", "u", "c"))
|
||||||
@patch('gita.info.get_commit_msg', return_value="msg")
|
@patch('gita.info.get_commit_msg', return_value="msg")
|
||||||
@patch('gita.utils.get_config_fname')
|
@patch('gita.common.get_config_fname')
|
||||||
def testWithPathFiles(self, mock_path_fname, _0, _1, _2, _3, path_fname,
|
def testWithPathFiles(self, mock_path_fname, _0, _1, _2, _3, path_fname,
|
||||||
expected, capfd):
|
expected, capfd):
|
||||||
mock_path_fname.return_value = path_fname
|
def side_effect(input):
|
||||||
|
if input == 'repo_path':
|
||||||
|
return path_fname
|
||||||
|
return f'/{input}'
|
||||||
|
mock_path_fname.side_effect = side_effect
|
||||||
utils.get_repos.cache_clear()
|
utils.get_repos.cache_clear()
|
||||||
__main__.main(['ll'])
|
__main__.main(['ll'])
|
||||||
out, err = capfd.readouterr()
|
out, err = capfd.readouterr()
|
||||||
|
@ -78,7 +96,7 @@ class TestLsLl:
|
||||||
|
|
||||||
|
|
||||||
@patch('os.path.isfile', return_value=True)
|
@patch('os.path.isfile', return_value=True)
|
||||||
@patch('gita.utils.get_config_fname', return_value='some path')
|
@patch('gita.common.get_config_fname', return_value='some path')
|
||||||
@patch('gita.utils.get_repos', return_value={'repo1': '/a/', 'repo2': '/b/'})
|
@patch('gita.utils.get_repos', return_value={'repo1': '/a/', 'repo2': '/b/'})
|
||||||
@patch('gita.utils.write_to_repo_file')
|
@patch('gita.utils.write_to_repo_file')
|
||||||
def test_rm(mock_write, *_):
|
def test_rm(mock_write, *_):
|
||||||
|
@ -131,34 +149,133 @@ def test_superman(mock_run, _, input):
|
||||||
mock_run.assert_called_once_with(expected_cmds, cwd='path7')
|
mock_run.assert_called_once_with(expected_cmds, cwd='path7')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('input, expected', [
|
class TestContext:
|
||||||
('a', {'xx': ['b'], 'yy': ['c', 'd']}),
|
|
||||||
("c", {'xx': ['a', 'b'], 'yy': ['a', 'd']}),
|
@patch('gita.utils.get_context', return_value=None)
|
||||||
("a b", {'yy': ['c', 'd']}),
|
def testDisplayNoContext(self, _, capfd):
|
||||||
])
|
__main__.main(['context'])
|
||||||
@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''})
|
out, err = capfd.readouterr()
|
||||||
@patch('gita.utils.get_config_fname', return_value=GROUP_FNAME)
|
assert err == ''
|
||||||
@patch('gita.utils.write_to_groups_file')
|
assert 'Context is not set\n' == out
|
||||||
def test_ungroup(mock_write, _, __, input, expected):
|
|
||||||
utils.get_groups.cache_clear()
|
@patch('gita.utils.get_context', return_value=Path('gname.context'))
|
||||||
args = ['ungroup'] + shlex.split(input)
|
@patch('gita.utils.get_groups', return_value={'gname': ['a', 'b']})
|
||||||
__main__.main(args)
|
def testDisplayContext(self, _, __, capfd):
|
||||||
mock_write.assert_called_once_with(expected, 'w')
|
__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):
|
||||||
|
__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, *_):
|
||||||
|
ctx = TEST_DIR / 'lala.context'
|
||||||
|
assert not ctx.is_file()
|
||||||
|
__main__.main(['context', 'lala'])
|
||||||
|
assert ctx.is_file()
|
||||||
|
ctx.unlink()
|
||||||
|
|
||||||
|
@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, *_):
|
||||||
|
__main__.main(['context', 'kaka'])
|
||||||
|
mock_ctx.return_value.rename.assert_called()
|
||||||
|
|
||||||
|
|
||||||
@patch('gita.utils.get_config_fname', return_value=GROUP_FNAME)
|
class TestGroupCmd:
|
||||||
def test_group_display(_, capfd):
|
|
||||||
args = argparse.Namespace()
|
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
|
||||||
args.to_group = None
|
def testLs(self, _, capfd):
|
||||||
utils.get_groups.cache_clear()
|
args = argparse.Namespace()
|
||||||
__main__.f_group(args)
|
args.to_group = None
|
||||||
out, err = capfd.readouterr()
|
args.group_cmd = 'ls'
|
||||||
assert err == ''
|
utils.get_groups.cache_clear()
|
||||||
assert 'xx: a b\nyy: a c d\n' == out
|
__main__.f_group(args)
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert err == ''
|
||||||
|
assert 'xx yy\n' == out
|
||||||
|
|
||||||
|
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
|
||||||
|
def testLl(self, _, capfd):
|
||||||
|
args = argparse.Namespace()
|
||||||
|
args.to_group = None
|
||||||
|
args.group_cmd = None
|
||||||
|
utils.get_groups.cache_clear()
|
||||||
|
__main__.f_group(args)
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert err == ''
|
||||||
|
assert 'xx: a b\nyy: a c d\n' == out
|
||||||
|
|
||||||
|
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
|
||||||
|
@patch('gita.utils.write_to_groups_file')
|
||||||
|
def testRename(self, mock_write, _):
|
||||||
|
args = argparse.Namespace()
|
||||||
|
args.gname = 'xx'
|
||||||
|
args.new_name = 'zz'
|
||||||
|
args.group_cmd = 'rename'
|
||||||
|
utils.get_groups.cache_clear()
|
||||||
|
__main__.f_group(args)
|
||||||
|
expected = {'yy': ['a', 'c', 'd'], 'zz': ['a', 'b']}
|
||||||
|
mock_write.assert_called_once_with(expected, 'w')
|
||||||
|
|
||||||
|
@patch('gita.common.get_config_fname', return_value=GROUP_FNAME)
|
||||||
|
def testRenameError(self, *_):
|
||||||
|
args = argparse.Namespace()
|
||||||
|
args.gname = 'xx'
|
||||||
|
args.new_name = 'yy'
|
||||||
|
args.group_cmd = 'rename'
|
||||||
|
utils.get_groups.cache_clear()
|
||||||
|
with pytest.raises(SystemExit, match='yy already exists.'):
|
||||||
|
__main__.f_group(args)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('input, expected', [
|
||||||
|
('xx', {'yy': ['a', 'c', 'd']}),
|
||||||
|
("xx yy", {}),
|
||||||
|
])
|
||||||
|
@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):
|
||||||
|
utils.get_groups.cache_clear()
|
||||||
|
args = ['group', 'rm'] + shlex.split(input)
|
||||||
|
__main__.main(args)
|
||||||
|
mock_write.assert_called_once_with(expected, 'w')
|
||||||
|
|
||||||
|
@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, *_):
|
||||||
|
args = argparse.Namespace()
|
||||||
|
args.to_group = ['a', 'c']
|
||||||
|
args.group_cmd = 'add'
|
||||||
|
args.gname = 'zz'
|
||||||
|
utils.get_groups.cache_clear()
|
||||||
|
__main__.f_group(args)
|
||||||
|
mock_write.assert_called_once_with({'zz': ['a', 'c']}, 'a+')
|
||||||
|
|
||||||
|
@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, *_):
|
||||||
|
args = argparse.Namespace()
|
||||||
|
args.to_group = ['a', 'c']
|
||||||
|
args.group_cmd = 'add'
|
||||||
|
args.gname = 'xx'
|
||||||
|
utils.get_groups.cache_clear()
|
||||||
|
__main__.f_group(args)
|
||||||
|
mock_write.assert_called_once_with(
|
||||||
|
{'xx': ['a', 'b', 'c'], 'yy': ['a', 'c', 'd']}, 'w')
|
||||||
|
|
||||||
|
|
||||||
@patch('gita.utils.is_git', return_value=True)
|
@patch('gita.utils.is_git', return_value=True)
|
||||||
@patch('gita.utils.get_config_fname', return_value=PATH_FNAME)
|
@patch('gita.common.get_config_fname', return_value=PATH_FNAME)
|
||||||
@patch('gita.utils.rename_repo')
|
@patch('gita.utils.rename_repo')
|
||||||
def test_rename(mock_rename, _, __):
|
def test_rename(mock_rename, _, __):
|
||||||
utils.get_repos.cache_clear()
|
utils.get_repos.cache_clear()
|
||||||
|
@ -170,9 +287,39 @@ def test_rename(mock_rename, _, __):
|
||||||
'repo1', 'abc')
|
'repo1', 'abc')
|
||||||
|
|
||||||
|
|
||||||
@patch('os.path.isfile', return_value=False)
|
class TestInfo:
|
||||||
def test_info(mock_isfile, capfd):
|
|
||||||
__main__.f_info(None)
|
@patch('gita.common.get_config_fname', return_value='')
|
||||||
out, err = capfd.readouterr()
|
def testLl(self, _, capfd):
|
||||||
assert 'In use: branch,commit_msg\nUnused: path\n' == out
|
args = argparse.Namespace()
|
||||||
assert err == ''
|
args.info_cmd = None
|
||||||
|
__main__.f_info(args)
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert 'In use: branch,commit_msg\nUnused: path\n' == out
|
||||||
|
assert err == ''
|
||||||
|
|
||||||
|
@patch('gita.common.get_config_fname', return_value='')
|
||||||
|
@patch('yaml.dump')
|
||||||
|
def testAdd(self, mock_dump, _):
|
||||||
|
args = argparse.Namespace()
|
||||||
|
args.info_cmd = 'add'
|
||||||
|
args.info_item = 'path'
|
||||||
|
with patch('builtins.open', mock_open(), create=True):
|
||||||
|
__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}
|
||||||
|
|
||||||
|
@patch('gita.common.get_config_fname', return_value='')
|
||||||
|
@patch('yaml.dump')
|
||||||
|
def testRm(self, mock_dump, _):
|
||||||
|
args = argparse.Namespace()
|
||||||
|
args.info_cmd = 'rm'
|
||||||
|
args.info_item = 'commit_msg'
|
||||||
|
with patch('builtins.open', mock_open(), create=True):
|
||||||
|
__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}
|
||||||
|
|
|
@ -4,17 +4,14 @@ from unittest.mock import patch, mock_open
|
||||||
|
|
||||||
from gita import utils, info
|
from gita import utils, info
|
||||||
from conftest import (
|
from conftest import (
|
||||||
PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME,
|
PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME, TEST_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('test_input, diff_return, expected', [
|
@pytest.mark.parametrize('test_input, diff_return, expected', [
|
||||||
({
|
([{'abc': '/root/repo/'}, False], True, 'abc \x1b[31mrepo *+_ \x1b[0m msg'),
|
||||||
'abc': '/root/repo/'
|
([{'abc': '/root/repo/'}, True], True, 'abc repo *+_ msg'),
|
||||||
}, True, 'abc \x1b[31mrepo *+_ \x1b[0m msg'),
|
([{'repo': '/root/repo2/'}, False], False, 'repo \x1b[32mrepo _ \x1b[0m msg'),
|
||||||
({
|
|
||||||
'repo': '/root/repo2/'
|
|
||||||
}, False, 'repo \x1b[32mrepo _ \x1b[0m msg'),
|
|
||||||
])
|
])
|
||||||
def test_describe(test_input, diff_return, expected, monkeypatch):
|
def test_describe(test_input, diff_return, expected, monkeypatch):
|
||||||
monkeypatch.setattr(info, 'get_head', lambda x: 'repo')
|
monkeypatch.setattr(info, 'get_head', lambda x: 'repo')
|
||||||
|
@ -23,8 +20,8 @@ def test_describe(test_input, diff_return, expected, monkeypatch):
|
||||||
monkeypatch.setattr(info, 'has_untracked', lambda: True)
|
monkeypatch.setattr(info, 'has_untracked', lambda: True)
|
||||||
monkeypatch.setattr('os.chdir', lambda x: None)
|
monkeypatch.setattr('os.chdir', lambda x: None)
|
||||||
print('expected: ', repr(expected))
|
print('expected: ', repr(expected))
|
||||||
print('got: ', repr(next(utils.describe(test_input))))
|
print('got: ', repr(next(utils.describe(*test_input))))
|
||||||
assert expected == next(utils.describe(test_input))
|
assert expected == next(utils.describe(*test_input))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('path_fname, expected', [
|
@pytest.mark.parametrize('path_fname, expected', [
|
||||||
|
@ -41,17 +38,28 @@ def test_describe(test_input, diff_return, expected, monkeypatch):
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@patch('gita.utils.is_git', return_value=True)
|
@patch('gita.utils.is_git', return_value=True)
|
||||||
@patch('gita.utils.get_config_fname')
|
@patch('gita.common.get_config_fname')
|
||||||
def test_get_repos(mock_path_fname, _, path_fname, expected):
|
def test_get_repos(mock_path_fname, _, path_fname, expected):
|
||||||
mock_path_fname.return_value = path_fname
|
mock_path_fname.return_value = path_fname
|
||||||
utils.get_repos.cache_clear()
|
utils.get_repos.cache_clear()
|
||||||
assert utils.get_repos() == expected
|
assert utils.get_repos() == expected
|
||||||
|
|
||||||
|
|
||||||
|
@patch('gita.common.get_config_dir')
|
||||||
|
def test_get_context(mock_config_dir):
|
||||||
|
mock_config_dir.return_value = TEST_DIR
|
||||||
|
utils.get_context.cache_clear()
|
||||||
|
assert utils.get_context() == TEST_DIR / 'xx.context'
|
||||||
|
|
||||||
|
mock_config_dir.return_value = '/'
|
||||||
|
utils.get_context.cache_clear()
|
||||||
|
assert utils.get_context() == None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('group_fname, expected', [
|
@pytest.mark.parametrize('group_fname, expected', [
|
||||||
(GROUP_FNAME, {'xx': ['a', 'b'], 'yy': ['a', 'c', 'd']}),
|
(GROUP_FNAME, {'xx': ['a', 'b'], 'yy': ['a', 'c', 'd']}),
|
||||||
])
|
])
|
||||||
@patch('gita.utils.get_config_fname')
|
@patch('gita.common.get_config_fname')
|
||||||
def test_get_groups(mock_group_fname, group_fname, expected):
|
def test_get_groups(mock_group_fname, group_fname, expected):
|
||||||
mock_group_fname.return_value = group_fname
|
mock_group_fname.return_value = group_fname
|
||||||
utils.get_groups.cache_clear()
|
utils.get_groups.cache_clear()
|
||||||
|
|
0
tests/xx.context
Normal file
0
tests/xx.context
Normal file
Loading…
Add table
Add a link
Reference in a new issue