diff options
Diffstat (limited to 'gcl')
-rwxr-xr-x | gcl | 188 |
1 files changed, 188 insertions, 0 deletions
@@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# git config linker (gcl) v1.0 +# Written by Martin Fischer <martin@push-f.com> and licensed under MIT. +import sys +import os +import configparser +import subprocess +import shutil + +GITCONFIGS = 'gitconfigs/' + +class colors: + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + +def print_usage(): + print('''git config linker (gcl): + + gcl status check the status of the directories in . + + gcl link <repo>... move <repo>/.git/config to gitconfigs/<repo> + (leaving a symlink behind) + + gcl clone list all configs that are not cloned + + gcl clone <config>... clone a repository using gitconfigs/<config> + + gcl mv <src> <dest> rename a repo and its config and update the symlink + + gcl unlink <repo>... undo "gcl link <repo>" + (only needed if you want to stop using gcl) +''') + sys.exit() + +args = sys.argv[1:] +if len(args) == 0: + print_usage() + +if not os.path.exists(GITCONFIGS + '/.git/config'): + sys.exit(f'expected {GITCONFIGS} to be a git repository') + +def get_link_error(path): + configpath = path + '/.git/config' + if not os.path.exists(configpath): + missing = os.path.relpath(os.path.realpath(configpath)) + return colors.FAIL + 'BROKEN LINK' + colors.ENDC + f' ({missing} does not exist)' + elif os.path.islink(configpath) and os.path.relpath(os.path.realpath(configpath)) != GITCONFIGS + path: + missing = os.path.relpath(os.path.realpath(configpath)) + return colors.WARNING + 'LINK MISMATCH' + colors.ENDC + f' ({missing})' + +def get_repo_status(path): + errors = [] + link_error = get_link_error(path) + if link_error: + errors.append(link_error) + + if hasremote(path + '/.git/config'): + try: + if subprocess.check_output(['git', 'branch', '-r', '--contains', 'HEAD'], cwd=path, stderr=subprocess.DEVNULL) == b'': + errors.append(colors.WARNING + 'UNPUSHED' + colors.ENDC) + except subprocess.CalledProcessError as err: + if subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=path, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) == 0: + # the first command did not just fail because there are no commits yet + raise err + else: + errors.append(colors.FAIL + 'NO REMOTE' + colors.ENDC) + + if subprocess.check_output(['git', 'status', '--porcelain'], cwd=path) != b'': + errors.append(colors.WARNING + 'DIRTY' + colors.ENDC) + return ', '.join(errors) + +def print_repo_table(reponames): + maxlen = max([len(s) for s in reponames]) + for reponame in reponames: + print(f'\t{reponame.ljust(maxlen)} {get_repo_status(reponame)}') + print() + +linked = [] +unlinked = [] +not_a_repo = [] + +def hasremote(path): + config = configparser.ConfigParser() + config.read(path) + return any([':' in config[s].get('url','') for s in config.sections() if s.startswith('remote')]) + +def scan(): + for filename in sorted(os.listdir()): + if not os.path.isdir(filename): + continue + + configpath = filename + '/.git/config' + if os.path.islink(configpath): + linked.append(filename) + elif not os.path.exists(configpath): + not_a_repo.append(filename) + else: + unlinked.append(filename) + +cmd = args.pop(0) + +if cmd == 'status': + scan() + if not_a_repo: + print(f"non-repositories:\n") + print('\t' + '\n\t'.join(not_a_repo)) + print() + if unlinked: + print('unlinked repos:\n (link with "gcl link <name>...")\n') + print_repo_table(unlinked) + if linked: + print('linked repos:\n') + print_repo_table(linked) + +elif cmd == 'link': + if len(args) == 0: + sys.exit('Nothing specified, nothing linked.') + + for repo in args: + configpath = repo + '/.git/config' + if not os.path.isfile(configpath): + print(f"skipping '{repo}' because {configpath} is not a file (maybe already linked?)") + continue + if os.path.exists(GITCONFIGS + repo): + sys.exit(f"aborting since {GITCONFIGS + repo} already exists") + os.rename(configpath, GITCONFIGS + repo) + os.symlink('../../' + GITCONFIGS + repo, configpath) + +elif cmd == 'unlink': + if len(args) == 0: + sys.exit('Nothing specified, nothing unlinked.') + + for repo in args: + configpath = repo + '/.git/config' + if not os.path.islink(configpath): + continue + if not os.path.isfile(configpath): + print(f"skipping '{repo}' because {config} is not a file") + continue + + os.unlink(configpath) + shutil.copyfile(GITCONFIGS + repo, configpath) + os.unlink(GITCONFIGS + repo) + +elif cmd == 'clone': + if len(args) == 0: + scan() + available = set(os.listdir(GITCONFIGS)) - set(linked) - set(['.git']) + if available: + print('uncloned configs:\n (clone with "gcl clone <name>...")\n') + for l in sorted(available): + print(' ' + l) + print() + for arg in args: + if os.path.exists(arg): + print(f"skipping '{arg}' because the directory already exists") + continue + if not os.path.isfile(GITCONFIGS + arg): + print(f"skipping '{arg}' because {GITCONFIGS}/{arg} doesn't exist") + continue + subprocess.run(['git', 'init', arg]) + os.unlink(arg + '/.git/config') + os.symlink('../../' + GITCONFIGS + arg, arg + '/.git/config') + subprocess.run(['git', 'pull'], cwd=arg) + +elif cmd == 'mv': + if len(args) != 2: + print_usage() + src, dest = args + if '/' in src or '/' in dest: + sys.exit('fatal: src and dest may not contain slashes') + if src == 'gitconfigs': + sys.exit('fatal: cannot move gitconfigs') + link_error = get_link_error(src) + if link_error: + sys.exit('aborting because src is not properly linked: ' + link_error) + if os.path.exists(dest): + sys.exit(f"fatal: repo destination '{dest}' already exists") + if os.path.exists(GITCONFIGS + dest): + sys.exit(f"fatal: config destination '{GITCONFIGS + dest}' already exists") + + os.rename(src, dest) + os.rename(GITCONFIGS + src, GITCONFIGS + dest) + os.unlink(dest + '/.git/config') + os.symlink('../../' + GITCONFIGS + dest, dest + '/.git/config') +else: + print_usage() |