summaryrefslogtreecommitdiffstats
path: root/gita/__main__.py
diff options
context:
space:
mode:
Diffstat (limited to 'gita/__main__.py')
-rw-r--r--gita/__main__.py289
1 files changed, 289 insertions, 0 deletions
diff --git a/gita/__main__.py b/gita/__main__.py
new file mode 100644
index 0000000..ba0d270
--- /dev/null
+++ b/gita/__main__.py
@@ -0,0 +1,289 @@
+'''
+Gita manages multiple git repos. It has two functionalities
+
+ 1. display the status of multiple repos side by side
+ 2. delegate git commands/aliases from any working directory
+
+Examples:
+ gita ls
+ gita fetch
+ gita stat myrepo2
+ gita super myrepo1 commit -am 'add some cool feature'
+
+For bash auto completion, download and source
+https://github.com/nosarthur/gita/blob/master/.gita-completion.bash
+'''
+
+import os
+import argparse
+import subprocess
+import pkg_resources
+
+from . import utils, info
+
+
+def f_add(args: argparse.Namespace):
+ repos = utils.get_repos()
+ utils.add_repos(repos, args.paths)
+
+
+def f_rename(args: argparse.Namespace):
+ repos = utils.get_repos()
+ utils.rename_repo(repos, args.repo[0], args.new_name)
+
+
+def f_info(_):
+ all_items, to_display = info.get_info_items()
+ print('In use:', ','.join(to_display))
+ unused = set(all_items) - set(to_display)
+ if unused:
+ print('Unused:', ' '.join(unused))
+
+
+def f_ll(args: argparse.Namespace):
+ """
+ Display details of all repos
+ """
+ repos = utils.get_repos()
+ if args.group: # only display repos in this group
+ group_repos = utils.get_groups()[args.group]
+ repos = {k: repos[k] for k in group_repos if k in repos}
+ for line in utils.describe(repos):
+ print(line)
+
+
+def f_ls(args: argparse.Namespace):
+ repos = utils.get_repos()
+ if args.repo: # one repo, show its path
+ print(repos[args.repo])
+ else: # show names of all repos
+ print(' '.join(repos))
+
+
+def f_group(args: argparse.Namespace):
+ repos = utils.get_repos()
+ groups = utils.get_groups()
+ if args.to_group:
+ gname = input('group name? ')
+ if gname in groups:
+ gname_repos = set(groups[gname])
+ gname_repos.update(args.to_group)
+ groups[gname] = sorted(gname_repos)
+ utils.write_to_groups_file(groups, 'w')
+ else:
+ 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):
+ groups = utils.get_groups()
+ to_ungroup = set(args.to_ungroup)
+ to_del = []
+ for name, repos in groups.items():
+ remaining = set(repos) - to_ungroup
+ if remaining:
+ groups[name] = list(sorted(remaining))
+ else:
+ to_del.append(name)
+ for name in to_del:
+ del groups[name]
+ utils.write_to_groups_file(groups, 'w')
+
+
+def f_rm(args: argparse.Namespace):
+ """
+ Unregister repo(s) from gita
+ """
+ path_file = utils.get_config_fname('repo_path')
+ if os.path.isfile(path_file):
+ repos = utils.get_repos()
+ for repo in args.repo:
+ del repos[repo]
+ utils.write_to_repo_file(repos, 'w')
+
+
+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()
+ 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 = ['git'] + args.cmd
+ if len(repos) == 1 or cmds[1] in args.async_blacklist:
+ for path in repos.values():
+ print(path)
+ subprocess.run(cmds, cwd=path)
+ 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())
+ for path in errors:
+ if path:
+ print(path)
+ subprocess.run(cmds, cwd=path)
+
+
+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 = args.man[i:]
+ args.repo = names
+ f_git_cmd(args)
+
+
+def main(argv=None):
+ p = argparse.ArgumentParser(prog='gita',
+ formatter_class=argparse.RawTextHelpFormatter,
+ description=__doc__)
+ subparsers = p.add_subparsers(title='sub-commands',
+ help='additional help with sub-command -h')
+
+ version = pkg_resources.require('gita')[0].version
+ p.add_argument('-v',
+ '--version',
+ action='version',
+ version=f'%(prog)s {version}')
+
+ # bookkeeping sub-commands
+ p_add = subparsers.add_parser('add', help='add repo(s)')
+ p_add.add_argument('paths', nargs='+', help="add repo(s)")
+ p_add.set_defaults(func=f_add)
+
+ p_rm = subparsers.add_parser('rm', help='remove repo(s)')
+ p_rm.add_argument('repo',
+ nargs='+',
+ choices=utils.get_repos(),
+ help="remove the chosen repo(s)")
+ p_rm.set_defaults(func=f_rm)
+
+ p_rename = subparsers.add_parser('rename', help='rename a repo')
+ p_rename.add_argument(
+ 'repo',
+ nargs=1,
+ choices=utils.get_repos(),
+ help="rename the chosen repo")
+ p_rename.add_argument(
+ 'new_name',
+ help="new name")
+ p_rename.set_defaults(func=f_rename)
+
+ p_info = subparsers.add_parser('info', help='show information items of the ll sub-command')
+ p_info.set_defaults(func=f_info)
+
+ ll_doc = f''' status symbols:
+ +: staged changes
+ *: unstaged changes
+ _: untracked files/folders
+
+ branch colors:
+ {info.Color.white}white{info.Color.end}: local has no remote
+ {info.Color.green}green{info.Color.end}: local is the same as remote
+ {info.Color.red}red{info.Color.end}: local has diverged from remote
+ {info.Color.purple}purple{info.Color.end}: local is ahead of remote (good for push)
+ {info.Color.yellow}yellow{info.Color.end}: local is behind remote (good for merge)'''
+ p_ll = subparsers.add_parser('ll',
+ help='display summary of all repos',
+ formatter_class=argparse.RawTextHelpFormatter,
+ description=ll_doc)
+ p_ll.add_argument('group',
+ nargs='?',
+ choices=utils.get_groups(),
+ help="show repos in the chosen group")
+ p_ll.set_defaults(func=f_ll)
+
+ p_ls = subparsers.add_parser(
+ 'ls', help='display names of all repos, or path of a chosen repo')
+ p_ls.add_argument('repo',
+ nargs='?',
+ choices=utils.get_repos(),
+ help="show path of the chosen repo")
+ p_ls.set_defaults(func=f_ls)
+
+ p_group = subparsers.add_parser(
+ 'group', help='group repos or display names of all groups if no repo is provided')
+ 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_ungroup = subparsers.add_parser(
+ 'ungroup', help='remove group information for repos',
+ description="Remove group information on repos")
+ p_ungroup.add_argument('to_ungroup',
+ nargs='+',
+ choices=utils.get_repos(),
+ help="repo(s) to be ungrouped")
+ p_ungroup.set_defaults(func=f_ungroup)
+
+ # superman mode
+ p_super = subparsers.add_parser(
+ 'super',
+ help='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'
+ '\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 "
+ "Example: gita super myrepo1 diff --name-only --staged "
+ "Another: gita super checkout master ")
+ p_super.set_defaults(func=f_super)
+
+ # sub-commands that fit boilerplate
+ cmds = utils.get_cmds_from_files()
+ for name, data in cmds.items():
+ help = data.get('help')
+ cmd = data.get('cmd') or name
+ if data.get('allow_all'):
+ choices = utils.get_choices()
+ nargs = '*'
+ help += ' for all repos or'
+ else:
+ choices = utils.get_repos().keys() | utils.get_groups().keys()
+ nargs = '+'
+ help += ' for the chosen repo(s) or group(s)'
+ sp = subparsers.add_parser(name, help=help)
+ sp.add_argument('repo', nargs=nargs, choices=choices, help=help)
+ sp.set_defaults(func=f_git_cmd, cmd=cmd.split())
+
+ args = p.parse_args(argv)
+
+ args.async_blacklist = {
+ name
+ for name, data in cmds.items() if data.get('disable_async')
+ }
+
+ if 'func' in args:
+ args.func(args)
+ else:
+ p.print_help() # pragma: no cover
+
+
+if __name__ == '__main__':
+ main() # pragma: no cover