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