diff options
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | doc/README_CN.md | 1 | ||||
-rw-r--r-- | gita/__main__.py | 101 | ||||
-rw-r--r-- | gita/utils.py | 39 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | tests/test_utils.py | 13 |
6 files changed, 91 insertions, 72 deletions
@@ -361,6 +361,13 @@ Thus the installed git version may matter. I have git `1.8.3.1`, `2.17.2`, and `2.20.1` on my machines, and their results agree. +## tips + +effect | shell command +---|--- +enter `<repo>` directory|`` cd `gita ls <repo>` `` +delete repos in `<group>` | `gita group ll <group> \| xargs gita rm` + ## Contributing To contribute, you can diff --git a/doc/README_CN.md b/doc/README_CN.md index 9666824..592ad6d 100644 --- a/doc/README_CN.md +++ b/doc/README_CN.md @@ -55,7 +55,6 @@ - `gita add -a <repo-parent-path(s)>`: 递归添加路径下的所有库并自动产生层级分组,见 [customization section](#custom) - `gita add -b <bare-repo-path(s)>`: 添加bare库 -- `gita add -m <main-repo-path(s)>`: 添加main库, main库的定义见 [customization section](#custom) - `gita add -r <repo-parent-path(s)>`: 递归添加路径下的所有库 - `gita clone <config-file>`: 克隆`<config-file>` (由`gita freeze`生成)里的库 diff --git a/gita/__main__.py b/gita/__main__.py index d38e78d..d489f55 100644 --- a/gita/__main__.py +++ b/gita/__main__.py @@ -186,9 +186,9 @@ def f_ll(args: argparse.Namespace): for line in utils.describe(repos, no_colors=args.no_colors): print(' ', line) else: - for g, g_repos in utils.get_groups().items(): + for g, prop in utils.get_groups().items(): print(f'{g}:') - g_repos = {k: repos[k] for k in g_repos if k in repos} + g_repos = {k: repos[k] for k in prop['repos'] if k in repos} for line in utils.describe(g_repos, no_colors=args.no_colors): print(' ', line) else: @@ -286,26 +286,16 @@ def f_rm(args: argparse.Namespace): 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 group_updated = False + groups = utils.get_groups() for repo in args.repo: del repos[repo] - groups = utils.get_groups() - group_updated = group_updated or utils.delete_repo_from_groups(repo, groups) + up = utils.delete_repo_from_groups(repo, groups) + group_updated = group_updated or up if group_updated: utils.write_to_groups_file(groups, 'w') - # If cwd is relative to any main repo, write to local config - cwd = os.getcwd() - # TODO: delete main path mechanism - for p in main_paths: - if utils.get_relative_path(cwd, p) is not None: - utils.write_to_repo_file(repos, 'w', p) - break - else: # global config - utils.write_to_repo_file(repos, 'w') + utils.write_to_repo_file(repos, 'w') def f_git_cmd(args: argparse.Namespace): @@ -313,20 +303,11 @@ def f_git_cmd(args: argparse.Namespace): Delegate git command/alias defined in `args.cmd`. Asynchronous execution is disabled for commands in the `args.async_blacklist`. """ - repos = utils.get_repos() - 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) - chosen = {} - for k in args.repo: - if k in repos: - chosen[k] = repos[k] - if k in groups: - for r in groups[k]['repos']: - chosen[r] = repos[r] - repos = chosen + if '_parsed_repos' in args: + repos = args._parsed_repos + else: + repos, _ = utils.parse_repos_and_rest(args.repo) + per_repo_cmds = [] for prop in repos.values(): cmds = args.cmd.copy() @@ -361,29 +342,12 @@ def f_shell(args): Delegate shell command defined in `args.man`, which may or may not contain repo names. """ - names = [] - repos = utils.get_repos() - groups = utils.get_groups() - ctx = utils.get_context() - for i, word in enumerate(args.man): - if word in repos or word in groups: - names.append(word) - else: - break - args.repo = names - # TODO: redundant with f_git_cmd - if not args.repo and ctx: - args.repo = [ctx.stem] - if args.repo: # with user specified repo(s) or group(s) - chosen = {} - for k in args.repo: - if k in repos: - chosen[k] = repos[k] - if k in groups: - for r in groups[k]: - chosen[r] = repos[r] - repos = chosen - cmds = ' '.join(args.man[i:]) # join the shell command into a single string + repos, cmds = utils.parse_repos_and_rest(args.man) + if not cmds: + print('Missing commands') + sys.exit(2) + + cmds = ' '.join(cmds) # 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=prop['path'], shell=True, @@ -397,16 +361,13 @@ def f_super(args): Delegate git command/alias defined in `args.man`, which may or may not contain repo names. """ - names = [] - repos = utils.get_repos() - groups = utils.get_groups() - for i, word in enumerate(args.man): - if word in repos or word in groups: - names.append(word) - else: - break - args.cmd = ['git'] + args.man[i:] - args.repo = names + repos, cmds = utils.parse_repos_and_rest(args.man) + if not cmds: + print('Missing commands') + sys.exit(2) + + args.cmd = ['git'] + cmds + args._parsed_repos = repos args.shell = False f_git_cmd(args) @@ -625,15 +586,15 @@ def main(argv=None): p_super = subparsers.add_parser( 'super', help='run any git command/alias', - description='Superman mode: delegate any git command/alias in specified or ' + description='Superman mode: delegate any git command/alias in specified repo(s), group(s), or ' 'all repo(s).\n' 'Examples:\n \t gita super myrepo1 commit -am "fix a bug"\n' '\t gita super repo1 repo2 repo3 checkout new-feature') p_super.add_argument( 'man', nargs=argparse.REMAINDER, - help="execute arbitrary git command/alias for specified or all repos\n" - "Example: gita super myrepo1 diff --name-only --staged " + help="execute arbitrary git command/alias for specified repo(s), group(s), or all repos.\n" + "Example: gita super myrepo1 diff --name-only --staged\n" "Another: gita super checkout master ") p_super.set_defaults(func=f_super) @@ -641,15 +602,15 @@ def main(argv=None): p_shell = subparsers.add_parser( 'shell', help='run any shell command', - description='shell mode: delegate any shell command in specified or ' + description='shell mode: delegate any shell command in specified repo(s), group(s), or ' 'all repo(s).\n' - 'Examples:\n \t gita shell pwd\n' + 'Examples:\n \t gita shell pwd; \n' '\t gita shell repo1 repo2 repo3 touch xx') p_shell.add_argument( 'man', nargs=argparse.REMAINDER, - help="execute arbitrary shell command for specified or all repos " - "Example: gita shell myrepo1 ls" + help="execute arbitrary shell command for specified repo(s), group(s), or all repos.\n" + "Example: gita shell myrepo1 ls\n" "Another: gita shell git checkout master ") p_shell.set_defaults(func=f_shell) diff --git a/gita/utils.py b/gita/utils.py index 7a1020c..2431fde 100644 --- a/gita/utils.py +++ b/gita/utils.py @@ -256,6 +256,10 @@ def write_to_groups_file(groups: Dict[str, Dict], mode: str): if not groups: # all groups are deleted open(fname, 'w').close() else: + # delete the group if there are no repos + for name in list(groups): + if not groups[name]['repos']: + del groups[name] with open(fname, mode, newline='') as f: data = [ (group, ' '.join(prop['repos']), prop['path']) @@ -475,3 +479,38 @@ def get_cmds_from_files() -> Dict[str, Dict[str, str]]: # custom commands shadow default ones cmds.update(custom_cmds) return cmds + + +def parse_repos_and_rest(input: List[str] + ) -> Tuple[Dict[str, Dict[str, str]], List[str]]: + """ + Parse gita input arguments + + @return: repos and the rest (e.g., gita shell and super commands) + """ + i = None + names = [] + repos = get_repos() + groups = get_groups() + ctx = get_context() + for i, word in enumerate(input): + if word in repos or word in groups: + names.append(word) + else: + break + else: # all input is repos and groups, shift the index once more + if i is not None: + i += 1 + if not names and ctx: + names = [ctx.stem] + if names: + chosen = {} + for k in names: + if k in repos: + chosen[k] = repos[k] + if k in groups: + for r in groups[k]['repos']: + chosen[r] = repos[r] + # if not set here, all repos are chosen + repos = chosen + return repos, input[i:] @@ -7,7 +7,7 @@ with open('README.md', encoding='utf-8') as f: setup( name='gita', packages=['gita'], - version='0.15.7', + version='0.15.7.3', license='MIT', description='Manage multiple git repos with sanity', long_description=long_description, diff --git a/tests/test_utils.py b/tests/test_utils.py index 3ff0cb1..65096e9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,6 +10,19 @@ from conftest import ( ) +@pytest.mark.parametrize('input, expected', [ + ([], ({'repo1': {'path': '/a/bcd/repo1', 'type': '', 'flags': []}, 'xxx': {'path': '/a/b/c/repo3', 'type': '', 'flags': []}, 'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': []}}, [])), + (['st'], ({'repo1': {'path': '/a/bcd/repo1', 'type': '', 'flags': []}, 'xxx': {'path': '/a/b/c/repo3', 'type': '', 'flags': []}, 'repo2': {'path': '/e/fgh/repo2', 'type': '', 'flags': []}}, ['st'])), + (['repo1', 'st'], ({'repo1': {'flags': [], 'path': '/a/bcd/repo1', 'type': ''}}, ['st'])), + (['repo1'], ({'repo1': {'flags': [], 'path': '/a/bcd/repo1', 'type': ''}}, [])), + ]) +@patch('gita.utils.is_git', return_value=True) +@patch('gita.common.get_config_fname', return_value=PATH_FNAME) +def test_parse_repos_and_rest(mock_path_fname, _, input, expected): + got = utils.parse_repos_and_rest(input) + assert got == expected + + @pytest.mark.parametrize('repo_path, paths, expected', [ ('/a/b/c/repo', ['/a/b'], (('b', 'c'), '/a')), ]) |